diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2021-11-13 12:23:49 +0900 |
|---|---|---|
| committer | syuilo <Syuilotan@yahoo.co.jp> | 2021-11-13 12:23:49 +0900 |
| commit | 2795fe457909c687f668d020ef65d52abc3182fb (patch) | |
| tree | 0a52e4e4d854333496fcc487560c93c3de5d5eb5 /src/client/pages | |
| parent | Merge branch 'develop' (diff) | |
| parent | 12.96.0 (diff) | |
| download | misskey-2795fe457909c687f668d020ef65d52abc3182fb.tar.gz misskey-2795fe457909c687f668d020ef65d52abc3182fb.tar.bz2 misskey-2795fe457909c687f668d020ef65d52abc3182fb.zip | |
Merge branch 'develop'
Diffstat (limited to 'src/client/pages')
174 files changed, 0 insertions, 25640 deletions
diff --git a/src/client/pages/_error_.vue b/src/client/pages/_error_.vue deleted file mode 100644 index d1cefad8dd..0000000000 --- a/src/client/pages/_error_.vue +++ /dev/null @@ -1,94 +0,0 @@ -<template> -<MkLoading v-if="!loaded" /> -<transition :name="$store.state.animation ? 'zoom' : ''" appear> - <div class="mjndxjch" v-show="loaded"> - <img src="https://xn--931a.moe/assets/error.jpg" class="_ghost"/> - <p><b><i class="fas fa-exclamation-triangle"></i> {{ $ts.pageLoadError }}</b></p> - <p v-if="version === meta.version">{{ $ts.pageLoadErrorDescription }}</p> - <p v-else-if="serverIsDead">{{ $ts.serverIsDead }}</p> - <template v-else> - <p>{{ $ts.newVersionOfClientAvailable }}</p> - <p>{{ $ts.youShouldUpgradeClient }}</p> - <MkButton @click="reload" class="button primary">{{ $ts.reload }}</MkButton> - </template> - <p><MkA to="/docs/general/troubleshooting" class="_link">{{ $ts.troubleshooting }}</MkA></p> - <p v-if="error" class="error">ERROR: {{ error }}</p> - </div> -</transition> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import MkButton from '@client/components/ui/button.vue'; -import * as symbols from '@client/symbols'; -import { version } from '@client/config'; -import * as os from '@client/os'; -import { unisonReload } from '@client/scripts/unison-reload'; - -export default defineComponent({ - components: { - MkButton, - }, - props: { - error: { - required: false, - } - }, - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.error, - icon: 'fas fa-exclamation-triangle' - }, - loaded: false, - serverIsDead: false, - meta: {} as any, - version, - }; - }, - created() { - os.api('meta', { - detail: false - }).then(meta => { - this.loaded = true; - this.serverIsDead = false; - this.meta = meta; - localStorage.setItem('v', meta.version); - }, () => { - this.loaded = true; - this.serverIsDead = true; - }); - }, - methods: { - reload() { - unisonReload(); - }, - }, -}); -</script> - -<style lang="scss" scoped> -.mjndxjch { - padding: 32px; - text-align: center; - - > p { - margin: 0 0 12px 0; - } - - > .button { - margin: 8px auto; - } - - > img { - vertical-align: bottom; - height: 128px; - margin-bottom: 24px; - border-radius: 16px; - } - - > .error { - opacity: 0.7; - } -} -</style> diff --git a/src/client/pages/_loading_.vue b/src/client/pages/_loading_.vue deleted file mode 100644 index 34ecaf9b33..0000000000 --- a/src/client/pages/_loading_.vue +++ /dev/null @@ -1,10 +0,0 @@ -<template> -<MkLoading/> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import * as os from '@client/os'; - -export default defineComponent({}); -</script> diff --git a/src/client/pages/about-misskey.vue b/src/client/pages/about-misskey.vue deleted file mode 100644 index d2c0ec0550..0000000000 --- a/src/client/pages/about-misskey.vue +++ /dev/null @@ -1,238 +0,0 @@ -<template> -<div style="overflow: clip;"> - <FormBase class="znqjceqz"> - <div id="debug"></div> - <section class="_debobigegoItem about"> - <div class="_debobigegoPanel panel" :class="{ playing: easterEggEngine != null }" ref="about"> - <img src="/static-assets/client/about-icon.png" alt="" class="icon" @load="iconLoaded" draggable="false" @click="gravity"/> - <div class="misskey">Misskey</div> - <div class="version">v{{ version }}</div> - <span class="emoji" v-for="emoji in easterEggEmojis" :key="emoji.id" :data-physics-x="emoji.left" :data-physics-y="emoji.top" :class="{ _physics_circle_: !emoji.emoji.startsWith(':') }"><MkEmoji class="emoji" :emoji="emoji.emoji" :custom-emojis="$instance.emojis" :is-reaction="false" :normal="true" :no-style="true"/></span> - </div> - </section> - <section class="_debobigegoItem" style="text-align: center; padding: 0 16px;"> - {{ $ts._aboutMisskey.about }}<br><MkA class="_link" to="/docs/general/misskey">{{ $ts.learnMore }}</MkA> - </section> - <FormGroup> - <FormLink to="https://github.com/misskey-dev/misskey" external> - <template #icon><i class="fas fa-code"></i></template> - {{ $ts._aboutMisskey.source }} - <template #suffix>GitHub</template> - </FormLink> - <FormLink to="https://crowdin.com/project/misskey" external> - <template #icon><i class="fas fa-language"></i></template> - {{ $ts._aboutMisskey.translation }} - <template #suffix>Crowdin</template> - </FormLink> - <FormLink to="https://www.patreon.com/syuilo" external> - <template #icon><i class="fas fa-hand-holding-medical"></i></template> - {{ $ts._aboutMisskey.donate }} - <template #suffix>Patreon</template> - </FormLink> - </FormGroup> - <FormGroup> - <template #label>{{ $ts._aboutMisskey.contributors }}</template> - <FormLink to="https://github.com/syuilo" external>@syuilo</FormLink> - <FormLink to="https://github.com/AyaMorisawa" external>@AyaMorisawa</FormLink> - <FormLink to="https://github.com/mei23" external>@mei23</FormLink> - <FormLink to="https://github.com/acid-chicken" external>@acid-chicken</FormLink> - <FormLink to="https://github.com/tamaina" external>@tamaina</FormLink> - <FormLink to="https://github.com/rinsuki" external>@rinsuki</FormLink> - <FormLink to="https://github.com/Xeltica" external>@Xeltica</FormLink> - <FormLink to="https://github.com/u1-liquid" external>@u1-liquid</FormLink> - <FormLink to="https://github.com/marihachi" external>@marihachi</FormLink> - <template #caption><MkLink url="https://github.com/misskey-dev/misskey/graphs/contributors">{{ $ts._aboutMisskey.allContributors }}</MkLink></template> - </FormGroup> - <FormGroup> - <template #label><Mfm text="[jelly โค]"/> {{ $ts._aboutMisskey.patrons }}</template> - <FormKeyValueView v-for="patron in patrons" :key="patron"><template #key>{{ patron }}</template></FormKeyValueView> - <template #caption>{{ $ts._aboutMisskey.morePatrons }}</template> - </FormGroup> - </FormBase> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import { version } from '@client/config'; -import FormLink from '@client/components/debobigego/link.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import FormKeyValueView from '@client/components/debobigego/key-value-view.vue'; -import MkLink from '@client/components/link.vue'; -import { physics } from '@client/scripts/physics'; -import * as symbols from '@client/symbols'; - -const patrons = [ - 'Satsuki Yanagi', - 'noellabo', - 'mametsuko', - 'AureoleArk', - 'Gargron', - 'Nokotaro Takeda', - 'Suji Yan', - 'Hekovic', - 'Gitmo Life Services', - 'nenohi', - 'naga_rus', - 'Melilot', - 'Efertone', - 'oi_yekssim', - 'nanami kan', - 'motcha', - 'dansup', - 'Quinton Macejkovic', - 'YUKIMOCHI', - 'mewl hayabusa', - 'makokunsan', - 'Peter G.', - 'Nesakko', - 'regtan', - '่ฆๅฝใใชใฟ', - 'natalie', - 'Jerry', - 'takimura', - 'sikyosyounin', - 'YuzuRyo61', - 'sheeta.s', - 'osapon', - 'mkatze', - 'CG', - 'nafuchoco', - 'Takumi Sugita', - 'chidori ninokura', - 'mydarkstar', - 'kiritan', - 'kabo2468y', - 'weepjp', - 'Liaizon Wakest', - 'Steffen K9', - 'Roujo', - 'uroco @99', - 'totokoro', - 'public_yusuke', - 'wara', - 'S Y', - 'Denshi', - 'Osushimaru', - 'ๅดๆตฅ', - 'DignifiedSilence', - 't_w', -]; - -export default defineComponent({ - components: { - FormBase, - FormGroup, - FormLink, - FormKeyValueView, - MkLink, - }, - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.aboutMisskey, - icon: null - }, - version, - patrons, - easterEggReady: false, - easterEggEmojis: [], - easterEggEngine: null, - } - }, - - beforeUnmount() { - if (this.easterEggEngine) { - this.easterEggEngine.stop(); - } - }, - - methods: { - iconLoaded() { - const emojis = this.$store.state.reactions; - const containerWidth = this.$refs.about.offsetWidth; - for (let i = 0; i < 32; i++) { - this.easterEggEmojis.push({ - id: i.toString(), - top: -(128 + (Math.random() * 256)), - left: (Math.random() * containerWidth), - emoji: emojis[Math.floor(Math.random() * emojis.length)], - }); - } - - this.$nextTick(() => { - this.easterEggReady = true; - }); - }, - - gravity() { - if (!this.easterEggReady) return; - this.easterEggReady = false; - this.easterEggEngine = physics(this.$refs.about); - } - } -}); -</script> - -<style lang="scss" scoped> -.znqjceqz { - max-width: 800px; - box-sizing: border-box; - margin: 0 auto; - - > .about { - > .panel { - position: relative; - text-align: center; - padding: 16px; - - &.playing { - &, * { - user-select: none; - } - - * { - will-change: transform; - } - - > .emoji { - visibility: visible; - } - } - - > .icon { - display: block; - width: 100px; - margin: 0 auto; - border-radius: 16px; - } - - > .misskey { - margin: 0.75em auto 0 auto; - width: max-content; - } - - > .version { - margin: 0 auto; - width: max-content; - opacity: 0.5; - } - - > .emoji { - position: absolute; - top: 0; - left: 0; - visibility: hidden; - - > .emoji { - pointer-events: none; - font-size: 24px; - width: 24px; - } - } - } - } -} -</style> diff --git a/src/client/pages/about.vue b/src/client/pages/about.vue deleted file mode 100644 index 2c580c293a..0000000000 --- a/src/client/pages/about.vue +++ /dev/null @@ -1,123 +0,0 @@ -<template> -<FormBase> - <div class="_debobigegoItem"> - <div class="_debobigegoPanel fwhjspax"> - <img :src="$instance.iconUrl || $instance.faviconUrl || '/favicon.ico'" alt="" class="icon"/> - <span class="name">{{ $instance.name || host }}</span> - </div> - </div> - - <FormTextarea readonly :value="$instance.description"> - </FormTextarea> - - <FormGroup> - <FormKeyValueView> - <template #key>Misskey</template> - <template #value>v{{ version }}</template> - </FormKeyValueView> - <FormLink to="/about-misskey">{{ $ts.aboutMisskey }}</FormLink> - </FormGroup> - - <FormGroup> - <FormKeyValueView> - <template #key>{{ $ts.administrator }}</template> - <template #value>{{ $instance.maintainerName }}</template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>{{ $ts.contact }}</template> - <template #value>{{ $instance.maintainerEmail }}</template> - </FormKeyValueView> - </FormGroup> - - <FormLink v-if="$instance.tosUrl" :to="$instance.tosUrl" external>{{ $ts.tos }}</FormLink> - - <FormSuspense :p="initStats"> - <FormGroup> - <template #label>{{ $ts.statistics }}</template> - <FormKeyValueView> - <template #key>{{ $ts.users }}</template> - <template #value>{{ number(stats.originalUsersCount) }}</template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>{{ $ts.notes }}</template> - <template #value>{{ number(stats.originalNotesCount) }}</template> - </FormKeyValueView> - </FormGroup> - </FormSuspense> - - <FormGroup> - <template #label>Well-known resources</template> - <FormLink :to="`/.well-known/host-meta`" external>host-meta</FormLink> - <FormLink :to="`/.well-known/host-meta.json`" external>host-meta.json</FormLink> - <FormLink :to="`/.well-known/nodeinfo`" external>nodeinfo</FormLink> - <FormLink :to="`/robots.txt`" external>robots.txt</FormLink> - <FormLink :to="`/manifest.json`" external>manifest.json</FormLink> - </FormGroup> -</FormBase> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import { version, instanceName } from '@client/config'; -import FormLink from '@client/components/debobigego/link.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import FormKeyValueView from '@client/components/debobigego/key-value-view.vue'; -import FormTextarea from '@client/components/debobigego/textarea.vue'; -import FormSuspense from '@client/components/debobigego/suspense.vue'; -import * as os from '@client/os'; -import number from '@client/filters/number'; -import * as symbols from '@client/symbols'; -import { host } from '@client/config'; - -export default defineComponent({ - components: { - FormBase, - FormGroup, - FormLink, - FormKeyValueView, - FormTextarea, - FormSuspense, - }, - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.instanceInfo, - icon: 'fas fa-info-circle' - }, - host, - version, - instanceName, - stats: null, - initStats: () => os.api('stats', { - }).then((stats) => { - this.stats = stats; - }) - } - }, - - methods: { - number - } -}); -</script> - -<style lang="scss" scoped> -.fwhjspax { - padding: 16px; - text-align: center; - - > .icon { - display: block; - margin: auto; - height: 64px; - border-radius: 8px; - } - - > .name { - display: block; - margin-top: 12px; - } -} -</style> diff --git a/src/client/pages/admin/abuses.vue b/src/client/pages/admin/abuses.vue deleted file mode 100644 index 29da8cc2c5..0000000000 --- a/src/client/pages/admin/abuses.vue +++ /dev/null @@ -1,170 +0,0 @@ -<template> -<div class="lcixvhis"> - <div class="_section reports"> - <div class="_content"> - <div class="inputs" style="display: flex;"> - <MkSelect v-model="state" style="margin: 0; flex: 1;"> - <template #label>{{ $ts.state }}</template> - <option value="all">{{ $ts.all }}</option> - <option value="unresolved">{{ $ts.unresolved }}</option> - <option value="resolved">{{ $ts.resolved }}</option> - </MkSelect> - <MkSelect v-model="targetUserOrigin" style="margin: 0; flex: 1;"> - <template #label>{{ $ts.targetUserOrigin }}</template> - <option value="combined">{{ $ts.all }}</option> - <option value="local">{{ $ts.local }}</option> - <option value="remote">{{ $ts.remote }}</option> - </MkSelect> - <MkSelect v-model="reporterOrigin" style="margin: 0; flex: 1;"> - <template #label>{{ $ts.reporterOrigin }}</template> - <option value="combined">{{ $ts.all }}</option> - <option value="local">{{ $ts.local }}</option> - <option value="remote">{{ $ts.remote }}</option> - </MkSelect> - </div> - <!-- TODO - <div class="inputs" style="display: flex; padding-top: 1.2em;"> - <MkInput v-model="searchUsername" style="margin: 0; flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.reports.reload()"> - <span>{{ $ts.username }}</span> - </MkInput> - <MkInput v-model="searchHost" style="margin: 0; flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.reports.reload()" :disabled="pagination.params().origin === 'local'"> - <span>{{ $ts.host }}</span> - </MkInput> - </div> - --> - - <MkPagination :pagination="pagination" #default="{items}" ref="reports" style="margin-top: var(--margin);"> - <div class="bcekxzvu _card _gap" v-for="report in items" :key="report.id"> - <div class="_content target"> - <MkAvatar class="avatar" :user="report.targetUser" :show-indicator="true"/> - <div class="info"> - <MkUserName class="name" :user="report.targetUser"/> - <div class="acct">@{{ acct(report.targetUser) }}</div> - </div> - </div> - <div class="_content"> - <div> - <Mfm :text="report.comment"/> - </div> - <hr> - <div>Reporter: <MkAcct :user="report.reporter"/></div> - <div><MkTime :time="report.createdAt"/></div> - </div> - <div class="_footer"> - <div v-if="report.assignee">Assignee: <MkAcct :user="report.assignee"/></div> - <MkButton @click="resolve(report)" primary v-if="!report.resolved">{{ $ts.abuseMarkAsResolved }}</MkButton> - </div> - </div> - </MkPagination> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import { parseAcct } from '@/misc/acct'; -import MkButton from '@client/components/ui/button.vue'; -import MkInput from '@client/components/form/input.vue'; -import MkSelect from '@client/components/form/select.vue'; -import MkPagination from '@client/components/ui/pagination.vue'; -import { acct } from '@client/filters/user'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - MkButton, - MkInput, - MkSelect, - MkPagination, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.abuseReports, - icon: 'fas fa-exclamation-circle', - bg: 'var(--bg)', - }, - searchUsername: '', - searchHost: '', - state: 'unresolved', - reporterOrigin: 'combined', - targetUserOrigin: 'combined', - pagination: { - endpoint: 'admin/abuse-user-reports', - limit: 10, - params: () => ({ - state: this.state, - reporterOrigin: this.reporterOrigin, - targetUserOrigin: this.targetUserOrigin, - }), - }, - } - }, - - watch: { - state() { - this.$refs.reports.reload(); - }, - - reporterOrigin() { - this.$refs.reports.reload(); - }, - - targetUserOrigin() { - this.$refs.reports.reload(); - }, - }, - - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - methods: { - acct, - - resolve(report) { - os.apiWithDialog('admin/resolve-abuse-user-report', { - reportId: report.id, - }).then(() => { - this.$refs.reports.removeItem(item => item.id === report.id); - }); - }, - } -}); -</script> - -<style lang="scss" scoped> -.lcixvhis { - margin: var(--margin); -} - -.bcekxzvu { - > .target { - display: flex; - width: 100%; - box-sizing: border-box; - text-align: left; - align-items: center; - - > .avatar { - width: 42px; - height: 42px; - } - - > .info { - margin-left: 0.3em; - padding: 0 8px; - flex: 1; - - > .name { - font-weight: bold; - } - } - } -} -</style> diff --git a/src/client/pages/admin/ads.vue b/src/client/pages/admin/ads.vue deleted file mode 100644 index 4d39bb4e40..0000000000 --- a/src/client/pages/admin/ads.vue +++ /dev/null @@ -1,138 +0,0 @@ -<template> -<div class="uqshojas"> - <section class="_card _gap ads" v-for="ad in ads"> - <div class="_content ad"> - <MkAd v-if="ad.url" :specify="ad"/> - <MkInput v-model="ad.url" type="url"> - <template #label>URL</template> - </MkInput> - <MkInput v-model="ad.imageUrl"> - <template #label>{{ $ts.imageUrl }}</template> - </MkInput> - <div style="margin: 32px 0;"> - <MkRadio v-model="ad.place" value="square">square</MkRadio> - <MkRadio v-model="ad.place" value="horizontal">horizontal</MkRadio> - <MkRadio v-model="ad.place" value="horizontal-big">horizontal-big</MkRadio> - </div> - <!-- - <div style="margin: 32px 0;"> - {{ $ts.priority }} - <MkRadio v-model="ad.priority" value="high">{{ $ts.high }}</MkRadio> - <MkRadio v-model="ad.priority" value="middle">{{ $ts.middle }}</MkRadio> - <MkRadio v-model="ad.priority" value="low">{{ $ts.low }}</MkRadio> - </div> - --> - <MkInput v-model="ad.ratio" type="number"> - <template #label>{{ $ts.ratio }}</template> - </MkInput> - <MkInput v-model="ad.expiresAt" type="date"> - <template #label>{{ $ts.expiration }}</template> - </MkInput> - <MkTextarea v-model="ad.memo"> - <template #label>{{ $ts.memo }}</template> - </MkTextarea> - <div class="buttons"> - <MkButton class="button" inline @click="save(ad)" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton> - <MkButton class="button" inline @click="remove(ad)" danger><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton> - </div> - </div> - </section> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import MkButton from '@client/components/ui/button.vue'; -import MkInput from '@client/components/form/input.vue'; -import MkTextarea from '@client/components/form/textarea.vue'; -import MkRadio from '@client/components/form/radio.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - MkButton, - MkInput, - MkTextarea, - MkRadio, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.ads, - icon: 'fas fa-audio-description', - bg: 'var(--bg)', - actions: [{ - asFullButton: true, - icon: 'fas fa-plus', - text: this.$ts.add, - handler: this.add, - }], - }, - ads: [], - } - }, - - created() { - os.api('admin/ad/list').then(ads => { - this.ads = ads; - }); - }, - - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - methods: { - add() { - this.ads.unshift({ - id: null, - memo: '', - place: 'square', - priority: 'middle', - ratio: 1, - url: '', - imageUrl: null, - expiresAt: null, - }); - }, - - remove(ad) { - os.dialog({ - type: 'warning', - text: this.$t('removeAreYouSure', { x: ad.url }), - showCancelButton: true - }).then(({ canceled }) => { - if (canceled) return; - this.ads = this.ads.filter(x => x != ad); - os.apiWithDialog('admin/ad/delete', { - id: ad.id - }); - }); - }, - - save(ad) { - if (ad.id == null) { - os.apiWithDialog('admin/ad/create', { - ...ad, - expiresAt: new Date(ad.expiresAt).getTime() - }); - } else { - os.apiWithDialog('admin/ad/update', { - ...ad, - expiresAt: new Date(ad.expiresAt).getTime() - }); - } - } - } -}); -</script> - -<style lang="scss" scoped> -.uqshojas { - margin: var(--margin); -} -</style> diff --git a/src/client/pages/admin/announcements.vue b/src/client/pages/admin/announcements.vue deleted file mode 100644 index 4ace515b0b..0000000000 --- a/src/client/pages/admin/announcements.vue +++ /dev/null @@ -1,125 +0,0 @@ -<template> -<div class="ztgjmzrw"> - <section class="_card _gap announcements" v-for="announcement in announcements"> - <div class="_content announcement"> - <MkInput v-model="announcement.title"> - <template #label>{{ $ts.title }}</template> - </MkInput> - <MkTextarea v-model="announcement.text"> - <template #label>{{ $ts.text }}</template> - </MkTextarea> - <MkInput v-model="announcement.imageUrl"> - <template #label>{{ $ts.imageUrl }}</template> - </MkInput> - <p v-if="announcement.reads">{{ $t('nUsersRead', { n: announcement.reads }) }}</p> - <div class="buttons"> - <MkButton class="button" inline @click="save(announcement)" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton> - <MkButton class="button" inline @click="remove(announcement)"><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton> - </div> - </div> - </section> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import MkButton from '@client/components/ui/button.vue'; -import MkInput from '@client/components/form/input.vue'; -import MkTextarea from '@client/components/form/textarea.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - MkButton, - MkInput, - MkTextarea, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.announcements, - icon: 'fas fa-broadcast-tower', - bg: 'var(--bg)', - actions: [{ - asFullButton: true, - icon: 'fas fa-plus', - text: this.$ts.add, - handler: this.add, - }], - }, - announcements: [], - } - }, - - created() { - os.api('admin/announcements/list').then(announcements => { - this.announcements = announcements; - }); - }, - - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - methods: { - add() { - this.announcements.unshift({ - id: null, - title: '', - text: '', - imageUrl: null - }); - }, - - remove(announcement) { - os.dialog({ - type: 'warning', - text: this.$t('removeAreYouSure', { x: announcement.title }), - showCancelButton: true - }).then(({ canceled }) => { - if (canceled) return; - this.announcements = this.announcements.filter(x => x != announcement); - os.api('admin/announcements/delete', announcement); - }); - }, - - save(announcement) { - if (announcement.id == null) { - os.api('admin/announcements/create', announcement).then(() => { - os.dialog({ - type: 'success', - text: this.$ts.saved - }); - }).catch(e => { - os.dialog({ - type: 'error', - text: e - }); - }); - } else { - os.api('admin/announcements/update', announcement).then(() => { - os.dialog({ - type: 'success', - text: this.$ts.saved - }); - }).catch(e => { - os.dialog({ - type: 'error', - text: e - }); - }); - } - } - } -}); -</script> - -<style lang="scss" scoped> -.ztgjmzrw { - margin: var(--margin); -} -</style> diff --git a/src/client/pages/admin/bot-protection.vue b/src/client/pages/admin/bot-protection.vue deleted file mode 100644 index 731f114cc2..0000000000 --- a/src/client/pages/admin/bot-protection.vue +++ /dev/null @@ -1,138 +0,0 @@ -<template> -<FormBase> - <FormSuspense :p="init"> - <FormRadios v-model="provider"> - <template #desc><i class="fas fa-shield-alt"></i> {{ $ts.botProtection }}</template> - <option :value="null">{{ $ts.none }} ({{ $ts.notRecommended }})</option> - <option value="hcaptcha">hCaptcha</option> - <option value="recaptcha">reCAPTCHA</option> - </FormRadios> - - <template v-if="provider === 'hcaptcha'"> - <div class="_debobigegoItem _debobigegoNoConcat" v-sticky-container> - <div class="_debobigegoLabel">hCaptcha</div> - <div class="main"> - <FormInput v-model="hcaptchaSiteKey"> - <template #prefix><i class="fas fa-key"></i></template> - <span>{{ $ts.hcaptchaSiteKey }}</span> - </FormInput> - <FormInput v-model="hcaptchaSecretKey"> - <template #prefix><i class="fas fa-key"></i></template> - <span>{{ $ts.hcaptchaSecretKey }}</span> - </FormInput> - </div> - </div> - <div class="_debobigegoItem _debobigegoNoConcat" v-sticky-container> - <div class="_debobigegoLabel">{{ $ts.preview }}</div> - <div class="_debobigegoPanel" style="padding: var(--debobigegoContentHMargin);"> - <MkCaptcha provider="hcaptcha" :sitekey="hcaptchaSiteKey || '10000000-ffff-ffff-ffff-000000000001'"/> - </div> - </div> - </template> - <template v-else-if="provider === 'recaptcha'"> - <div class="_debobigegoItem _debobigegoNoConcat" v-sticky-container> - <div class="_debobigegoLabel">reCAPTCHA</div> - <div class="main"> - <FormInput v-model="recaptchaSiteKey"> - <template #prefix><i class="fas fa-key"></i></template> - <span>{{ $ts.recaptchaSiteKey }}</span> - </FormInput> - <FormInput v-model="recaptchaSecretKey"> - <template #prefix><i class="fas fa-key"></i></template> - <span>{{ $ts.recaptchaSecretKey }}</span> - </FormInput> - </div> - </div> - <div v-if="recaptchaSiteKey" class="_debobigegoItem _debobigegoNoConcat" v-sticky-container> - <div class="_debobigegoLabel">{{ $ts.preview }}</div> - <div class="_debobigegoPanel" style="padding: var(--debobigegoContentHMargin);"> - <MkCaptcha provider="recaptcha" :sitekey="recaptchaSiteKey"/> - </div> - </div> - </template> - - <FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton> - </FormSuspense> -</FormBase> -</template> - -<script lang="ts"> -import { defineAsyncComponent, defineComponent } from 'vue'; -import FormRadios from '@client/components/debobigego/radios.vue'; -import FormInput from '@client/components/debobigego/input.vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import FormInfo from '@client/components/debobigego/info.vue'; -import FormSuspense from '@client/components/debobigego/suspense.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; -import { fetchInstance } from '@client/instance'; - -export default defineComponent({ - components: { - FormRadios, - FormInput, - FormBase, - FormGroup, - FormButton, - FormInfo, - FormSuspense, - MkCaptcha: defineAsyncComponent(() => import('@client/components/captcha.vue')), - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.botProtection, - icon: 'fas fa-shield-alt' - }, - provider: null, - enableHcaptcha: false, - hcaptchaSiteKey: null, - hcaptchaSecretKey: null, - enableRecaptcha: false, - recaptchaSiteKey: null, - recaptchaSecretKey: null, - } - }, - - async mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - methods: { - async init() { - const meta = await os.api('meta', { detail: true }); - this.enableHcaptcha = meta.enableHcaptcha; - this.hcaptchaSiteKey = meta.hcaptchaSiteKey; - this.hcaptchaSecretKey = meta.hcaptchaSecretKey; - this.enableRecaptcha = meta.enableRecaptcha; - this.recaptchaSiteKey = meta.recaptchaSiteKey; - this.recaptchaSecretKey = meta.recaptchaSecretKey; - - this.provider = this.enableHcaptcha ? 'hcaptcha' : this.enableRecaptcha ? 'recaptcha' : null; - - this.$watch(() => this.provider, () => { - this.enableHcaptcha = this.provider === 'hcaptcha'; - this.enableRecaptcha = this.provider === 'recaptcha'; - }); - }, - - save() { - os.apiWithDialog('admin/update-meta', { - enableHcaptcha: this.enableHcaptcha, - hcaptchaSiteKey: this.hcaptchaSiteKey, - hcaptchaSecretKey: this.hcaptchaSecretKey, - enableRecaptcha: this.enableRecaptcha, - recaptchaSiteKey: this.recaptchaSiteKey, - recaptchaSecretKey: this.recaptchaSecretKey, - }).then(() => { - fetchInstance(); - }); - } - } -}); -</script> diff --git a/src/client/pages/admin/database.vue b/src/client/pages/admin/database.vue deleted file mode 100644 index ffbeed8b30..0000000000 --- a/src/client/pages/admin/database.vue +++ /dev/null @@ -1,61 +0,0 @@ -<template> -<FormBase> - <FormSuspense :p="databasePromiseFactory" v-slot="{ result: database }"> - <FormGroup v-for="table in database" :key="table[0]"> - <template #label>{{ table[0] }}</template> - <FormKeyValueView> - <template #key>Size</template> - <template #value>{{ bytes(table[1].size) }}</template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>Records</template> - <template #value>{{ number(table[1].count) }}</template> - </FormKeyValueView> - </FormGroup> - </FormSuspense> -</FormBase> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import FormSuspense from '@client/components/debobigego/suspense.vue'; -import FormKeyValueView from '@client/components/debobigego/key-value-view.vue'; -import FormLink from '@client/components/debobigego/link.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; -import bytes from '@client/filters/bytes'; -import number from '@client/filters/number'; - -export default defineComponent({ - components: { - FormSuspense, - FormKeyValueView, - FormBase, - FormGroup, - FormLink, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.database, - icon: 'fas fa-database', - bg: 'var(--bg)', - }, - databasePromiseFactory: () => os.api('admin/get-table-stats', {}).then(res => Object.entries(res).sort((a, b) => b[1].size - a[1].size)), - } - }, - - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - methods: { - bytes, number, - } -}); -</script> diff --git a/src/client/pages/admin/email-settings.vue b/src/client/pages/admin/email-settings.vue deleted file mode 100644 index ebf724fcdd..0000000000 --- a/src/client/pages/admin/email-settings.vue +++ /dev/null @@ -1,128 +0,0 @@ -<template> -<FormBase> - <FormSuspense :p="init"> - <FormSwitch v-model="enableEmail">{{ $ts.enableEmail }}<template #desc>{{ $ts.emailConfigInfo }}</template></FormSwitch> - - <template v-if="enableEmail"> - <FormInput v-model="email" type="email"> - <span>{{ $ts.emailAddress }}</span> - </FormInput> - - <div class="_debobigegoItem _debobigegoNoConcat" v-sticky-container> - <div class="_debobigegoLabel">{{ $ts.smtpConfig }}</div> - <div class="main"> - <FormInput v-model="smtpHost"> - <span>{{ $ts.smtpHost }}</span> - </FormInput> - <FormInput v-model="smtpPort" type="number"> - <span>{{ $ts.smtpPort }}</span> - </FormInput> - <FormInput v-model="smtpUser"> - <span>{{ $ts.smtpUser }}</span> - </FormInput> - <FormInput v-model="smtpPass" type="password"> - <span>{{ $ts.smtpPass }}</span> - </FormInput> - <FormInfo>{{ $ts.emptyToDisableSmtpAuth }}</FormInfo> - <FormSwitch v-model="smtpSecure">{{ $ts.smtpSecure }}<template #desc>{{ $ts.smtpSecureInfo }}</template></FormSwitch> - </div> - </div> - - <FormButton @click="testEmail">{{ $ts.testEmail }}</FormButton> - </template> - - <FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton> - </FormSuspense> -</FormBase> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import FormSwitch from '@client/components/debobigego/switch.vue'; -import FormInput from '@client/components/debobigego/input.vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import FormInfo from '@client/components/debobigego/info.vue'; -import FormSuspense from '@client/components/debobigego/suspense.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; -import { fetchInstance } from '@client/instance'; - -export default defineComponent({ - components: { - FormSwitch, - FormInput, - FormBase, - FormGroup, - FormButton, - FormInfo, - FormSuspense, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.emailServer, - icon: 'fas fa-envelope', - bg: 'var(--bg)', - }, - enableEmail: false, - email: null, - smtpSecure: false, - smtpHost: '', - smtpPort: 0, - smtpUser: '', - smtpPass: '', - } - }, - - async mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - methods: { - async init() { - const meta = await os.api('meta', { detail: true }); - this.enableEmail = meta.enableEmail; - this.email = meta.email; - this.smtpSecure = meta.smtpSecure; - this.smtpHost = meta.smtpHost; - this.smtpPort = meta.smtpPort; - this.smtpUser = meta.smtpUser; - this.smtpPass = meta.smtpPass; - }, - - async testEmail() { - const { canceled, result: destination } = await os.dialog({ - title: this.$ts.destination, - input: { - placeholder: this.$instance.maintainerEmail - } - }); - if (canceled) return; - os.apiWithDialog('admin/send-email', { - to: destination, - subject: 'Test email', - text: 'Yo' - }); - }, - - save() { - os.apiWithDialog('admin/update-meta', { - enableEmail: this.enableEmail, - email: this.email, - smtpSecure: this.smtpSecure, - smtpHost: this.smtpHost, - smtpPort: this.smtpPort, - smtpUser: this.smtpUser, - smtpPass: this.smtpPass, - }).then(() => { - fetchInstance(); - }); - } - } -}); -</script> diff --git a/src/client/pages/admin/emoji-edit-dialog.vue b/src/client/pages/admin/emoji-edit-dialog.vue deleted file mode 100644 index 4854c69884..0000000000 --- a/src/client/pages/admin/emoji-edit-dialog.vue +++ /dev/null @@ -1,120 +0,0 @@ -<template> -<XModalWindow ref="dialog" - :width="370" - :with-ok-button="true" - @close="$refs.dialog.close()" - @closed="$emit('closed')" - @ok="ok()" -> - <template #header>:{{ emoji.name }}:</template> - - <div class="_monolithic_"> - <div class="yigymqpb _section"> - <img :src="emoji.url" class="img"/> - <MkInput class="_formBlock" v-model="name"> - <template #label>{{ $ts.name }}</template> - </MkInput> - <MkInput class="_formBlock" v-model="category" :datalist="categories"> - <template #label>{{ $ts.category }}</template> - </MkInput> - <MkInput class="_formBlock" v-model="aliases"> - <template #label>{{ $ts.tags }}</template> - <template #caption>{{ $ts.setMultipleBySeparatingWithSpace }}</template> - </MkInput> - <MkButton danger @click="del()"><i class="fas fa-trash-alt"></i> {{ $ts.delete }}</MkButton> - </div> - </div> -</XModalWindow> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import XModalWindow from '@client/components/ui/modal-window.vue'; -import MkButton from '@client/components/ui/button.vue'; -import MkInput from '@client/components/form/input.vue'; -import * as os from '@client/os'; -import { unique } from '../../../prelude/array'; - -export default defineComponent({ - components: { - XModalWindow, - MkButton, - MkInput, - }, - - props: { - emoji: { - required: true, - } - }, - - emits: ['done', 'closed'], - - data() { - return { - name: this.emoji.name, - category: this.emoji.category, - aliases: this.emoji.aliases?.join(' '), - categories: [], - } - }, - - created() { - os.api('meta', { detail: false }).then(({ emojis }) => { - this.categories = unique(emojis.map((x: any) => x.category || '').filter((x: string) => x !== '')); - }); - }, - - methods: { - ok() { - this.update(); - }, - - async update() { - await os.apiWithDialog('admin/emoji/update', { - id: this.emoji.id, - name: this.name, - category: this.category, - aliases: this.aliases.split(' '), - }); - - this.$emit('done', { - updated: { - name: this.name, - category: this.category, - aliases: this.aliases.split(' '), - } - }); - this.$refs.dialog.close(); - }, - - async del() { - const { canceled } = await os.dialog({ - type: 'warning', - text: this.$t('removeAreYouSure', { x: this.emoji.name }), - showCancelButton: true - }); - if (canceled) return; - - os.api('admin/emoji/remove', { - id: this.emoji.id - }).then(() => { - this.$emit('done', { - deleted: true - }); - this.$refs.dialog.close(); - }); - }, - } -}); -</script> - -<style lang="scss" scoped> -.yigymqpb { - > .img { - display: block; - height: 64px; - margin: 0 auto; - } -} -</style> diff --git a/src/client/pages/admin/emojis.vue b/src/client/pages/admin/emojis.vue deleted file mode 100644 index 5876db349e..0000000000 --- a/src/client/pages/admin/emojis.vue +++ /dev/null @@ -1,263 +0,0 @@ -<template> -<div class="ogwlenmc"> - <div class="local" v-if="tab === 'local'"> - <MkInput v-model="query" :debounce="true" type="search" style="margin: var(--margin);"> - <template #prefix><i class="fas fa-search"></i></template> - <template #label>{{ $ts.search }}</template> - </MkInput> - <MkPagination :pagination="pagination" ref="emojis"> - <template #empty><span>{{ $ts.noCustomEmojis }}</span></template> - <template #default="{items}"> - <div class="ldhfsamy"> - <button class="emoji _panel _button" v-for="emoji in items" :key="emoji.id" @click="edit(emoji)"> - <img :src="emoji.url" class="img" :alt="emoji.name"/> - <div class="body"> - <div class="name _monospace">{{ emoji.name }}</div> - <div class="info">{{ emoji.category }}</div> - </div> - </button> - </div> - </template> - </MkPagination> - </div> - - <div class="remote" v-else-if="tab === 'remote'"> - <MkInput v-model="queryRemote" :debounce="true" type="search" style="margin: var(--margin);"> - <template #prefix><i class="fas fa-search"></i></template> - <template #label>{{ $ts.search }}</template> - </MkInput> - <MkInput v-model="host" :debounce="true" style="margin: var(--margin);"> - <template #label>{{ $ts.host }}</template> - </MkInput> - <MkPagination :pagination="remotePagination" ref="remoteEmojis"> - <template #empty><span>{{ $ts.noCustomEmojis }}</span></template> - <template #default="{items}"> - <div class="ldhfsamy"> - <div class="emoji _panel _button" v-for="emoji in items" :key="emoji.id" @click="remoteMenu(emoji, $event)"> - <img :src="emoji.url" class="img" :alt="emoji.name"/> - <div class="body"> - <div class="name _monospace">{{ emoji.name }}</div> - <div class="info">{{ emoji.host }}</div> - </div> - </div> - </div> - </template> - </MkPagination> - </div> -</div> -</template> - -<script lang="ts"> -import { computed, defineComponent, toRef } from 'vue'; -import MkButton from '@client/components/ui/button.vue'; -import MkInput from '@client/components/form/input.vue'; -import MkPagination from '@client/components/ui/pagination.vue'; -import MkTab from '@client/components/tab.vue'; -import { selectFile } from '@client/scripts/select-file'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - MkTab, - MkButton, - MkInput, - MkPagination, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: computed(() => ({ - title: this.$ts.customEmojis, - icon: 'fas fa-laugh', - bg: 'var(--bg)', - actions: [{ - asFullButton: true, - icon: 'fas fa-plus', - text: this.$ts.addEmoji, - handler: this.add, - }], - tabs: [{ - active: this.tab === 'local', - title: this.$ts.local, - onClick: () => { this.tab = 'local'; }, - }, { - active: this.tab === 'remote', - title: this.$ts.remote, - onClick: () => { this.tab = 'remote'; }, - },] - })), - tab: 'local', - query: null, - queryRemote: null, - host: '', - pagination: { - endpoint: 'admin/emoji/list', - limit: 30, - params: computed(() => ({ - query: (this.query && this.query !== '') ? this.query : null - })) - }, - remotePagination: { - endpoint: 'admin/emoji/list-remote', - limit: 30, - params: computed(() => ({ - query: (this.queryRemote && this.queryRemote !== '') ? this.queryRemote : null, - host: (this.host && this.host !== '') ? this.host : null - })) - }, - } - }, - - async mounted() { - this.$emit('info', toRef(this, symbols.PAGE_INFO)); - }, - - methods: { - async add(e) { - const files = await selectFile(e.currentTarget || e.target, null, true); - - const promise = Promise.all(files.map(file => os.api('admin/emoji/add', { - fileId: file.id, - }))); - promise.then(() => { - this.$refs.emojis.reload(); - }); - os.promiseDialog(promise); - }, - - edit(emoji) { - os.popup(import('./emoji-edit-dialog.vue'), { - emoji: emoji - }, { - done: result => { - if (result.updated) { - this.$refs.emojis.replaceItem(item => item.id === emoji.id, { - ...emoji, - ...result.updated - }); - } else if (result.deleted) { - this.$refs.emojis.removeItem(item => item.id === emoji.id); - } - }, - }, 'closed'); - }, - - im(emoji) { - os.apiWithDialog('admin/emoji/copy', { - emojiId: emoji.id, - }); - }, - - remoteMenu(emoji, ev) { - os.popupMenu([{ - type: 'label', - text: ':' + emoji.name + ':', - }, { - text: this.$ts.import, - icon: 'fas fa-plus', - action: () => { this.im(emoji) } - }], ev.currentTarget || ev.target); - } - } -}); -</script> - -<style lang="scss" scoped> -.ogwlenmc { - > .local { - .empty { - margin: var(--margin); - } - - .ldhfsamy { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); - grid-gap: 12px; - margin: var(--margin); - - > .emoji { - display: flex; - align-items: center; - padding: 12px; - text-align: left; - - &:hover { - color: var(--accent); - } - - > .img { - width: 42px; - height: 42px; - } - - > .body { - padding: 0 0 0 8px; - white-space: nowrap; - overflow: hidden; - - > .name { - text-overflow: ellipsis; - overflow: hidden; - } - - > .info { - opacity: 0.5; - text-overflow: ellipsis; - overflow: hidden; - } - } - } - } - } - - > .remote { - .empty { - margin: var(--margin); - } - - .ldhfsamy { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); - grid-gap: 12px; - margin: var(--margin); - - > .emoji { - display: flex; - align-items: center; - padding: 12px; - text-align: left; - - &:hover { - color: var(--accent); - } - - > .img { - width: 32px; - height: 32px; - } - - > .body { - padding: 0 0 0 8px; - white-space: nowrap; - overflow: hidden; - - > .name { - text-overflow: ellipsis; - overflow: hidden; - } - - > .info { - opacity: 0.5; - font-size: 90%; - text-overflow: ellipsis; - overflow: hidden; - } - } - } - } - } -} -</style> diff --git a/src/client/pages/admin/file-dialog.vue b/src/client/pages/admin/file-dialog.vue deleted file mode 100644 index 02d83e5022..0000000000 --- a/src/client/pages/admin/file-dialog.vue +++ /dev/null @@ -1,129 +0,0 @@ -<template> -<XModalWindow ref="dialog" - :width="370" - @close="$refs.dialog.close()" - @closed="$emit('closed')" -> - <template #header v-if="file">{{ file.name }}</template> - <div class="cxqhhsmd" v-if="file"> - <div class="_section"> - <MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/> - <div class="info"> - <span style="margin-right: 1em;">{{ file.type }}</span> - <span>{{ bytes(file.size) }}</span> - <MkTime :time="file.createdAt" mode="detail" style="display: block;"/> - </div> - </div> - <div class="_section"> - <div class="_content"> - <MkSwitch @update:modelValue="toggleIsSensitive" v-model="isSensitive">NSFW</MkSwitch> - </div> - </div> - <div class="_section"> - <div class="_content"> - <MkButton full @click="showUser"><i class="fas fa-external-link-square-alt"></i> {{ $ts.user }}</MkButton> - <MkButton full danger @click="del"><i class="fas fa-trash-alt"></i> {{ $ts.delete }}</MkButton> - </div> - </div> - <div class="_section" v-if="info"> - <details class="_content rawdata"> - <pre><code>{{ JSON.stringify(info, null, 2) }}</code></pre> - </details> - </div> - </div> -</XModalWindow> -</template> - -<script lang="ts"> -import { computed, defineComponent } from 'vue'; -import MkButton from '@client/components/ui/button.vue'; -import MkSwitch from '@client/components/form/switch.vue'; -import XModalWindow from '@client/components/ui/modal-window.vue'; -import MkDriveFileThumbnail from '@client/components/drive-file-thumbnail.vue'; -import Progress from '@client/scripts/loading'; -import bytes from '@client/filters/bytes'; -import * as os from '@client/os'; - -export default defineComponent({ - components: { - MkButton, - MkSwitch, - XModalWindow, - MkDriveFileThumbnail, - }, - - props: { - fileId: { - required: true, - } - }, - - emits: ['closed'], - - data() { - return { - file: null, - info: null, - isSensitive: false, - }; - }, - - created() { - this.fetch(); - }, - - methods: { - async fetch() { - Progress.start(); - this.file = await os.api('drive/files/show', { fileId: this.fileId }); - this.info = await os.api('admin/drive/show-file', { fileId: this.fileId }); - this.isSensitive = this.file.isSensitive; - Progress.done(); - }, - - showUser() { - os.pageWindow(`/user-info/${this.file.userId}`); - }, - - async del() { - const { canceled } = await os.dialog({ - type: 'warning', - text: this.$t('removeAreYouSure', { x: this.file.name }), - showCancelButton: true - }); - if (canceled) return; - - os.apiWithDialog('drive/files/delete', { - fileId: this.file.id - }); - }, - - async toggleIsSensitive(v) { - await os.api('drive/files/update', { fileId: this.fileId, isSensitive: v }); - this.isSensitive = v; - }, - - bytes - } -}); -</script> - -<style lang="scss" scoped> -.cxqhhsmd { - > ._section { - > .thumbnail { - height: 150px; - max-width: 100%; - } - - > .info { - text-align: center; - margin-top: 8px; - } - - > .rawdata { - overflow: auto; - } - } -} -</style> diff --git a/src/client/pages/admin/files-settings.vue b/src/client/pages/admin/files-settings.vue deleted file mode 100644 index 8aefa9e90d..0000000000 --- a/src/client/pages/admin/files-settings.vue +++ /dev/null @@ -1,93 +0,0 @@ -<template> -<FormBase> - <FormSuspense :p="init"> - <FormSwitch v-model="cacheRemoteFiles"> - {{ $ts.cacheRemoteFiles }} - <template #desc>{{ $ts.cacheRemoteFilesDescription }}</template> - </FormSwitch> - - <FormSwitch v-model="proxyRemoteFiles"> - {{ $ts.proxyRemoteFiles }} - <template #desc>{{ $ts.proxyRemoteFilesDescription }}</template> - </FormSwitch> - - <FormInput v-model="localDriveCapacityMb" type="number"> - <span>{{ $ts.driveCapacityPerLocalAccount }}</span> - <template #suffix>MB</template> - <template #desc>{{ $ts.inMb }}</template> - </FormInput> - - <FormInput v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles"> - <span>{{ $ts.driveCapacityPerRemoteAccount }}</span> - <template #suffix>MB</template> - <template #desc>{{ $ts.inMb }}</template> - </FormInput> - - <FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton> - </FormSuspense> -</FormBase> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import FormSwitch from '@client/components/debobigego/switch.vue'; -import FormInput from '@client/components/debobigego/input.vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import FormSuspense from '@client/components/debobigego/suspense.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; -import { fetchInstance } from '@client/instance'; - -export default defineComponent({ - components: { - FormSwitch, - FormInput, - FormBase, - FormGroup, - FormButton, - FormSuspense, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.files, - icon: 'fas fa-cloud', - bg: 'var(--bg)', - }, - cacheRemoteFiles: false, - proxyRemoteFiles: false, - localDriveCapacityMb: 0, - remoteDriveCapacityMb: 0, - } - }, - - async mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - methods: { - async init() { - const meta = await os.api('meta', { detail: true }); - this.cacheRemoteFiles = meta.cacheRemoteFiles; - this.proxyRemoteFiles = meta.proxyRemoteFiles; - this.localDriveCapacityMb = meta.driveCapacityPerLocalUserMb; - this.remoteDriveCapacityMb = meta.driveCapacityPerRemoteUserMb; - }, - save() { - os.apiWithDialog('admin/update-meta', { - cacheRemoteFiles: this.cacheRemoteFiles, - proxyRemoteFiles: this.proxyRemoteFiles, - localDriveCapacityMb: parseInt(this.localDriveCapacityMb, 10), - remoteDriveCapacityMb: parseInt(this.remoteDriveCapacityMb, 10), - }).then(() => { - fetchInstance(); - }); - } - } -}); -</script> diff --git a/src/client/pages/admin/files.vue b/src/client/pages/admin/files.vue deleted file mode 100644 index 55189cfd84..0000000000 --- a/src/client/pages/admin/files.vue +++ /dev/null @@ -1,209 +0,0 @@ -<template> -<div class="xrmjdkdw"> - <MkContainer :foldable="true" class="lookup"> - <template #header><i class="fas fa-search"></i> {{ $ts.lookup }}</template> - <div class="xrmjdkdw-lookup"> - <MkInput class="item" v-model="q" type="text" @enter="find()"> - <template #label>{{ $ts.fileIdOrUrl }}</template> - </MkInput> - <MkButton @click="find()" primary><i class="fas fa-search"></i> {{ $ts.lookup }}</MkButton> - </div> - </MkContainer> - - <div class="_section"> - <div class="_content"> - <div class="inputs" style="display: flex;"> - <MkSelect v-model="origin" style="margin: 0; flex: 1;"> - <template #label>{{ $ts.instance }}</template> - <option value="combined">{{ $ts.all }}</option> - <option value="local">{{ $ts.local }}</option> - <option value="remote">{{ $ts.remote }}</option> - </MkSelect> - <MkInput v-model="searchHost" :debounce="true" type="search" style="margin: 0; flex: 1;" :disabled="pagination.params().origin === 'local'"> - <template #label>{{ $ts.host }}</template> - </MkInput> - </div> - <div class="inputs" style="display: flex; padding-top: 1.2em;"> - <MkInput v-model="type" :debounce="true" type="search" style="margin: 0; flex: 1;"> - <template #label>MIME type</template> - </MkInput> - </div> - <MkPagination :pagination="pagination" #default="{items}" class="urempief" ref="files"> - <button class="file _panel _button _gap" v-for="file in items" :key="file.id" @click="show(file, $event)"> - <MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/> - <div class="body"> - <div> - <small style="opacity: 0.7;">{{ file.name }}</small> - </div> - <div> - <MkAcct v-if="file.user" :user="file.user"/> - <div v-else>{{ $ts.system }}</div> - </div> - <div> - <span style="margin-right: 1em;">{{ file.type }}</span> - <span>{{ bytes(file.size) }}</span> - </div> - <div> - <span>{{ $ts.registeredDate }}: <MkTime :time="file.createdAt" mode="detail"/></span> - </div> - </div> - </button> - </MkPagination> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import MkButton from '@client/components/ui/button.vue'; -import MkInput from '@client/components/form/input.vue'; -import MkSelect from '@client/components/form/select.vue'; -import MkPagination from '@client/components/ui/pagination.vue'; -import MkContainer from '@client/components/ui/container.vue'; -import MkDriveFileThumbnail from '@client/components/drive-file-thumbnail.vue'; -import bytes from '@client/filters/bytes'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - MkButton, - MkInput, - MkSelect, - MkPagination, - MkContainer, - MkDriveFileThumbnail, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.files, - icon: 'fas fa-cloud', - bg: 'var(--bg)', - actions: [{ - text: this.$ts.clearCachedFiles, - icon: 'fas fa-trash-alt', - handler: this.clear - }] - }, - q: null, - origin: 'local', - type: null, - searchHost: '', - pagination: { - endpoint: 'admin/drive/files', - limit: 10, - params: () => ({ - type: (this.type && this.type !== '') ? this.type : null, - origin: this.origin, - hostname: (this.hostname && this.hostname !== '') ? this.hostname : null, - }), - }, - } - }, - - watch: { - type() { - this.$refs.files.reload(); - }, - origin() { - this.$refs.files.reload(); - }, - searchHost() { - this.$refs.files.reload(); - }, - }, - - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - methods: { - clear() { - os.dialog({ - type: 'warning', - text: this.$ts.clearCachedFilesConfirm, - showCancelButton: true - }).then(({ canceled }) => { - if (canceled) return; - - os.apiWithDialog('admin/drive/clean-remote-files', {}); - }); - }, - - show(file, ev) { - os.popup(import('./file-dialog.vue'), { - fileId: file.id - }, {}, 'closed'); - }, - - find() { - os.api('admin/drive/show-file', this.q.startsWith('http://') || this.q.startsWith('https://') ? { url: this.q.trim() } : { fileId: this.q.trim() }).then(file => { - this.show(file); - }).catch(e => { - if (e.code === 'NO_SUCH_FILE') { - os.dialog({ - type: 'error', - text: this.$ts.notFound - }); - } - }); - }, - - bytes - } -}); -</script> - -<style lang="scss" scoped> -.xrmjdkdw { - margin: var(--margin); - - > .lookup { - margin-bottom: 16px; - } - - .urempief { - margin-top: var(--margin); - - > .file { - display: flex; - width: 100%; - box-sizing: border-box; - text-align: left; - align-items: center; - - &:hover { - color: var(--accent); - } - - > .thumbnail { - width: 128px; - height: 128px; - } - - > .body { - margin-left: 0.3em; - padding: 8px; - flex: 1; - - @media (max-width: 500px) { - font-size: 14px; - } - } - } - } -} - -.xrmjdkdw-lookup { - padding: 16px; - - > .item { - margin-bottom: 16px; - } -} -</style> diff --git a/src/client/pages/admin/index.vue b/src/client/pages/admin/index.vue deleted file mode 100644 index 28157ff05a..0000000000 --- a/src/client/pages/admin/index.vue +++ /dev/null @@ -1,388 +0,0 @@ -<template> -<div class="hiyeyicy" :class="{ wide: !narrow }" ref="el"> - <div class="nav" v-if="!narrow || page == null"> - <MkHeader :info="header"></MkHeader> - - <MkSpacer :content-max="700"> - <div class="lxpfedzu"> - <div class="banner"> - <img :src="$instance.iconUrl || '/favicon.ico'" alt="" class="icon"/> - </div> - - <MkInfo v-if="noMaintainerInformation" warn class="info">{{ $ts.noMaintainerInformationWarning }} <MkA to="/admin/settings" class="_link">{{ $ts.configure }}</MkA></MkInfo> - <MkInfo v-if="noBotProtection" warn class="info">{{ $ts.noBotProtectionWarning }} <MkA to="/admin/bot-protection" class="_link">{{ $ts.configure }}</MkA></MkInfo> - - <MkSuperMenu :def="menuDef" :grid="page == null"></MkSuperMenu> - </div> - </MkSpacer> - </div> - <div class="main"> - <MkStickyContainer> - <template #header><MkHeader v-if="childInfo && !childInfo.hideHeader" :info="childInfo"/></template> - <component :is="component" :key="page" @info="onInfo" v-bind="pageProps"/> - </MkStickyContainer> - </div> -</div> -</template> - -<script lang="ts"> -import { computed, defineAsyncComponent, defineComponent, isRef, nextTick, onMounted, reactive, ref, watch } from 'vue'; -import { i18n } from '@client/i18n'; -import MkSuperMenu from '@client/components/ui/super-menu.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import MkInfo from '@client/components/ui/info.vue'; -import { scroll } from '@client/scripts/scroll'; -import { instance } from '@client/instance'; -import * as symbols from '@client/symbols'; -import * as os from '@client/os'; -import { lookupUser } from '@client/scripts/lookup-user'; - -export default defineComponent({ - components: { - FormBase, - MkSuperMenu, - FormGroup, - FormButton, - MkInfo, - }, - - provide: { - shouldOmitHeaderTitle: false, - }, - - props: { - initialPage: { - type: String, - required: false - } - }, - - setup(props, context) { - const indexInfo = { - title: i18n.locale.controlPanel, - icon: 'fas fa-cog', - bg: 'var(--bg)', - hideHeader: true, - }; - const INFO = ref(indexInfo); - const childInfo = ref(null); - const page = ref(props.initialPage); - const narrow = ref(false); - const view = ref(null); - const el = ref(null); - const onInfo = (viewInfo) => { - if (isRef(viewInfo)) { - watch(viewInfo, () => { - childInfo.value = viewInfo.value; - }, { immediate: true }); - } else { - childInfo.value = viewInfo; - } - }; - const pageProps = ref({}); - - const isEmpty = (x: any) => x == null || x == ''; - - const noMaintainerInformation = ref(false); - const noBotProtection = ref(false); - - os.api('meta', { detail: true }).then(meta => { - // TODO: ่จญๅฎใๅฎไบใใฆใๆฎใฃใใพใพใซใชใใฎใงใในใใชใผใใณใฐใงmetaๆดๆฐใคใใณใใๅใๅใฃใฆใใใชใซๆดๆฐใใ - noMaintainerInformation.value = isEmpty(meta.maintainerName) || isEmpty(meta.maintainerEmail); - noBotProtection.value = !meta.enableHcaptcha && !meta.enableRecaptcha; - }); - - const menuDef = computed(() => [{ - title: i18n.locale.quickAction, - items: [{ - type: 'button', - icon: 'fas fa-search', - text: i18n.locale.lookup, - action: lookup, - }, ...(instance.disableRegistration ? [{ - type: 'button', - icon: 'fas fa-user', - text: i18n.locale.invite, - action: invite, - }] : [])], - }, { - title: i18n.locale.administration, - items: [{ - icon: 'fas fa-tachometer-alt', - text: i18n.locale.dashboard, - to: '/admin/overview', - active: page.value === 'overview', - }, { - icon: 'fas fa-users', - text: i18n.locale.users, - to: '/admin/users', - active: page.value === 'users', - }, { - icon: 'fas fa-laugh', - text: i18n.locale.customEmojis, - to: '/admin/emojis', - active: page.value === 'emojis', - }, { - icon: 'fas fa-globe', - text: i18n.locale.federation, - to: '/admin/federation', - active: page.value === 'federation', - }, { - icon: 'fas fa-clipboard-list', - text: i18n.locale.jobQueue, - to: '/admin/queue', - active: page.value === 'queue', - }, { - icon: 'fas fa-cloud', - text: i18n.locale.files, - to: '/admin/files', - active: page.value === 'files', - }, { - icon: 'fas fa-broadcast-tower', - text: i18n.locale.announcements, - to: '/admin/announcements', - active: page.value === 'announcements', - }, { - icon: 'fas fa-audio-description', - text: i18n.locale.ads, - to: '/admin/ads', - active: page.value === 'ads', - }, { - icon: 'fas fa-exclamation-circle', - text: i18n.locale.abuseReports, - to: '/admin/abuses', - active: page.value === 'abuses', - }], - }, { - title: i18n.locale.settings, - items: [{ - icon: 'fas fa-cog', - text: i18n.locale.general, - to: '/admin/settings', - active: page.value === 'settings', - }, { - icon: 'fas fa-cloud', - text: i18n.locale.files, - to: '/admin/files-settings', - active: page.value === 'files-settings', - }, { - icon: 'fas fa-envelope', - text: i18n.locale.emailServer, - to: '/admin/email-settings', - active: page.value === 'email-settings', - }, { - icon: 'fas fa-cloud', - text: i18n.locale.objectStorage, - to: '/admin/object-storage', - active: page.value === 'object-storage', - }, { - icon: 'fas fa-lock', - 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, - to: '/admin/relays', - active: page.value === 'relays', - }, { - icon: 'fas fa-share-alt', - text: i18n.locale.integration, - to: '/admin/integrations', - active: page.value === 'integrations', - }, { - icon: 'fas fa-ban', - text: i18n.locale.instanceBlocking, - to: '/admin/instance-block', - active: page.value === 'instance-block', - }, { - icon: 'fas fa-ghost', - text: i18n.locale.proxyAccount, - to: '/admin/proxy-account', - active: page.value === 'proxy-account', - }, { - icon: 'fas fa-cogs', - text: i18n.locale.other, - to: '/admin/other-settings', - active: page.value === 'other-settings', - }], - }, { - title: i18n.locale.info, - items: [{ - icon: 'fas fa-database', - text: i18n.locale.database, - to: '/admin/database', - active: page.value === 'database', - }], - }]); - const component = computed(() => { - if (page.value == null) return null; - switch (page.value) { - case 'overview': return defineAsyncComponent(() => import('./overview.vue')); - case 'users': return defineAsyncComponent(() => import('./users.vue')); - case 'emojis': return defineAsyncComponent(() => import('./emojis.vue')); - case 'federation': return defineAsyncComponent(() => import('../federation.vue')); - case 'queue': return defineAsyncComponent(() => import('./queue.vue')); - case 'files': return defineAsyncComponent(() => import('./files.vue')); - case 'announcements': return defineAsyncComponent(() => import('./announcements.vue')); - case 'ads': return defineAsyncComponent(() => import('./ads.vue')); - case 'database': return defineAsyncComponent(() => import('./database.vue')); - case 'abuses': return defineAsyncComponent(() => import('./abuses.vue')); - case 'settings': return defineAsyncComponent(() => import('./settings.vue')); - case 'files-settings': return defineAsyncComponent(() => import('./files-settings.vue')); - 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 'bot-protection': return defineAsyncComponent(() => import('./bot-protection.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 'integrations/twitter': return defineAsyncComponent(() => import('./integrations-twitter.vue')); - case 'integrations/github': return defineAsyncComponent(() => import('./integrations-github.vue')); - case 'integrations/discord': return defineAsyncComponent(() => import('./integrations-discord.vue')); - case 'instance-block': return defineAsyncComponent(() => import('./instance-block.vue')); - case 'proxy-account': return defineAsyncComponent(() => import('./proxy-account.vue')); - case 'other-settings': return defineAsyncComponent(() => import('./other-settings.vue')); - } - }); - - watch(component, () => { - pageProps.value = {}; - - nextTick(() => { - scroll(el.value, { top: 0 }); - }); - }, { immediate: true }); - - watch(() => props.initialPage, () => { - if (props.initialPage == null && !narrow.value) { - page.value = 'overview'; - } else { - page.value = props.initialPage; - if (props.initialPage == null) { - INFO.value = indexInfo; - } - } - }); - - onMounted(() => { - narrow.value = el.value.offsetWidth < 800; - if (!narrow.value) { - page.value = 'overview'; - } - }); - - const invite = () => { - os.api('admin/invite').then(x => { - os.dialog({ - type: 'info', - text: x.code - }); - }).catch(e => { - os.dialog({ - type: 'error', - text: e - }); - }); - }; - - const lookup = (ev) => { - os.popupMenu([{ - text: i18n.locale.user, - icon: 'fas fa-user', - action: () => { - lookupUser(); - } - }, { - text: i18n.locale.note, - icon: 'fas fa-pencil-alt', - action: () => { - alert('TODO'); - } - }, { - text: i18n.locale.file, - icon: 'fas fa-cloud', - action: () => { - alert('TODO'); - } - }, { - text: i18n.locale.instance, - icon: 'fas fa-globe', - action: () => { - alert('TODO'); - } - }], ev.currentTarget || ev.target); - }; - - return { - [symbols.PAGE_INFO]: INFO, - menuDef, - header: { - title: i18n.locale.controlPanel, - }, - noMaintainerInformation, - noBotProtection, - page, - narrow, - view, - el, - onInfo, - childInfo, - pageProps, - component, - invite, - lookup, - }; - }, -}); -</script> - -<style lang="scss" scoped> -.hiyeyicy { - &.wide { - display: flex; - margin: 0 auto; - height: 100%; - - > .nav { - width: 32%; - max-width: 280px; - box-sizing: border-box; - border-right: solid 0.5px var(--divider); - overflow: auto; - height: 100%; - } - - > .main { - flex: 1; - min-width: 0; - } - } - - > .nav { - .lxpfedzu { - > .info { - margin: 16px 0; - } - - > .banner { - margin: 16px; - - > .icon { - display: block; - margin: auto; - height: 42px; - border-radius: 8px; - } - } - } - } -} -</style> diff --git a/src/client/pages/admin/instance-block.vue b/src/client/pages/admin/instance-block.vue deleted file mode 100644 index 105cdb4941..0000000000 --- a/src/client/pages/admin/instance-block.vue +++ /dev/null @@ -1,72 +0,0 @@ -<template> -<FormBase> - <FormSuspense :p="init"> - <FormTextarea v-model="blockedHosts"> - <span>{{ $ts.blockedInstances }}</span> - <template #desc>{{ $ts.blockedInstancesDescription }}</template> - </FormTextarea> - - <FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton> - </FormSuspense> -</FormBase> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import FormSwitch from '@client/components/debobigego/switch.vue'; -import FormInput from '@client/components/debobigego/input.vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import FormTextarea from '@client/components/debobigego/textarea.vue'; -import FormInfo from '@client/components/debobigego/info.vue'; -import FormSuspense from '@client/components/debobigego/suspense.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; -import { fetchInstance } from '@client/instance'; - -export default defineComponent({ - components: { - FormSwitch, - FormInput, - FormBase, - FormGroup, - FormButton, - FormTextarea, - FormInfo, - FormSuspense, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.instanceBlocking, - icon: 'fas fa-ban', - bg: 'var(--bg)', - }, - blockedHosts: '', - } - }, - - async mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - methods: { - async init() { - const meta = await os.api('meta', { detail: true }); - this.blockedHosts = meta.blockedHosts.join('\n'); - }, - - save() { - os.apiWithDialog('admin/update-meta', { - blockedHosts: this.blockedHosts.split('\n') || [], - }).then(() => { - fetchInstance(); - }); - } - } -}); -</script> diff --git a/src/client/pages/admin/instance.vue b/src/client/pages/admin/instance.vue deleted file mode 100644 index 5572fbbf75..0000000000 --- a/src/client/pages/admin/instance.vue +++ /dev/null @@ -1,321 +0,0 @@ -<template> -<XModalWindow ref="dialog" - :width="520" - :height="500" - @close="$refs.dialog.close()" - @closed="$emit('closed')" -> - <template #header>{{ instance.host }}</template> - <div class="mk-instance-info"> - <div class="_table section"> - <div class="_row"> - <div class="_cell"> - <div class="_label">{{ $ts.software }}</div> - <div class="_data">{{ instance.softwareName || '?' }}</div> - </div> - <div class="_cell"> - <div class="_label">{{ $ts.version }}</div> - <div class="_data">{{ instance.softwareVersion || '?' }}</div> - </div> - </div> - </div> - <div class="_table data section"> - <div class="_row"> - <div class="_cell"> - <div class="_label">{{ $ts.registeredAt }}</div> - <div class="_data">{{ new Date(instance.caughtAt).toLocaleString() }} (<MkTime :time="instance.caughtAt"/>)</div> - </div> - </div> - <div class="_row"> - <div class="_cell"> - <div class="_label">{{ $ts.following }}</div> - <button class="_data _textButton" @click="showFollowing()">{{ number(instance.followingCount) }}</button> - </div> - <div class="_cell"> - <div class="_label">{{ $ts.followers }}</div> - <button class="_data _textButton" @click="showFollowers()">{{ number(instance.followersCount) }}</button> - </div> - </div> - <div class="_row"> - <div class="_cell"> - <div class="_label">{{ $ts.users }}</div> - <button class="_data _textButton" @click="showUsers()">{{ number(instance.usersCount) }}</button> - </div> - <div class="_cell"> - <div class="_label">{{ $ts.notes }}</div> - <div class="_data">{{ number(instance.notesCount) }}</div> - </div> - </div> - <div class="_row"> - <div class="_cell"> - <div class="_label">{{ $ts.files }}</div> - <div class="_data">{{ number(instance.driveFiles) }}</div> - </div> - <div class="_cell"> - <div class="_label">{{ $ts.storageUsage }}</div> - <div class="_data">{{ bytes(instance.driveUsage) }}</div> - </div> - </div> - <div class="_row"> - <div class="_cell"> - <div class="_label">{{ $ts.latestRequestSentAt }}</div> - <div class="_data"><MkTime v-if="instance.latestRequestSentAt" :time="instance.latestRequestSentAt"/><span v-else>N/A</span></div> - </div> - <div class="_cell"> - <div class="_label">{{ $ts.latestStatus }}</div> - <div class="_data">{{ instance.latestStatus ? instance.latestStatus : 'N/A' }}</div> - </div> - </div> - <div class="_row"> - <div class="_cell"> - <div class="_label">{{ $ts.latestRequestReceivedAt }}</div> - <div class="_data"><MkTime v-if="instance.latestRequestReceivedAt" :time="instance.latestRequestReceivedAt"/><span v-else>N/A</span></div> - </div> - </div> - </div> - <div class="chart"> - <div class="header"> - <span class="label">{{ $ts.charts }}</span> - <div class="selects"> - <MkSelect v-model="chartSrc" style="margin: 0; flex: 1;"> - <option value="instance-requests">{{ $ts._instanceCharts.requests }}</option> - <option value="instance-users">{{ $ts._instanceCharts.users }}</option> - <option value="instance-users-total">{{ $ts._instanceCharts.usersTotal }}</option> - <option value="instance-notes">{{ $ts._instanceCharts.notes }}</option> - <option value="instance-notes-total">{{ $ts._instanceCharts.notesTotal }}</option> - <option value="instance-ff">{{ $ts._instanceCharts.ff }}</option> - <option value="instance-ff-total">{{ $ts._instanceCharts.ffTotal }}</option> - <option value="instance-drive-usage">{{ $ts._instanceCharts.cacheSize }}</option> - <option value="instance-drive-usage-total">{{ $ts._instanceCharts.cacheSizeTotal }}</option> - <option value="instance-drive-files">{{ $ts._instanceCharts.files }}</option> - <option value="instance-drive-files-total">{{ $ts._instanceCharts.filesTotal }}</option> - </MkSelect> - <MkSelect v-model="chartSpan" style="margin: 0;"> - <option value="hour">{{ $ts.perHour }}</option> - <option value="day">{{ $ts.perDay }}</option> - </MkSelect> - </div> - </div> - <div class="chart"> - <MkChart :src="chartSrc" :span="chartSpan" :limit="90" :detailed="true"></MkChart> - </div> - </div> - <div class="operations section"> - <span class="label">{{ $ts.operations }}</span> - <MkSwitch v-model="isSuspended" class="switch">{{ $ts.stopActivityDelivery }}</MkSwitch> - <MkSwitch :model-value="isBlocked" class="switch" @update:modelValue="changeBlock">{{ $ts.blockThisInstance }}</MkSwitch> - <details> - <summary>{{ $ts.deleteAllFiles }}</summary> - <MkButton @click="deleteAllFiles()" style="margin: 0.5em 0 0.5em 0;"><i class="fas fa-trash-alt"></i> {{ $ts.deleteAllFiles }}</MkButton> - </details> - <details> - <summary>{{ $ts.removeAllFollowing }}</summary> - <MkButton @click="removeAllFollowing()" style="margin: 0.5em 0 0.5em 0;"><i class="fas fa-minus-circle"></i> {{ $ts.removeAllFollowing }}</MkButton> - <MkInfo warn>{{ $t('removeAllFollowingDescription', { host: instance.host }) }}</MkInfo> - </details> - </div> - <details class="metadata section"> - <summary class="label">{{ $ts.metadata }}</summary> - <pre><code>{{ JSON.stringify(instance, null, 2) }}</code></pre> - </details> - </div> -</XModalWindow> -</template> - -<script lang="ts"> -import { defineComponent, markRaw } from 'vue'; -import XModalWindow from '@client/components/ui/modal-window.vue'; -import MkUsersDialog from '@client/components/users-dialog.vue'; -import MkSelect from '@client/components/form/select.vue'; -import MkButton from '@client/components/ui/button.vue'; -import MkSwitch from '@client/components/form/switch.vue'; -import MkInfo from '@client/components/ui/info.vue'; -import MkChart from '@client/components/chart.vue'; -import bytes from '@client/filters/bytes'; -import number from '@client/filters/number'; -import * as os from '@client/os'; - -export default defineComponent({ - components: { - XModalWindow, - MkSelect, - MkButton, - MkSwitch, - MkInfo, - MkChart, - }, - - props: { - instance: { - type: Object, - required: true - } - }, - - emits: ['closed'], - - data() { - return { - isSuspended: this.instance.isSuspended, - chartSrc: 'requests', - chartSpan: 'hour', - }; - }, - - computed: { - meta() { - return this.$instance; - }, - - isBlocked() { - return this.meta && this.meta.blockedHosts && this.meta.blockedHosts.includes(this.instance.host); - } - }, - - watch: { - isSuspended() { - os.api('admin/federation/update-instance', { - host: this.instance.host, - isSuspended: this.isSuspended - }); - }, - }, - - methods: { - changeBlock(e) { - os.api('admin/update-meta', { - blockedHosts: this.isBlocked ? this.meta.blockedHosts.concat([this.instance.host]) : this.meta.blockedHosts.filter(x => x !== this.instance.host) - }); - }, - - removeAllFollowing() { - os.apiWithDialog('admin/federation/remove-all-following', { - host: this.instance.host - }); - }, - - deleteAllFiles() { - os.apiWithDialog('admin/federation/delete-all-files', { - host: this.instance.host - }); - }, - - showFollowing() { - os.modal(MkUsersDialog, { - title: this.$ts.instanceFollowing, - pagination: { - endpoint: 'federation/following', - limit: 10, - params: { - host: this.instance.host - } - }, - extract: item => item.follower - }); - }, - - showFollowers() { - os.modal(MkUsersDialog, { - title: this.$ts.instanceFollowers, - pagination: { - endpoint: 'federation/followers', - limit: 10, - params: { - host: this.instance.host - } - }, - extract: item => item.followee - }); - }, - - showUsers() { - os.modal(MkUsersDialog, { - title: this.$ts.instanceUsers, - pagination: { - endpoint: 'federation/users', - limit: 10, - params: { - host: this.instance.host - } - } - }); - }, - - bytes, - - number - } -}); -</script> - -<style lang="scss" scoped> -.mk-instance-info { - overflow: auto; - - > .section { - padding: 16px 32px; - - @media (max-width: 500px) { - padding: 8px 16px; - } - - &:not(:first-child) { - border-top: solid 0.5px var(--divider); - } - } - - > .chart { - border-top: solid 0.5px var(--divider); - padding: 16px 0 12px 0; - - > .header { - padding: 0 32px; - - @media (max-width: 500px) { - padding: 0 16px; - } - - > .label { - font-size: 80%; - opacity: 0.7; - } - - > .selects { - display: flex; - } - } - - > .chart { - padding: 0 16px; - - @media (max-width: 500px) { - padding: 0; - } - } - } - - > .operations { - > .label { - font-size: 80%; - opacity: 0.7; - } - - > .switch { - margin: 16px 0; - } - } - - > .metadata { - > .label { - font-size: 80%; - opacity: 0.7; - } - - > pre > code { - display: block; - max-height: 200px; - overflow: auto; - } - } -} -</style> diff --git a/src/client/pages/admin/integrations-discord.vue b/src/client/pages/admin/integrations-discord.vue deleted file mode 100644 index c33b24f17f..0000000000 --- a/src/client/pages/admin/integrations-discord.vue +++ /dev/null @@ -1,85 +0,0 @@ -<template> -<FormBase> - <FormSuspense :p="init"> - <FormSwitch v-model="enableDiscordIntegration"> - {{ $ts.enable }} - </FormSwitch> - - <template v-if="enableDiscordIntegration"> - <FormInfo>Callback URL: {{ `${url}/api/dc/cb` }}</FormInfo> - - <FormInput v-model="discordClientId"> - <template #prefix><i class="fas fa-key"></i></template> - Client ID - </FormInput> - - <FormInput v-model="discordClientSecret"> - <template #prefix><i class="fas fa-key"></i></template> - Client Secret - </FormInput> - </template> - - <FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton> - </FormSuspense> -</FormBase> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import FormSwitch from '@client/components/debobigego/switch.vue'; -import FormInput from '@client/components/debobigego/input.vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormInfo from '@client/components/debobigego/info.vue'; -import FormSuspense from '@client/components/debobigego/suspense.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; -import { fetchInstance } from '@client/instance'; - -export default defineComponent({ - components: { - FormSwitch, - FormInput, - FormBase, - FormInfo, - FormButton, - FormSuspense, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: 'Discord', - icon: 'fab fa-discord' - }, - enableDiscordIntegration: false, - discordClientId: null, - discordClientSecret: null, - } - }, - - async mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - methods: { - async init() { - const meta = await os.api('meta', { detail: true }); - this.enableDiscordIntegration = meta.enableDiscordIntegration; - this.discordClientId = meta.discordClientId; - this.discordClientSecret = meta.discordClientSecret; - }, - save() { - os.apiWithDialog('admin/update-meta', { - enableDiscordIntegration: this.enableDiscordIntegration, - discordClientId: this.discordClientId, - discordClientSecret: this.discordClientSecret, - }).then(() => { - fetchInstance(); - }); - } - } -}); -</script> diff --git a/src/client/pages/admin/integrations-github.vue b/src/client/pages/admin/integrations-github.vue deleted file mode 100644 index cdf85868ff..0000000000 --- a/src/client/pages/admin/integrations-github.vue +++ /dev/null @@ -1,85 +0,0 @@ -<template> -<FormBase> - <FormSuspense :p="init"> - <FormSwitch v-model="enableGithubIntegration"> - {{ $ts.enable }} - </FormSwitch> - - <template v-if="enableGithubIntegration"> - <FormInfo>Callback URL: {{ `${url}/api/gh/cb` }}</FormInfo> - - <FormInput v-model="githubClientId"> - <template #prefix><i class="fas fa-key"></i></template> - Client ID - </FormInput> - - <FormInput v-model="githubClientSecret"> - <template #prefix><i class="fas fa-key"></i></template> - Client Secret - </FormInput> - </template> - - <FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton> - </FormSuspense> -</FormBase> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import FormSwitch from '@client/components/debobigego/switch.vue'; -import FormInput from '@client/components/debobigego/input.vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormInfo from '@client/components/debobigego/info.vue'; -import FormSuspense from '@client/components/debobigego/suspense.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; -import { fetchInstance } from '@client/instance'; - -export default defineComponent({ - components: { - FormSwitch, - FormInput, - FormBase, - FormInfo, - FormButton, - FormSuspense, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: 'GitHub', - icon: 'fab fa-github' - }, - enableGithubIntegration: false, - githubClientId: null, - githubClientSecret: null, - } - }, - - async mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - methods: { - async init() { - const meta = await os.api('meta', { detail: true }); - this.enableGithubIntegration = meta.enableGithubIntegration; - this.githubClientId = meta.githubClientId; - this.githubClientSecret = meta.githubClientSecret; - }, - save() { - os.apiWithDialog('admin/update-meta', { - enableGithubIntegration: this.enableGithubIntegration, - githubClientId: this.githubClientId, - githubClientSecret: this.githubClientSecret, - }).then(() => { - fetchInstance(); - }); - } - } -}); -</script> diff --git a/src/client/pages/admin/integrations-twitter.vue b/src/client/pages/admin/integrations-twitter.vue deleted file mode 100644 index ed7d097d0a..0000000000 --- a/src/client/pages/admin/integrations-twitter.vue +++ /dev/null @@ -1,85 +0,0 @@ -<template> -<FormBase> - <FormSuspense :p="init"> - <FormSwitch v-model="enableTwitterIntegration"> - {{ $ts.enable }} - </FormSwitch> - - <template v-if="enableTwitterIntegration"> - <FormInfo>Callback URL: {{ `${url}/api/tw/cb` }}</FormInfo> - - <FormInput v-model="twitterConsumerKey"> - <template #prefix><i class="fas fa-key"></i></template> - Consumer Key - </FormInput> - - <FormInput v-model="twitterConsumerSecret"> - <template #prefix><i class="fas fa-key"></i></template> - Consumer Secret - </FormInput> - </template> - - <FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton> - </FormSuspense> -</FormBase> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import FormSwitch from '@client/components/debobigego/switch.vue'; -import FormInput from '@client/components/debobigego/input.vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormInfo from '@client/components/debobigego/info.vue'; -import FormSuspense from '@client/components/debobigego/suspense.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; -import { fetchInstance } from '@client/instance'; - -export default defineComponent({ - components: { - FormSwitch, - FormInput, - FormBase, - FormInfo, - FormButton, - FormSuspense, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: 'Twitter', - icon: 'fab fa-twitter' - }, - enableTwitterIntegration: false, - twitterConsumerKey: null, - twitterConsumerSecret: null, - } - }, - - async mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - methods: { - async init() { - const meta = await os.api('meta', { detail: true }); - this.enableTwitterIntegration = meta.enableTwitterIntegration; - this.twitterConsumerKey = meta.twitterConsumerKey; - this.twitterConsumerSecret = meta.twitterConsumerSecret; - }, - save() { - os.apiWithDialog('admin/update-meta', { - enableTwitterIntegration: this.enableTwitterIntegration, - twitterConsumerKey: this.twitterConsumerKey, - twitterConsumerSecret: this.twitterConsumerSecret, - }).then(() => { - fetchInstance(); - }); - } - } -}); -</script> diff --git a/src/client/pages/admin/integrations.vue b/src/client/pages/admin/integrations.vue deleted file mode 100644 index bdc2cec4d0..0000000000 --- a/src/client/pages/admin/integrations.vue +++ /dev/null @@ -1,74 +0,0 @@ -<template> -<FormBase> - <FormSuspense :p="init"> - <FormLink to="/admin/integrations/twitter"> - <i class="fab fa-twitter"></i> Twitter - <template #suffix>{{ enableTwitterIntegration ? $ts.enabled : $ts.disabled }}</template> - </FormLink> - <FormLink to="/admin/integrations/github"> - <i class="fab fa-github"></i> GitHub - <template #suffix>{{ enableGithubIntegration ? $ts.enabled : $ts.disabled }}</template> - </FormLink> - <FormLink to="/admin/integrations/discord"> - <i class="fab fa-discord"></i> Discord - <template #suffix>{{ enableDiscordIntegration ? $ts.enabled : $ts.disabled }}</template> - </FormLink> - </FormSuspense> -</FormBase> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import FormLink from '@client/components/debobigego/link.vue'; -import FormInput from '@client/components/debobigego/input.vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import FormTextarea from '@client/components/debobigego/textarea.vue'; -import FormInfo from '@client/components/debobigego/info.vue'; -import FormSuspense from '@client/components/debobigego/suspense.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; -import { fetchInstance } from '@client/instance'; - -export default defineComponent({ - components: { - FormLink, - FormInput, - FormBase, - FormGroup, - FormButton, - FormTextarea, - FormInfo, - FormSuspense, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.integration, - icon: 'fas fa-share-alt', - bg: 'var(--bg)', - }, - enableTwitterIntegration: false, - enableGithubIntegration: false, - enableDiscordIntegration: false, - } - }, - - async mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - methods: { - async init() { - const meta = await os.api('meta', { detail: true }); - this.enableTwitterIntegration = meta.enableTwitterIntegration; - this.enableGithubIntegration = meta.enableGithubIntegration; - this.enableDiscordIntegration = meta.enableDiscordIntegration; - }, - } -}); -</script> diff --git a/src/client/pages/admin/metrics.vue b/src/client/pages/admin/metrics.vue deleted file mode 100644 index da36f6c688..0000000000 --- a/src/client/pages/admin/metrics.vue +++ /dev/null @@ -1,472 +0,0 @@ -<template> -<div class="_debobigegoItem"> - <div class="_debobigegoLabel"><i class="fas fa-microchip"></i> {{ $ts.cpuAndMemory }}</div> - <div class="_debobigegoPanel xhexznfu"> - <div> - <canvas :ref="cpumem"></canvas> - </div> - <div v-if="serverInfo"> - <div class="_table"> - <div class="_row"> - <div class="_cell"><div class="_label">MEM total</div>{{ bytes(serverInfo.mem.total) }}</div> - <div class="_cell"><div class="_label">MEM used</div>{{ bytes(memUsage) }} ({{ (memUsage / serverInfo.mem.total * 100).toFixed(0) }}%)</div> - <div class="_cell"><div class="_label">MEM free</div>{{ bytes(serverInfo.mem.total - memUsage) }} ({{ ((serverInfo.mem.total - memUsage) / serverInfo.mem.total * 100).toFixed(0) }}%)</div> - </div> - </div> - </div> - </div> -</div> -<div class="_debobigegoItem"> - <div class="_debobigegoLabel"><i class="fas fa-hdd"></i> {{ $ts.disk }}</div> - <div class="_debobigegoPanel xhexznfu"> - <div> - <canvas :ref="disk"></canvas> - </div> - <div v-if="serverInfo"> - <div class="_table"> - <div class="_row"> - <div class="_cell"><div class="_label">Disk total</div>{{ bytes(serverInfo.fs.total) }}</div> - <div class="_cell"><div class="_label">Disk used</div>{{ bytes(serverInfo.fs.used) }} ({{ (serverInfo.fs.used / serverInfo.fs.total * 100).toFixed(0) }}%)</div> - <div class="_cell"><div class="_label">Disk free</div>{{ bytes(serverInfo.fs.total - serverInfo.fs.used) }} ({{ ((serverInfo.fs.total - serverInfo.fs.used) / serverInfo.fs.total * 100).toFixed(0) }}%)</div> - </div> - </div> - </div> - </div> -</div> -<div class="_debobigegoItem"> - <div class="_debobigegoLabel"><i class="fas fa-exchange-alt"></i> {{ $ts.network }}</div> - <div class="_debobigegoPanel xhexznfu"> - <div> - <canvas :ref="net"></canvas> - </div> - <div v-if="serverInfo"> - <div class="_table"> - <div class="_row"> - <div class="_cell"><div class="_label">Interface</div>{{ serverInfo.net.interface }}</div> - </div> - </div> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import { defineComponent, markRaw } from 'vue'; -import { - Chart, - ArcElement, - LineElement, - BarElement, - PointElement, - BarController, - LineController, - CategoryScale, - LinearScale, - Legend, - Title, - Tooltip, - SubTitle -} from 'chart.js'; -import MkButton from '@client/components/ui/button.vue'; -import MkSelect from '@client/components/form/select.vue'; -import MkInput from '@client/components/form/input.vue'; -import MkContainer from '@client/components/ui/container.vue'; -import MkFolder from '@client/components/ui/folder.vue'; -import MkwFederation from '../../widgets/federation.vue'; -import { version, url } from '@client/config'; -import bytes from '@client/filters/bytes'; -import number from '@client/filters/number'; -import MkInstanceInfo from './instance.vue'; - -Chart.register( - ArcElement, - LineElement, - BarElement, - PointElement, - BarController, - LineController, - CategoryScale, - LinearScale, - Legend, - Title, - Tooltip, - SubTitle -); - -const alpha = (hex, a) => { - const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!; - const r = parseInt(result[1], 16); - const g = parseInt(result[2], 16); - const b = parseInt(result[3], 16); - return `rgba(${r}, ${g}, ${b}, ${a})`; -}; -import * as os from '@client/os'; - -export default defineComponent({ - components: { - MkButton, - MkSelect, - MkInput, - MkContainer, - MkFolder, - MkwFederation, - }, - - data() { - return { - version, - url, - stats: null, - serverInfo: null, - connection: null, - queueConnection: markRaw(os.stream.useChannel('queueStats')), - memUsage: 0, - chartCpuMem: null, - chartNet: null, - jobs: [], - logs: [], - logLevel: 'all', - logDomain: '', - modLogs: [], - dbInfo: null, - overviewHeight: '1fr', - queueHeight: '1fr', - paused: false, - } - }, - - computed: { - gridColor() { - // TODO: var(--panel)ใฎ่ฒใๆใใๆใใใใงๅคๅฎใใ - return this.$store.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'; - }, - }, - - mounted() { - this.fetchJobs(); - - Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg'); - - os.api('admin/server-info', {}).then(res => { - this.serverInfo = res; - - this.connection = markRaw(os.stream.useChannel('serverStats')); - this.connection.on('stats', this.onStats); - this.connection.on('statsLog', this.onStatsLog); - this.connection.send('requestLog', { - id: Math.random().toString().substr(2, 8), - length: 150 - }); - - this.$nextTick(() => { - this.queueConnection.send('requestLog', { - id: Math.random().toString().substr(2, 8), - length: 200 - }); - }); - }); - }, - - beforeUnmount() { - if (this.connection) { - this.connection.off('stats', this.onStats); - this.connection.off('statsLog', this.onStatsLog); - this.connection.dispose(); - } - this.queueConnection.dispose(); - }, - - methods: { - cpumem(el) { - if (this.chartCpuMem != null) return; - this.chartCpuMem = markRaw(new Chart(el, { - type: 'line', - data: { - labels: [], - datasets: [{ - label: 'CPU', - pointRadius: 0, - tension: 0, - borderWidth: 2, - borderColor: '#86b300', - backgroundColor: alpha('#86b300', 0.1), - data: [] - }, { - label: 'MEM (active)', - pointRadius: 0, - tension: 0, - borderWidth: 2, - borderColor: '#935dbf', - backgroundColor: alpha('#935dbf', 0.02), - data: [] - }, { - label: 'MEM (used)', - pointRadius: 0, - tension: 0, - borderWidth: 2, - borderColor: '#935dbf', - borderDash: [5, 5], - fill: false, - data: [] - }] - }, - options: { - aspectRatio: 3, - layout: { - padding: { - left: 16, - right: 16, - top: 16, - bottom: 0 - } - }, - legend: { - position: 'bottom', - labels: { - boxWidth: 16, - } - }, - scales: { - x: { - gridLines: { - display: false, - color: this.gridColor, - zeroLineColor: this.gridColor, - }, - ticks: { - display: false, - } - }, - y: { - position: 'right', - gridLines: { - display: true, - color: this.gridColor, - zeroLineColor: this.gridColor, - }, - ticks: { - display: false, - max: 100 - } - } - }, - tooltips: { - intersect: false, - mode: 'index', - } - } - })); - }, - - net(el) { - if (this.chartNet != null) return; - this.chartNet = markRaw(new Chart(el, { - type: 'line', - data: { - labels: [], - datasets: [{ - label: 'In', - pointRadius: 0, - tension: 0, - borderWidth: 2, - borderColor: '#94a029', - backgroundColor: alpha('#94a029', 0.1), - data: [] - }, { - label: 'Out', - pointRadius: 0, - tension: 0, - borderWidth: 2, - borderColor: '#ff9156', - backgroundColor: alpha('#ff9156', 0.1), - data: [] - }] - }, - options: { - aspectRatio: 3, - layout: { - padding: { - left: 16, - right: 16, - top: 16, - bottom: 0 - } - }, - legend: { - position: 'bottom', - labels: { - boxWidth: 16, - } - }, - scales: { - x: { - gridLines: { - display: false, - color: this.gridColor, - zeroLineColor: this.gridColor, - }, - ticks: { - display: false - } - }, - y: { - position: 'right', - gridLines: { - display: true, - color: this.gridColor, - zeroLineColor: this.gridColor, - }, - ticks: { - display: false, - } - } - }, - tooltips: { - intersect: false, - mode: 'index', - } - } - })); - }, - - disk(el) { - if (this.chartDisk != null) return; - this.chartDisk = markRaw(new Chart(el, { - type: 'line', - data: { - labels: [], - datasets: [{ - label: 'Read', - pointRadius: 0, - tension: 0, - borderWidth: 2, - borderColor: '#94a029', - backgroundColor: alpha('#94a029', 0.1), - data: [] - }, { - label: 'Write', - pointRadius: 0, - tension: 0, - borderWidth: 2, - borderColor: '#ff9156', - backgroundColor: alpha('#ff9156', 0.1), - data: [] - }] - }, - options: { - aspectRatio: 3, - layout: { - padding: { - left: 16, - right: 16, - top: 16, - bottom: 0 - } - }, - legend: { - position: 'bottom', - labels: { - boxWidth: 16, - } - }, - scales: { - x: { - gridLines: { - display: false, - color: this.gridColor, - zeroLineColor: this.gridColor, - }, - ticks: { - display: false - } - }, - y: { - position: 'right', - gridLines: { - display: true, - color: this.gridColor, - zeroLineColor: this.gridColor, - }, - ticks: { - display: false, - } - } - }, - tooltips: { - intersect: false, - mode: 'index', - } - } - })); - }, - - fetchJobs() { - os.api('admin/queue/deliver-delayed', {}).then(jobs => { - this.jobs = jobs; - }); - }, - - onStats(stats) { - if (this.paused) return; - - const cpu = (stats.cpu * 100).toFixed(0); - const memActive = (stats.mem.active / this.serverInfo.mem.total * 100).toFixed(0); - const memUsed = (stats.mem.used / this.serverInfo.mem.total * 100).toFixed(0); - this.memUsage = stats.mem.active; - - this.chartCpuMem.data.labels.push(''); - this.chartCpuMem.data.datasets[0].data.push(cpu); - this.chartCpuMem.data.datasets[1].data.push(memActive); - this.chartCpuMem.data.datasets[2].data.push(memUsed); - this.chartNet.data.labels.push(''); - this.chartNet.data.datasets[0].data.push(stats.net.rx); - this.chartNet.data.datasets[1].data.push(stats.net.tx); - this.chartDisk.data.labels.push(''); - this.chartDisk.data.datasets[0].data.push(stats.fs.r); - this.chartDisk.data.datasets[1].data.push(stats.fs.w); - if (this.chartCpuMem.data.datasets[0].data.length > 150) { - this.chartCpuMem.data.labels.shift(); - this.chartCpuMem.data.datasets[0].data.shift(); - this.chartCpuMem.data.datasets[1].data.shift(); - this.chartCpuMem.data.datasets[2].data.shift(); - this.chartNet.data.labels.shift(); - this.chartNet.data.datasets[0].data.shift(); - this.chartNet.data.datasets[1].data.shift(); - this.chartDisk.data.labels.shift(); - this.chartDisk.data.datasets[0].data.shift(); - this.chartDisk.data.datasets[1].data.shift(); - } - this.chartCpuMem.update(); - this.chartNet.update(); - this.chartDisk.update(); - }, - - onStatsLog(statsLog) { - for (const stats of [...statsLog].reverse()) { - this.onStats(stats); - } - }, - - bytes, - - number, - - pause() { - this.paused = true; - }, - - resume() { - this.paused = false; - }, - } -}); -</script> - -<style lang="scss" scoped> -.xhexznfu { - > div:nth-child(2) { - padding: 16px; - border-top: solid 0.5px var(--divider); - } -} -</style> diff --git a/src/client/pages/admin/object-storage.vue b/src/client/pages/admin/object-storage.vue deleted file mode 100644 index 2d765270e6..0000000000 --- a/src/client/pages/admin/object-storage.vue +++ /dev/null @@ -1,155 +0,0 @@ -<template> -<FormBase> - <FormSuspense :p="init"> - <FormSwitch v-model="useObjectStorage">{{ $ts.useObjectStorage }}</FormSwitch> - - <template v-if="useObjectStorage"> - <FormInput v-model="objectStorageBaseUrl"> - <span>{{ $ts.objectStorageBaseUrl }}</span> - <template #desc>{{ $ts.objectStorageBaseUrlDesc }}</template> - </FormInput> - - <FormInput v-model="objectStorageBucket"> - <span>{{ $ts.objectStorageBucket }}</span> - <template #desc>{{ $ts.objectStorageBucketDesc }}</template> - </FormInput> - - <FormInput v-model="objectStoragePrefix"> - <span>{{ $ts.objectStoragePrefix }}</span> - <template #desc>{{ $ts.objectStoragePrefixDesc }}</template> - </FormInput> - - <FormInput v-model="objectStorageEndpoint"> - <span>{{ $ts.objectStorageEndpoint }}</span> - <template #desc>{{ $ts.objectStorageEndpointDesc }}</template> - </FormInput> - - <FormInput v-model="objectStorageRegion"> - <span>{{ $ts.objectStorageRegion }}</span> - <template #desc>{{ $ts.objectStorageRegionDesc }}</template> - </FormInput> - - <FormInput v-model="objectStorageAccessKey"> - <template #prefix><i class="fas fa-key"></i></template> - <span>Access key</span> - </FormInput> - - <FormInput v-model="objectStorageSecretKey"> - <template #prefix><i class="fas fa-key"></i></template> - <span>Secret key</span> - </FormInput> - - <FormSwitch v-model="objectStorageUseSSL"> - {{ $ts.objectStorageUseSSL }} - <template #desc>{{ $ts.objectStorageUseSSLDesc }}</template> - </FormSwitch> - - <FormSwitch v-model="objectStorageUseProxy"> - {{ $ts.objectStorageUseProxy }} - <template #desc>{{ $ts.objectStorageUseProxyDesc }}</template> - </FormSwitch> - - <FormSwitch v-model="objectStorageSetPublicRead"> - {{ $ts.objectStorageSetPublicRead }} - </FormSwitch> - - <FormSwitch v-model="objectStorageS3ForcePathStyle"> - s3ForcePathStyle - </FormSwitch> - </template> - - <FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton> - </FormSuspense> -</FormBase> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import FormSwitch from '@client/components/debobigego/switch.vue'; -import FormInput from '@client/components/debobigego/input.vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import FormSuspense from '@client/components/debobigego/suspense.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; -import { fetchInstance } from '@client/instance'; - -export default defineComponent({ - components: { - FormSwitch, - FormInput, - FormBase, - FormGroup, - FormButton, - FormSuspense, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.objectStorage, - icon: 'fas fa-cloud', - bg: 'var(--bg)', - }, - useObjectStorage: false, - objectStorageBaseUrl: null, - objectStorageBucket: null, - objectStoragePrefix: null, - objectStorageEndpoint: null, - objectStorageRegion: null, - objectStoragePort: null, - objectStorageAccessKey: null, - objectStorageSecretKey: null, - objectStorageUseSSL: false, - objectStorageUseProxy: false, - objectStorageSetPublicRead: false, - objectStorageS3ForcePathStyle: true, - } - }, - - async mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - methods: { - async init() { - const meta = await os.api('meta', { detail: true }); - this.useObjectStorage = meta.useObjectStorage; - this.objectStorageBaseUrl = meta.objectStorageBaseUrl; - this.objectStorageBucket = meta.objectStorageBucket; - this.objectStoragePrefix = meta.objectStoragePrefix; - this.objectStorageEndpoint = meta.objectStorageEndpoint; - this.objectStorageRegion = meta.objectStorageRegion; - this.objectStoragePort = meta.objectStoragePort; - this.objectStorageAccessKey = meta.objectStorageAccessKey; - this.objectStorageSecretKey = meta.objectStorageSecretKey; - this.objectStorageUseSSL = meta.objectStorageUseSSL; - this.objectStorageUseProxy = meta.objectStorageUseProxy; - this.objectStorageSetPublicRead = meta.objectStorageSetPublicRead; - this.objectStorageS3ForcePathStyle = meta.objectStorageS3ForcePathStyle; - }, - save() { - os.apiWithDialog('admin/update-meta', { - useObjectStorage: this.useObjectStorage, - objectStorageBaseUrl: this.objectStorageBaseUrl ? this.objectStorageBaseUrl : null, - objectStorageBucket: this.objectStorageBucket ? this.objectStorageBucket : null, - objectStoragePrefix: this.objectStoragePrefix ? this.objectStoragePrefix : null, - objectStorageEndpoint: this.objectStorageEndpoint ? this.objectStorageEndpoint : null, - objectStorageRegion: this.objectStorageRegion ? this.objectStorageRegion : null, - objectStoragePort: this.objectStoragePort ? this.objectStoragePort : null, - objectStorageAccessKey: this.objectStorageAccessKey ? this.objectStorageAccessKey : null, - objectStorageSecretKey: this.objectStorageSecretKey ? this.objectStorageSecretKey : null, - objectStorageUseSSL: this.objectStorageUseSSL, - objectStorageUseProxy: this.objectStorageUseProxy, - objectStorageSetPublicRead: this.objectStorageSetPublicRead, - objectStorageS3ForcePathStyle: this.objectStorageS3ForcePathStyle, - }).then(() => { - fetchInstance(); - }); - } - } -}); -</script> diff --git a/src/client/pages/admin/other-settings.vue b/src/client/pages/admin/other-settings.vue deleted file mode 100644 index 4e55df41fb..0000000000 --- a/src/client/pages/admin/other-settings.vue +++ /dev/null @@ -1,83 +0,0 @@ -<template> -<FormBase> - <FormSuspense :p="init"> - <FormGroup> - <FormInput v-model="summalyProxy"> - <template #prefix><i class="fas fa-link"></i></template> - Summaly Proxy URL - </FormInput> - </FormGroup> - <FormGroup> - <FormInput v-model="deeplAuthKey"> - <template #prefix><i class="fas fa-key"></i></template> - DeepL Auth Key - </FormInput> - <FormSwitch v-model="deeplIsPro"> - Pro account - </FormSwitch> - </FormGroup> - <FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton> - </FormSuspense> -</FormBase> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import FormSwitch from '@client/components/debobigego/switch.vue'; -import FormInput from '@client/components/debobigego/input.vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import FormSuspense from '@client/components/debobigego/suspense.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; -import { fetchInstance } from '@client/instance'; - -export default defineComponent({ - components: { - FormSwitch, - FormInput, - FormBase, - FormGroup, - FormButton, - FormSuspense, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.other, - icon: 'fas fa-cogs', - bg: 'var(--bg)', - }, - summalyProxy: '', - deeplAuthKey: '', - deeplIsPro: false, - } - }, - - async mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - 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(); - }); - } - } -}); -</script> diff --git a/src/client/pages/admin/overview.vue b/src/client/pages/admin/overview.vue deleted file mode 100644 index ced200351e..0000000000 --- a/src/client/pages/admin/overview.vue +++ /dev/null @@ -1,236 +0,0 @@ -<template> -<div class="edbbcaef" v-size="{ max: [740] }"> - <div v-if="stats" class="cfcdecdf" style="margin: var(--margin)"> - <div class="number _panel"> - <div class="label">Users</div> - <div class="value _monospace"> - {{ number(stats.originalUsersCount) }} - <MkNumberDiff v-if="usersComparedToThePrevDay != null" class="diff" :value="usersComparedToThePrevDay" v-tooltip="$ts.dayOverDayChanges"><template #before>(</template><template #after>)</template></MkNumberDiff> - </div> - </div> - <div class="number _panel"> - <div class="label">Notes</div> - <div class="value _monospace"> - {{ number(stats.originalNotesCount) }} - <MkNumberDiff v-if="notesComparedToThePrevDay != null" class="diff" :value="notesComparedToThePrevDay" v-tooltip="$ts.dayOverDayChanges"><template #before>(</template><template #after>)</template></MkNumberDiff> - </div> - </div> - </div> - - <MkContainer :foldable="true" class="charts"> - <template #header><i class="fas fa-chart-bar"></i>{{ $ts.charts }}</template> - <div style="padding-top: 12px;"> - <MkInstanceStats :chart-limit="500" :detailed="true"/> - </div> - </MkContainer> - - <div class="queue"> - <MkContainer :foldable="true" :thin="true" class="deliver"> - <template #header>Queue: deliver</template> - <MkQueueChart :connection="queueStatsConnection" domain="deliver"/> - </MkContainer> - <MkContainer :foldable="true" :thin="true" class="inbox"> - <template #header>Queue: inbox</template> - <MkQueueChart :connection="queueStatsConnection" domain="inbox"/> - </MkContainer> - </div> - - <!--<XMetrics/>--> - - <MkFolder style="margin: var(--margin)"> - <template #header><i class="fas fa-info-circle"></i> {{ $ts.info }}</template> - <div class="cfcdecdf"> - <div class="number _panel"> - <div class="label">Misskey</div> - <div class="value _monospace">{{ version }}</div> - </div> - <div class="number _panel" v-if="serverInfo"> - <div class="label">Node.js</div> - <div class="value _monospace">{{ serverInfo.node }}</div> - </div> - <div class="number _panel" v-if="serverInfo"> - <div class="label">PostgreSQL</div> - <div class="value _monospace">{{ serverInfo.psql }}</div> - </div> - <div class="number _panel" v-if="serverInfo"> - <div class="label">Redis</div> - <div class="value _monospace">{{ serverInfo.redis }}</div> - </div> - <div class="number _panel"> - <div class="label">Vue</div> - <div class="value _monospace">{{ vueVersion }}</div> - </div> - </div> - </MkFolder> -</div> -</template> - -<script lang="ts"> -import { computed, defineComponent, markRaw, version as vueVersion } from 'vue'; -import FormKeyValueView from '@client/components/debobigego/key-value-view.vue'; -import MkInstanceStats from '@client/components/instance-stats.vue'; -import MkButton from '@client/components/ui/button.vue'; -import MkSelect from '@client/components/form/select.vue'; -import MkNumberDiff from '@client/components/number-diff.vue'; -import MkContainer from '@client/components/ui/container.vue'; -import MkFolder from '@client/components/ui/folder.vue'; -import MkQueueChart from '@client/components/queue-chart.vue'; -import { version, url } from '@client/config'; -import bytes from '@client/filters/bytes'; -import number from '@client/filters/number'; -import MkInstanceInfo from './instance.vue'; -import XMetrics from './metrics.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - MkNumberDiff, - FormKeyValueView, - MkInstanceStats, - MkContainer, - MkFolder, - MkQueueChart, - XMetrics, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.dashboard, - icon: 'fas fa-tachometer-alt', - bg: 'var(--bg)', - }, - version, - vueVersion, - url, - stats: null, - meta: null, - serverInfo: null, - usersComparedToThePrevDay: null, - notesComparedToThePrevDay: null, - fetchJobs: () => os.api('admin/queue/deliver-delayed', {}), - fetchModLogs: () => os.api('admin/show-moderation-logs', {}), - queueStatsConnection: markRaw(os.stream.useChannel('queueStats')), - } - }, - - async mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - - os.api('meta', { detail: true }).then(meta => { - this.meta = meta; - }); - - os.api('stats', {}).then(stats => { - this.stats = stats; - - os.api('charts/users', { limit: 2, span: 'day' }).then(chart => { - this.usersComparedToThePrevDay = this.stats.originalUsersCount - chart.local.total[1]; - }); - - os.api('charts/notes', { limit: 2, span: 'day' }).then(chart => { - this.notesComparedToThePrevDay = this.stats.originalNotesCount - chart.local.total[1]; - }); - }); - - os.api('admin/server-info', {}).then(serverInfo => { - this.serverInfo = serverInfo; - }); - - this.$nextTick(() => { - this.queueStatsConnection.send('requestLog', { - id: Math.random().toString().substr(2, 8), - length: 200 - }); - }); - }, - - beforeUnmount() { - this.queueStatsConnection.dispose(); - }, - - methods: { - async showInstanceInfo(q) { - let instance = q; - if (typeof q === 'string') { - instance = await os.api('federation/show-instance', { - host: q - }); - } - os.popup(MkInstanceInfo, { - instance: instance - }, {}, 'closed'); - }, - - bytes, - - number, - } -}); -</script> - -<style lang="scss" scoped> -.edbbcaef { - .cfcdecdf { - display: grid; - grid-gap: 8px; - grid-template-columns: repeat(auto-fill,minmax(150px,1fr)); - - > .number { - padding: 12px 16px; - - > .label { - opacity: 0.7; - font-size: 0.8em; - } - - > .value { - font-weight: bold; - font-size: 1.2em; - - > .diff { - font-size: 0.8em; - } - } - } - } - - > .charts { - margin: var(--margin); - } - - > .queue { - margin: var(--margin); - display: flex; - - > .deliver, - > .inbox { - flex: 1; - width: 50%; - - &:not(:first-child) { - margin-left: var(--margin); - } - } - } - - &.max-width_740px { - > .queue { - display: block; - - > .deliver, - > .inbox { - width: 100%; - - &:not(:first-child) { - margin-top: var(--margin); - margin-left: 0; - } - } - } - } -} -</style> diff --git a/src/client/pages/admin/proxy-account.vue b/src/client/pages/admin/proxy-account.vue deleted file mode 100644 index b1ece19710..0000000000 --- a/src/client/pages/admin/proxy-account.vue +++ /dev/null @@ -1,87 +0,0 @@ -<template> -<FormBase> - <FormSuspense :p="init"> - <FormGroup> - <FormKeyValueView> - <template #key>{{ $ts.proxyAccount }}</template> - <template #value>{{ proxyAccount ? `@${proxyAccount.username}` : $ts.none }}</template> - </FormKeyValueView> - <template #caption>{{ $ts.proxyAccountDescription }}</template> - </FormGroup> - - <FormButton @click="chooseProxyAccount" primary>{{ $ts.selectAccount }}</FormButton> - </FormSuspense> -</FormBase> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import FormKeyValueView from '@client/components/debobigego/key-value-view.vue'; -import FormInput from '@client/components/debobigego/input.vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import FormTextarea from '@client/components/debobigego/textarea.vue'; -import FormInfo from '@client/components/debobigego/info.vue'; -import FormSuspense from '@client/components/debobigego/suspense.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; -import { fetchInstance } from '@client/instance'; - -export default defineComponent({ - components: { - FormKeyValueView, - FormInput, - FormBase, - FormGroup, - FormButton, - FormTextarea, - FormInfo, - FormSuspense, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.proxyAccount, - icon: 'fas fa-ghost', - bg: 'var(--bg)', - }, - proxyAccount: null, - proxyAccountId: null, - } - }, - - async mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - methods: { - async init() { - const meta = await os.api('meta', { detail: true }); - this.proxyAccountId = meta.proxyAccountId; - if (this.proxyAccountId) { - this.proxyAccount = await os.api('users/show', { userId: this.proxyAccountId }); - } - }, - - chooseProxyAccount() { - os.selectUser().then(user => { - this.proxyAccount = user; - this.proxyAccountId = user.id; - this.save(); - }); - }, - - save() { - os.apiWithDialog('admin/update-meta', { - proxyAccountId: this.proxyAccountId, - }).then(() => { - fetchInstance(); - }); - } - } -}); -</script> diff --git a/src/client/pages/admin/queue.chart.vue b/src/client/pages/admin/queue.chart.vue deleted file mode 100644 index 084181a606..0000000000 --- a/src/client/pages/admin/queue.chart.vue +++ /dev/null @@ -1,102 +0,0 @@ -<template> -<div class="_debobigegoItem"> - <div class="_debobigegoLabel"><slot name="title"></slot></div> - <div class="_debobigegoPanel pumxzjhg"> - <div class="_table status"> - <div class="_row"> - <div class="_cell"><div class="_label">Process</div>{{ number(activeSincePrevTick) }}</div> - <div class="_cell"><div class="_label">Active</div>{{ number(active) }}</div> - <div class="_cell"><div class="_label">Waiting</div>{{ number(waiting) }}</div> - <div class="_cell"><div class="_label">Delayed</div>{{ number(delayed) }}</div> - </div> - </div> - <div class=""> - <MkQueueChart :domain="domain" :connection="connection"/> - </div> - <div class="jobs"> - <div v-if="jobs.length > 0"> - <div v-for="job in jobs" :key="job[0]"> - <span>{{ job[0] }}</span> - <span style="margin-left: 8px; opacity: 0.7;">({{ number(job[1]) }} jobs)</span> - </div> - </div> - <span v-else style="opacity: 0.5;">{{ $ts.noJobs }}</span> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import { defineComponent, markRaw, onMounted, onUnmounted, ref } from 'vue'; -import number from '@client/filters/number'; -import MkQueueChart from '@client/components/queue-chart.vue'; -import * as os from '@client/os'; - -export default defineComponent({ - components: { - MkQueueChart - }, - - props: { - domain: { - type: String, - required: true, - }, - connection: { - required: true, - }, - }, - - setup(props) { - const activeSincePrevTick = ref(0); - const active = ref(0); - const waiting = ref(0); - const delayed = ref(0); - const jobs = ref([]); - - onMounted(() => { - os.api(props.domain === 'inbox' ? 'admin/queue/inbox-delayed' : props.domain === 'deliver' ? 'admin/queue/deliver-delayed' : null, {}).then(jobs => { - jobs.value = jobs; - }); - - const onStats = (stats) => { - activeSincePrevTick.value = stats[props.domain].activeSincePrevTick; - active.value = stats[props.domain].active; - waiting.value = stats[props.domain].waiting; - delayed.value = stats[props.domain].delayed; - }; - - props.connection.on('stats', onStats); - - onUnmounted(() => { - props.connection.off('stats', onStats); - }); - }); - - return { - jobs, - activeSincePrevTick, - active, - waiting, - delayed, - number, - }; - }, -}); -</script> - -<style lang="scss" scoped> -.pumxzjhg { - > .status { - padding: 16px; - border-bottom: solid 0.5px var(--divider); - } - - > .jobs { - padding: 16px; - border-top: solid 0.5px var(--divider); - max-height: 180px; - overflow: auto; - } -} -</style> diff --git a/src/client/pages/admin/queue.vue b/src/client/pages/admin/queue.vue deleted file mode 100644 index f88825eb19..0000000000 --- a/src/client/pages/admin/queue.vue +++ /dev/null @@ -1,73 +0,0 @@ -<template> -<FormBase> - <XQueue :connection="connection" domain="inbox"> - <template #title>In</template> - </XQueue> - <XQueue :connection="connection" domain="deliver"> - <template #title>Out</template> - </XQueue> - <FormButton @click="clear()" danger><i class="fas fa-trash-alt"></i> {{ $ts.clearQueue }}</FormButton> -</FormBase> -</template> - -<script lang="ts"> -import { defineComponent, markRaw } from 'vue'; -import MkButton from '@client/components/ui/button.vue'; -import XQueue from './queue.chart.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - FormBase, - FormButton, - MkButton, - XQueue, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.jobQueue, - icon: 'fas fa-clipboard-list', - bg: 'var(--bg)', - }, - connection: markRaw(os.stream.useChannel('queueStats')), - } - }, - - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - - this.$nextTick(() => { - this.connection.send('requestLog', { - id: Math.random().toString().substr(2, 8), - length: 200 - }); - }); - }, - - beforeUnmount() { - this.connection.dispose(); - }, - - methods: { - clear() { - os.dialog({ - type: 'warning', - title: this.$ts.clearQueueConfirmTitle, - text: this.$ts.clearQueueConfirmText, - showCancelButton: true - }).then(({ canceled }) => { - if (canceled) return; - - os.apiWithDialog('admin/queue/clear', {}); - }); - } - } -}); -</script> diff --git a/src/client/pages/admin/relays.vue b/src/client/pages/admin/relays.vue deleted file mode 100644 index 7d7888eaa8..0000000000 --- a/src/client/pages/admin/relays.vue +++ /dev/null @@ -1,99 +0,0 @@ -<template> -<FormBase class="relaycxt"> - <FormButton @click="addRelay" primary><i class="fas fa-plus"></i> {{ $ts.addRelay }}</FormButton> - - <div class="_debobigegoItem" v-for="relay in relays" :key="relay.inbox"> - <div class="_debobigegoPanel" style="padding: 16px;"> - <div>{{ relay.inbox }}</div> - <div>{{ $t(`_relayStatus.${relay.status}`) }}</div> - <MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton> - </div> - </div> -</FormBase> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import MkButton from '@client/components/ui/button.vue'; -import MkInput from '@client/components/form/input.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - FormBase, - FormButton, - MkButton, - MkInput, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.relays, - icon: 'fas fa-globe', - bg: 'var(--bg)', - }, - relays: [], - inbox: '', - } - }, - - created() { - this.refresh(); - }, - - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - methods: { - async addRelay() { - const { canceled, result: inbox } = await os.dialog({ - title: this.$ts.addRelay, - input: { - placeholder: this.$ts.inboxUrl - } - }); - if (canceled) return; - os.api('admin/relays/add', { - inbox - }).then((relay: any) => { - this.refresh(); - }).catch((e: any) => { - os.dialog({ - type: 'error', - text: e.message || e - }); - }); - }, - - remove(inbox: string) { - os.api('admin/relays/remove', { - inbox - }).then(() => { - this.refresh(); - }).catch((e: any) => { - os.dialog({ - type: 'error', - text: e.message || e - }); - }); - }, - - refresh() { - os.api('admin/relays/list').then((relays: any) => { - this.relays = relays; - }); - } - } -}); -</script> - -<style lang="scss" scoped> - -</style> diff --git a/src/client/pages/admin/security.vue b/src/client/pages/admin/security.vue deleted file mode 100644 index 4365b6800c..0000000000 --- a/src/client/pages/admin/security.vue +++ /dev/null @@ -1,83 +0,0 @@ -<template> -<FormBase> - <FormSuspense :p="init"> - <FormLink to="/admin/bot-protection"> - <i class="fas fa-shield-alt"></i> {{ $ts.botProtection }} - <template #suffix v-if="enableHcaptcha">hCaptcha</template> - <template #suffix v-else-if="enableRecaptcha">reCAPTCHA</template> - <template #suffix v-else>{{ $ts.none }} ({{ $ts.notRecommended }})</template> - </FormLink> - - <FormSwitch v-model="enableRegistration">{{ $ts.enableRegistration }}</FormSwitch> - - <FormSwitch v-model="emailRequiredForSignup">{{ $ts.emailRequiredForSignup }}</FormSwitch> - - <FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton> - </FormSuspense> -</FormBase> -</template> - -<script lang="ts"> -import { defineAsyncComponent, defineComponent } from 'vue'; -import FormLink from '@client/components/debobigego/link.vue'; -import FormSwitch from '@client/components/debobigego/switch.vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import FormInfo from '@client/components/debobigego/info.vue'; -import FormSuspense from '@client/components/debobigego/suspense.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; -import { fetchInstance } from '@client/instance'; - -export default defineComponent({ - components: { - FormLink, - FormSwitch, - FormBase, - FormGroup, - FormButton, - FormInfo, - FormSuspense, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.security, - icon: 'fas fa-lock', - bg: 'var(--bg)', - }, - enableHcaptcha: false, - enableRecaptcha: false, - enableRegistration: false, - emailRequiredForSignup: false, - } - }, - - async mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - methods: { - async init() { - const meta = await os.api('meta', { detail: true }); - this.enableHcaptcha = meta.enableHcaptcha; - this.enableRecaptcha = meta.enableRecaptcha; - this.enableRegistration = !meta.disableRegistration; - this.emailRequiredForSignup = meta.emailRequiredForSignup; - }, - - save() { - os.apiWithDialog('admin/update-meta', { - disableRegistration: !this.enableRegistration, - emailRequiredForSignup: this.emailRequiredForSignup, - }).then(() => { - fetchInstance(); - }); - } - } -}); -</script> diff --git a/src/client/pages/admin/service-worker.vue b/src/client/pages/admin/service-worker.vue deleted file mode 100644 index 430e02ad2e..0000000000 --- a/src/client/pages/admin/service-worker.vue +++ /dev/null @@ -1,85 +0,0 @@ -<template> -<FormBase> - <FormSuspense :p="init"> - <FormSwitch v-model="enableServiceWorker"> - {{ $ts.enableServiceworker }} - <template #desc>{{ $ts.serviceworkerInfo }}</template> - </FormSwitch> - - <template v-if="enableServiceWorker"> - <FormInput v-model="swPublicKey"> - <template #prefix><i class="fas fa-key"></i></template> - Public key - </FormInput> - - <FormInput v-model="swPrivateKey"> - <template #prefix><i class="fas fa-key"></i></template> - Private key - </FormInput> - </template> - - <FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton> - </FormSuspense> -</FormBase> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import FormSwitch from '@client/components/debobigego/switch.vue'; -import FormInput from '@client/components/debobigego/input.vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import FormSuspense from '@client/components/debobigego/suspense.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; -import { fetchInstance } from '@client/instance'; - -export default defineComponent({ - components: { - FormSwitch, - FormInput, - FormBase, - FormGroup, - 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> diff --git a/src/client/pages/admin/settings.vue b/src/client/pages/admin/settings.vue deleted file mode 100644 index 7bd363e5f3..0000000000 --- a/src/client/pages/admin/settings.vue +++ /dev/null @@ -1,151 +0,0 @@ -<template> -<FormBase> - <FormSuspense :p="init"> - <FormInput v-model="name"> - <span>{{ $ts.instanceName }}</span> - </FormInput> - - <FormTextarea v-model="description"> - <span>{{ $ts.instanceDescription }}</span> - </FormTextarea> - - <FormInput v-model="iconUrl"> - <template #prefix><i class="fas fa-link"></i></template> - <span>{{ $ts.iconUrl }}</span> - </FormInput> - - <FormInput v-model="bannerUrl"> - <template #prefix><i class="fas fa-link"></i></template> - <span>{{ $ts.bannerUrl }}</span> - </FormInput> - - <FormInput v-model="backgroundImageUrl"> - <template #prefix><i class="fas fa-link"></i></template> - <span>{{ $ts.backgroundImageUrl }}</span> - </FormInput> - - <FormInput v-model="tosUrl"> - <template #prefix><i class="fas fa-link"></i></template> - <span>{{ $ts.tosUrl }}</span> - </FormInput> - - <FormInput v-model="maintainerName"> - <span>{{ $ts.maintainerName }}</span> - </FormInput> - - <FormInput v-model="maintainerEmail" type="email"> - <template #prefix><i class="fas fa-envelope"></i></template> - <span>{{ $ts.maintainerEmail }}</span> - </FormInput> - - <FormTextarea v-model="pinnedUsers"> - <span>{{ $ts.pinnedUsers }}</span> - <template #desc>{{ $ts.pinnedUsersDescription }}</template> - </FormTextarea> - - <FormInput v-model="maxNoteTextLength" type="number"> - <template #prefix><i class="fas fa-pencil-alt"></i></template> - <span>{{ $ts.maxNoteTextLength }}</span> - </FormInput> - - <FormSwitch v-model="enableLocalTimeline">{{ $ts.enableLocalTimeline }}</FormSwitch> - <FormSwitch v-model="enableGlobalTimeline">{{ $ts.enableGlobalTimeline }}</FormSwitch> - <FormInfo>{{ $ts.disablingTimelinesInfo }}</FormInfo> - - <FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton> - </FormSuspense> -</FormBase> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import FormSwitch from '@client/components/debobigego/switch.vue'; -import FormInput from '@client/components/debobigego/input.vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import FormTextarea from '@client/components/debobigego/textarea.vue'; -import FormInfo from '@client/components/debobigego/info.vue'; -import FormSuspense from '@client/components/debobigego/suspense.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; -import { fetchInstance } from '@client/instance'; - -export default defineComponent({ - components: { - FormSwitch, - FormInput, - FormBase, - FormGroup, - FormButton, - FormTextarea, - FormInfo, - FormSuspense, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.general, - icon: 'fas fa-cog', - bg: 'var(--bg)', - }, - name: null, - description: null, - tosUrl: null as string | null, - maintainerName: null, - maintainerEmail: null, - iconUrl: null, - bannerUrl: null, - backgroundImageUrl: null, - maxNoteTextLength: 0, - enableLocalTimeline: false, - enableGlobalTimeline: false, - pinnedUsers: '', - } - }, - - async mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - methods: { - async init() { - const meta = await os.api('meta', { detail: true }); - this.name = meta.name; - this.description = meta.description; - this.tosUrl = meta.tosUrl; - this.iconUrl = meta.iconUrl; - this.bannerUrl = meta.bannerUrl; - this.backgroundImageUrl = meta.backgroundImageUrl; - this.maintainerName = meta.maintainerName; - this.maintainerEmail = meta.maintainerEmail; - this.maxNoteTextLength = meta.maxNoteTextLength; - this.enableLocalTimeline = !meta.disableLocalTimeline; - this.enableGlobalTimeline = !meta.disableGlobalTimeline; - this.pinnedUsers = meta.pinnedUsers.join('\n'); - }, - - save() { - os.apiWithDialog('admin/update-meta', { - name: this.name, - description: this.description, - tosUrl: this.tosUrl, - iconUrl: this.iconUrl, - bannerUrl: this.bannerUrl, - backgroundImageUrl: this.backgroundImageUrl, - maintainerName: this.maintainerName, - maintainerEmail: this.maintainerEmail, - maxNoteTextLength: this.maxNoteTextLength, - disableLocalTimeline: !this.enableLocalTimeline, - disableGlobalTimeline: !this.enableGlobalTimeline, - pinnedUsers: this.pinnedUsers.split('\n'), - }).then(() => { - fetchInstance(); - }); - } - } -}); -</script> diff --git a/src/client/pages/admin/users.vue b/src/client/pages/admin/users.vue deleted file mode 100644 index 37a54d2de3..0000000000 --- a/src/client/pages/admin/users.vue +++ /dev/null @@ -1,254 +0,0 @@ -<template> -<div class="lknzcolw"> - <div class="users"> - <div class="inputs"> - <MkSelect v-model="sort" style="flex: 1;"> - <template #label>{{ $ts.sort }}</template> - <option value="-createdAt">{{ $ts.registeredDate }} ({{ $ts.ascendingOrder }})</option> - <option value="+createdAt">{{ $ts.registeredDate }} ({{ $ts.descendingOrder }})</option> - <option value="-updatedAt">{{ $ts.lastUsed }} ({{ $ts.ascendingOrder }})</option> - <option value="+updatedAt">{{ $ts.lastUsed }} ({{ $ts.descendingOrder }})</option> - </MkSelect> - <MkSelect v-model="state" style="flex: 1;"> - <template #label>{{ $ts.state }}</template> - <option value="all">{{ $ts.all }}</option> - <option value="available">{{ $ts.normal }}</option> - <option value="admin">{{ $ts.administrator }}</option> - <option value="moderator">{{ $ts.moderator }}</option> - <option value="silenced">{{ $ts.silence }}</option> - <option value="suspended">{{ $ts.suspend }}</option> - </MkSelect> - <MkSelect v-model="origin" style="flex: 1;"> - <template #label>{{ $ts.instance }}</template> - <option value="combined">{{ $ts.all }}</option> - <option value="local">{{ $ts.local }}</option> - <option value="remote">{{ $ts.remote }}</option> - </MkSelect> - </div> - <div class="inputs"> - <MkInput v-model="searchUsername" style="flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.users.reload()"> - <template #prefix>@</template> - <template #label>{{ $ts.username }}</template> - </MkInput> - <MkInput v-model="searchHost" style="flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.users.reload()" :disabled="pagination.params().origin === 'local'"> - <template #prefix>@</template> - <template #label>{{ $ts.host }}</template> - </MkInput> - </div> - - <MkPagination :pagination="pagination" #default="{items}" class="users" ref="users"> - <button class="user _panel _button _gap" v-for="user in items" :key="user.id" @click="show(user)"> - <MkAvatar class="avatar" :user="user" :disable-link="true" :show-indicator="true"/> - <div class="body"> - <header> - <MkUserName class="name" :user="user"/> - <span class="acct">@{{ acct(user) }}</span> - <span class="staff" v-if="user.isAdmin"><i class="fas fa-bookmark"></i></span> - <span class="staff" v-if="user.isModerator"><i class="far fa-bookmark"></i></span> - <span class="punished" v-if="user.isSilenced"><i class="fas fa-microphone-slash"></i></span> - <span class="punished" v-if="user.isSuspended"><i class="fas fa-snowflake"></i></span> - </header> - <div> - <span>{{ $ts.lastUsed }}: <MkTime v-if="user.updatedAt" :time="user.updatedAt" mode="detail"/></span> - </div> - <div> - <span>{{ $ts.registeredDate }}: <MkTime :time="user.createdAt" mode="detail"/></span> - </div> - </div> - </button> - </MkPagination> - </div> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import MkButton from '@client/components/ui/button.vue'; -import MkInput from '@client/components/form/input.vue'; -import MkSelect from '@client/components/form/select.vue'; -import MkPagination from '@client/components/ui/pagination.vue'; -import { acct } from '@client/filters/user'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; -import { lookupUser } from '@client/scripts/lookup-user'; - -export default defineComponent({ - components: { - MkButton, - MkInput, - MkSelect, - MkPagination, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.users, - icon: 'fas fa-users', - bg: 'var(--bg)', - actions: [{ - icon: 'fas fa-search', - text: this.$ts.search, - handler: this.searchUser - }, { - asFullButton: true, - icon: 'fas fa-plus', - text: this.$ts.addUser, - handler: this.addUser - }, { - asFullButton: true, - icon: 'fas fa-search', - text: this.$ts.lookup, - handler: this.lookupUser - }], - }, - sort: '+createdAt', - state: 'all', - origin: 'local', - searchUsername: '', - searchHost: '', - pagination: { - endpoint: 'admin/show-users', - limit: 10, - params: () => ({ - sort: this.sort, - state: this.state, - origin: this.origin, - username: this.searchUsername, - hostname: this.searchHost, - }), - offsetMode: true - }, - } - }, - - watch: { - sort() { - this.$refs.users.reload(); - }, - state() { - this.$refs.users.reload(); - }, - origin() { - this.$refs.users.reload(); - }, - }, - - async mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - methods: { - lookupUser, - - searchUser() { - os.selectUser().then(user => { - this.show(user); - }); - }, - - async addUser() { - const { canceled: canceled1, result: username } = await os.dialog({ - title: this.$ts.username, - input: true - }); - if (canceled1) return; - - const { canceled: canceled2, result: password } = await os.dialog({ - title: this.$ts.password, - input: { type: 'password' } - }); - if (canceled2) return; - - os.apiWithDialog('admin/accounts/create', { - username: username, - password: password, - }).then(res => { - this.$refs.users.reload(); - }); - }, - - show(user) { - os.pageWindow(`/user-info/${user.id}`); - }, - - acct - } -}); -</script> - -<style lang="scss" scoped> -.lknzcolw { - > .users { - margin: var(--margin); - - > .inputs { - display: flex; - margin-bottom: 16px; - - > * { - margin-right: 16px; - - &:last-child { - margin-right: 0; - } - } - } - - > .users { - margin-top: var(--margin); - - > .user { - display: flex; - width: 100%; - box-sizing: border-box; - text-align: left; - align-items: center; - padding: 16px; - - &:hover { - color: var(--accent); - } - - > .avatar { - width: 60px; - height: 60px; - } - - > .body { - margin-left: 0.3em; - padding: 0 8px; - flex: 1; - - @media (max-width: 500px) { - font-size: 14px; - } - - > header { - > .name { - font-weight: bold; - } - - > .acct { - margin-left: 8px; - opacity: 0.7; - } - - > .staff { - margin-left: 0.5em; - color: var(--badge); - } - - > .punished { - margin-left: 0.5em; - color: #4dabf7; - } - } - } - } - } - } -} -</style> diff --git a/src/client/pages/advanced-theme-editor.vue b/src/client/pages/advanced-theme-editor.vue deleted file mode 100644 index 8a63d74887..0000000000 --- a/src/client/pages/advanced-theme-editor.vue +++ /dev/null @@ -1,352 +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 class="item" v-for="([ k, v ], i) in theme" :key="k"> - <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" v-text="baseProps[k]" class="default-value" /> - <!-- 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 class="select" v-else-if="v.type === 'refProp'" v-model="v.key"> - <option v-for="key in themeProps" :value="key" :key="key">{{ $t('_theme.keys.' + key) }}</option> - </MkSelect> - <!-- func --> - <template v-else-if="v.type === 'func'"> - <MkSelect class="select" v-model="v.name"> - <template #label>{{ $ts._theme.funcKind }}</template> - <option v-for="n in ['alpha', 'darken', 'lighten']" :value="n" :key="n">{{ $t('_theme.' + n) }}</option> - </MkSelect> - <MkInput type="number" v-model="v.arg"><span>{{ $ts._theme.argument }}</span></MkInput> - <MkSelect class="select" v-model="v.value"> - <template #label>{{ $ts._theme.basedProp }}</template> - <option v-for="key in themeProps" :value="key" :key="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 '@client/components/form/radio.vue'; -import MkButton from '@client/components/ui/button.vue'; -import MkInput from '@client/components/form/input.vue'; -import MkTextarea from '@client/components/form/textarea.vue'; -import MkSelect from '@client/components/form/select.vue'; -import MkSample from '@client/components/sample.vue'; - -import { convertToMisskeyTheme, ThemeValue, convertToViewModel, ThemeViewModel } from '@client/scripts/theme-editor'; -import { Theme, applyTheme, lightTheme, darkTheme, themeProps, validateTheme } from '@client/scripts/theme'; -import { host } from '@client/config'; -import * as os from '@client/os'; -import { ColdDeviceStorage } from '@client/store'; -import { addTheme } from '@client/theme-store'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - MkRadio, - MkButton, - MkInput, - MkTextarea, - MkSelect, - MkSample, - }, - - 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); - }, - - async beforeRouteLeave(to, from, next) { - if (this.changed && !(await this.confirm())) { - next(false); - } else { - next(); - } - }, - - 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.dialog({ - type: 'warning', - text: this.$ts.leaveConfirm, - showCancelButton: true - }); - 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.dialog({ - type: 'warning', - showCancelButton: true, - 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.dialog({ - title: this.$ts._theme.inputConstantName, - input: true - }); - 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.dialog({ - 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.dialog({ - 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.dialog({ - 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> diff --git a/src/client/pages/announcements.vue b/src/client/pages/announcements.vue deleted file mode 100644 index 429d183d1e..0000000000 --- a/src/client/pages/announcements.vue +++ /dev/null @@ -1,74 +0,0 @@ -<template> -<MkSpacer :content-max="800"> - <MkPagination :pagination="pagination" #default="{items}" class="ruryvtyk _content"> - <section class="_card announcement" v-for="(announcement, i) in items" :key="announcement.id"> - <div class="_title"><span v-if="$i && !announcement.isRead">๐ </span>{{ announcement.title }}</div> - <div class="_content"> - <Mfm :text="announcement.text"/> - <img v-if="announcement.imageUrl" :src="announcement.imageUrl"/> - </div> - <div class="_footer" v-if="$i && !announcement.isRead"> - <MkButton @click="read(items, announcement, i)" primary><i class="fas fa-check"></i> {{ $ts.gotIt }}</MkButton> - </div> - </section> - </MkPagination> -</MkSpacer> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import MkPagination from '@client/components/ui/pagination.vue'; -import MkButton from '@client/components/ui/button.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - MkPagination, - MkButton - }, - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.announcements, - icon: 'fas fa-broadcast-tower', - bg: 'var(--bg)', - }, - pagination: { - endpoint: 'announcements', - limit: 10, - }, - }; - }, - - methods: { - // TODO: ใใใฏๅฎ่ณช็ใซ่ฆชใณใณใใผใใณใใใๅญใณใณใใผใใณใใฎใใญใใใฃใๅคๆดใใฆใใฎใงใชใใจใใใใ - read(items, announcement, i) { - items[i] = { - ...announcement, - isRead: true, - }; - os.api('i/read-announcement', { announcementId: announcement.id }); - }, - } -}); -</script> - -<style lang="scss" scoped> -.ruryvtyk { - > .announcement { - &:not(:last-child) { - margin-bottom: var(--margin); - } - - > ._content { - > img { - display: block; - max-height: 300px; - max-width: 100%; - } - } - } -} -</style> diff --git a/src/client/pages/antenna-timeline.vue b/src/client/pages/antenna-timeline.vue deleted file mode 100644 index c99124dbdc..0000000000 --- a/src/client/pages/antenna-timeline.vue +++ /dev/null @@ -1,147 +0,0 @@ -<template> -<div class="tqmomfks" v-hotkey.global="keymap" v-size="{ min: [800] }"> - <div class="new" v-if="queue > 0"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div> - <div class="tl _block"> - <XTimeline ref="tl" class="tl" - :key="antennaId" - src="antenna" - :antenna="antennaId" - :sound="true" - @before="before()" - @after="after()" - @queue="queueUpdated" - /> - </div> -</div> -</template> - -<script lang="ts"> -import { defineComponent, defineAsyncComponent, computed } from 'vue'; -import Progress from '@client/scripts/loading'; -import XTimeline from '@client/components/timeline.vue'; -import { scroll } from '@client/scripts/scroll'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - XTimeline, - }, - - props: { - antennaId: { - type: String, - required: true - } - }, - - data() { - return { - antenna: null, - queue: 0, - [symbols.PAGE_INFO]: computed(() => this.antenna ? { - title: this.antenna.name, - icon: 'fas fa-satellite', - bg: 'var(--bg)', - actions: [{ - icon: 'fas fa-calendar-alt', - text: this.$ts.jumpToSpecifiedDate, - handler: this.timetravel - }, { - icon: 'fas fa-cog', - text: this.$ts.settings, - handler: this.settings - }], - } : null), - }; - }, - - computed: { - keymap(): any { - return { - 't': this.focus - }; - }, - }, - - watch: { - antennaId: { - async handler() { - this.antenna = await os.api('antennas/show', { - antennaId: this.antennaId - }); - }, - immediate: true - } - }, - - methods: { - before() { - Progress.start(); - }, - - after() { - Progress.done(); - }, - - queueUpdated(q) { - this.queue = q; - }, - - top() { - scroll(this.$el, { top: 0 }); - }, - - async timetravel() { - const { canceled, result: date } = await os.dialog({ - title: this.$ts.date, - input: { - type: 'date' - } - }); - if (canceled) return; - - this.$refs.tl.timetravel(new Date(date)); - }, - - settings() { - this.$router.push(`/my/antennas/${this.antennaId}`); - }, - - focus() { - (this.$refs.tl as any).focus(); - } - } -}); -</script> - -<style lang="scss" scoped> -.tqmomfks { - padding: var(--margin); - - > .new { - position: sticky; - top: calc(var(--stickyTop, 0px) + 16px); - z-index: 1000; - width: 100%; - - > button { - display: block; - margin: var(--margin) auto 0 auto; - padding: 8px 16px; - border-radius: 32px; - } - } - - > .tl { - background: var(--bg); - border-radius: var(--radius); - overflow: clip; - } - - &.min-width_800px { - max-width: 800px; - margin: 0 auto; - } -} -</style> diff --git a/src/client/pages/api-console.vue b/src/client/pages/api-console.vue deleted file mode 100644 index 9aa7d4ea4d..0000000000 --- a/src/client/pages/api-console.vue +++ /dev/null @@ -1,93 +0,0 @@ -<template> -<div class="_root"> - <div class="_block" style="padding: 24px;"> - <MkInput v-model="endpoint" :datalist="endpoints" @update:modelValue="onEndpointChange()" class=""> - <template #label>Endpoint</template> - </MkInput> - <MkTextarea v-model="body" code> - <template #label>Params (JSON or JSON5)</template> - </MkTextarea> - <MkSwitch v-model="withCredential"> - With credential - </MkSwitch> - <MkButton primary full @click="send" :disabled="sending"> - <template v-if="sending"><MkEllipsis/></template> - <template v-else><i class="fas fa-paper-plane"></i> Send</template> - </MkButton> - </div> - <div v-if="res" class="_block" style="padding: 24px;"> - <MkTextarea v-model="res" code readonly tall> - <template #label>Response</template> - </MkTextarea> - </div> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import * as JSON5 from 'json5'; -import MkButton from '@client/components/ui/button.vue'; -import MkInput from '@client/components/form/input.vue'; -import MkTextarea from '@client/components/form/textarea.vue'; -import MkSwitch from '@client/components/form/switch.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - MkButton, MkInput, MkTextarea, MkSwitch, - }, - - data() { - return { - [symbols.PAGE_INFO]: { - title: 'API console', - icon: 'fas fa-terminal' - }, - - endpoint: '', - body: '{}', - res: null, - sending: false, - endpoints: [], - withCredential: true, - - }; - }, - - created() { - os.api('endpoints').then(endpoints => { - this.endpoints = endpoints; - }); - }, - - methods: { - send() { - this.sending = true; - os.api(this.endpoint, JSON5.parse(this.body)).then(res => { - this.sending = false; - this.res = JSON5.stringify(res, null, 2); - }, err => { - this.sending = false; - this.res = JSON5.stringify(err, null, 2); - }); - }, - - onEndpointChange() { - os.api('endpoint', { endpoint: this.endpoint }, this.withCredential ? undefined : null).then(endpoint => { - const body = {}; - for (const p of endpoint.params) { - body[p.name] = - p.type === 'String' ? '' : - p.type === 'Number' ? 0 : - p.type === 'Boolean' ? false : - p.type === 'Array' ? [] : - p.type === 'Object' ? {} : - null; - } - this.body = JSON5.stringify(body, null, 2); - }); - } - } -}); -</script> diff --git a/src/client/pages/auth.form.vue b/src/client/pages/auth.form.vue deleted file mode 100644 index 10c466c73c..0000000000 --- a/src/client/pages/auth.form.vue +++ /dev/null @@ -1,60 +0,0 @@ -<template> -<section class="_section"> - <div class="_title">{{ $t('_auth.shareAccess', { name: app.name }) }}</div> - <div class="_content"> - <h2>{{ app.name }}</h2> - <p class="id">{{ app.id }}</p> - <p class="description">{{ app.description }}</p> - </div> - <div class="_content"> - <h2>{{ $ts._auth.permissionAsk }}</h2> - <ul> - <li v-for="p in app.permission" :key="p">{{ $t(`_permissions.${p}`) }}</li> - </ul> - </div> - <div class="_footer"> - <MkButton @click="cancel" inline>{{ $ts.cancel }}</MkButton> - <MkButton @click="accept" inline primary>{{ $ts.accept }}</MkButton> - </div> -</section> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import MkButton from '@client/components/ui/button.vue'; -import * as os from '@client/os'; - -export default defineComponent({ - components: { - MkButton - }, - props: ['session'], - computed: { - name(): string { - const el = document.createElement('div'); - el.textContent = this.app.name - return el.innerHTML; - }, - app(): any { - return this.session.app; - } - }, - methods: { - cancel() { - os.api('auth/deny', { - token: this.session.token - }).then(() => { - this.$emit('denied'); - }); - }, - - accept() { - os.api('auth/accept', { - token: this.session.token - }).then(() => { - this.$emit('accepted'); - }); - } - } -}); -</script> diff --git a/src/client/pages/auth.vue b/src/client/pages/auth.vue deleted file mode 100755 index 3656d48c42..0000000000 --- a/src/client/pages/auth.vue +++ /dev/null @@ -1,95 +0,0 @@ -<template> -<div class="" v-if="$i && fetching"> - <MkLoading/> -</div> -<div v-else-if="$i"> - <XForm - class="form" - ref="form" - v-if="state == 'waiting'" - :session="session" - @denied="state = 'denied'" - @accepted="accepted" - /> - <div class="denied" v-if="state == 'denied'"> - <h1>{{ $ts._auth.denied }}</h1> - </div> - <div class="accepted" v-if="state == 'accepted'"> - <h1>{{ session.app.isAuthorized ? this.$t('already-authorized') : this.$ts.allowed }}</h1> - <p v-if="session.app.callbackUrl">{{ $ts._auth.callback }}<MkEllipsis/></p> - <p v-if="!session.app.callbackUrl">{{ $ts._auth.pleaseGoBack }}</p> - </div> - <div class="error" v-if="state == 'fetch-session-error'"> - <p>{{ $ts.somethingHappened }}</p> - </div> -</div> -<div class="signin" v-else> - <MkSignin @login="onLogin"/> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import XForm from './auth.form.vue'; -import MkSignin from '@client/components/signin.vue'; -import * as os from '@client/os'; -import { login } from '@client/account'; - -export default defineComponent({ - components: { - XForm, - MkSignin, - }, - data() { - return { - state: null, - session: null, - fetching: true - }; - }, - computed: { - token(): string { - return this.$route.params.token; - } - }, - mounted() { - if (!this.$i) return; - - // Fetch session - os.api('auth/session/show', { - token: this.token - }).then(session => { - this.session = session; - this.fetching = false; - - // ๆขใซ้ฃๆบใใฆใใๅ ดๅ - if (this.session.app.isAuthorized) { - os.api('auth/accept', { - token: this.session.token - }).then(() => { - this.accepted(); - }); - } else { - this.state = 'waiting'; - } - }).catch(error => { - this.state = 'fetch-session-error'; - this.fetching = false; - }); - }, - methods: { - accepted() { - this.state = 'accepted'; - if (this.session.app.callbackUrl) { - location.href = `${this.session.app.callbackUrl}?token=${this.session.token}`; - } - }, onLogin(res) { - login(res.i); - } - } -}); -</script> - -<style lang="scss" scoped> - -</style> diff --git a/src/client/pages/channel-editor.vue b/src/client/pages/channel-editor.vue deleted file mode 100644 index 67e27896ce..0000000000 --- a/src/client/pages/channel-editor.vue +++ /dev/null @@ -1,129 +0,0 @@ -<template> -<div> - <div class="_section"> - <div class="_content"> - <MkInput v-model="name"> - <template #label>{{ $ts.name }}</template> - </MkInput> - - <MkTextarea v-model="description"> - <template #label>{{ $ts.description }}</template> - </MkTextarea> - - <div class="banner"> - <MkButton v-if="bannerId == null" @click="setBannerImage"><i class="fas fa-plus"></i> {{ $ts._channel.setBanner }}</MkButton> - <div v-else-if="bannerUrl"> - <img :src="bannerUrl" style="width: 100%;"/> - <MkButton @click="removeBannerImage()"><i class="fas fa-trash-alt"></i> {{ $ts._channel.removeBanner }}</MkButton> - </div> - </div> - </div> - <div class="_footer"> - <MkButton @click="save()" primary><i class="fas fa-save"></i> {{ channelId ? $ts.save : $ts.create }}</MkButton> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import { computed, defineComponent } from 'vue'; -import MkTextarea from '@client/components/form/textarea.vue'; -import MkButton from '@client/components/ui/button.vue'; -import MkInput from '@client/components/form/input.vue'; -import { selectFile } from '@client/scripts/select-file'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - MkTextarea, MkButton, MkInput, - }, - - props: { - channelId: { - type: String, - required: false - }, - }, - - data() { - return { - [symbols.PAGE_INFO]: computed(() => this.channelId ? { - title: this.$ts._channel.edit, - icon: 'fas fa-satellite-dish', - } : { - title: this.$ts._channel.create, - icon: 'fas fa-satellite-dish', - }), - channel: null, - name: null, - description: null, - bannerUrl: null, - bannerId: null, - }; - }, - - watch: { - async bannerId() { - if (this.bannerId == null) { - this.bannerUrl = null; - } else { - this.bannerUrl = (await os.api('drive/files/show', { - fileId: this.bannerId, - })).url; - } - }, - }, - - async created() { - if (this.channelId) { - this.channel = await os.api('channels/show', { - channelId: this.channelId, - }); - - this.name = this.channel.name; - this.description = this.channel.description; - this.bannerId = this.channel.bannerId; - this.bannerUrl = this.channel.bannerUrl; - } - }, - - methods: { - save() { - const params = { - name: this.name, - description: this.description, - bannerId: this.bannerId, - }; - - if (this.channelId) { - params.channelId = this.channelId; - os.api('channels/update', params) - .then(channel => { - os.success(); - }); - } else { - os.api('channels/create', params) - .then(channel => { - os.success(); - this.$router.push(`/channels/${channel.id}`); - }); - } - }, - - setBannerImage(e) { - selectFile(e.currentTarget || e.target, null, false).then(file => { - this.bannerId = file.id; - }); - }, - - removeBannerImage() { - this.bannerId = null; - } - } -}); -</script> - -<style lang="scss" scoped> - -</style> diff --git a/src/client/pages/channel.vue b/src/client/pages/channel.vue deleted file mode 100644 index d725db9e49..0000000000 --- a/src/client/pages/channel.vue +++ /dev/null @@ -1,186 +0,0 @@ -<template> -<div v-if="channel" class="_section"> - <div class="wpgynlbz _content _panel _gap" :class="{ hide: !showBanner }"> - <XChannelFollowButton :channel="channel" :full="true" class="subscribe"/> - <button class="_button toggle" @click="() => showBanner = !showBanner"> - <template v-if="showBanner"><i class="fas fa-angle-up"></i></template> - <template v-else><i class="fas fa-angle-down"></i></template> - </button> - <div class="hideOverlay" v-if="!showBanner"> - </div> - <div :style="{ backgroundImage: channel.bannerUrl ? `url(${channel.bannerUrl})` : null }" class="banner"> - <div class="status"> - <div><i class="fas fa-users fa-fw"></i><I18n :src="$ts._channel.usersCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.usersCount }}</b></template></I18n></div> - <div><i class="fas fa-pencil-alt fa-fw"></i><I18n :src="$ts._channel.notesCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.notesCount }}</b></template></I18n></div> - </div> - <div class="fade"></div> - </div> - <div class="description" v-if="channel.description"> - <Mfm :text="channel.description" :is-note="false" :i="$i"/> - </div> - </div> - - <XPostForm :channel="channel" class="post-form _content _panel _gap" fixed v-if="$i"/> - - <XTimeline class="_content _gap" src="channel" :key="channelId" :channel="channelId" @before="before" @after="after"/> -</div> -</template> - -<script lang="ts"> -import { computed, defineComponent } from 'vue'; -import MkContainer from '@client/components/ui/container.vue'; -import XPostForm from '@client/components/post-form.vue'; -import XTimeline from '@client/components/timeline.vue'; -import XChannelFollowButton from '@client/components/channel-follow-button.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - MkContainer, - XPostForm, - XTimeline, - XChannelFollowButton - }, - - props: { - channelId: { - type: String, - required: true - } - }, - - data() { - return { - [symbols.PAGE_INFO]: computed(() => this.channel ? { - title: this.channel.name, - icon: 'fas fa-satellite-dish', - } : null), - channel: null, - showBanner: true, - pagination: { - endpoint: 'channels/timeline', - limit: 10, - params: () => ({ - channelId: this.channelId, - }) - }, - }; - }, - - watch: { - channelId: { - async handler() { - this.channel = await os.api('channels/show', { - channelId: this.channelId, - }); - }, - immediate: true - } - }, - - created() { - - }, -}); -</script> - -<style lang="scss" scoped> -.wpgynlbz { - position: relative; - - > .subscribe { - position: absolute; - z-index: 1; - top: 16px; - left: 16px; - } - - > .toggle { - position: absolute; - z-index: 2; - top: 8px; - right: 8px; - font-size: 1.2em; - width: 48px; - height: 48px; - color: #fff; - background: rgba(0, 0, 0, 0.5); - border-radius: 100%; - - > i { - vertical-align: middle; - } - } - - > .banner { - position: relative; - height: 200px; - background-position: center; - background-size: cover; - - > .fade { - position: absolute; - bottom: 0; - left: 0; - width: 100%; - height: 64px; - background: linear-gradient(0deg, var(--panel), var(--X15)); - } - - > .status { - position: absolute; - z-index: 1; - bottom: 16px; - right: 16px; - padding: 8px 12px; - font-size: 80%; - background: rgba(0, 0, 0, 0.7); - border-radius: 6px; - color: #fff; - } - } - - > .description { - padding: 16px; - } - - > .hideOverlay { - position: absolute; - z-index: 1; - top: 0; - left: 0; - width: 100%; - height: 100%; - -webkit-backdrop-filter: var(--blur, blur(16px)); - backdrop-filter: var(--blur, blur(16px)); - background: rgba(0, 0, 0, 0.3); - } - - &.hide { - > .subscribe { - display: none; - } - - > .toggle { - top: 0; - right: 0; - height: 100%; - background: transparent; - } - - > .banner { - height: 42px; - filter: blur(8px); - - > * { - display: none; - } - } - - > .description { - display: none; - } - } -} -</style> diff --git a/src/client/pages/channels.vue b/src/client/pages/channels.vue deleted file mode 100644 index fd1408c253..0000000000 --- a/src/client/pages/channels.vue +++ /dev/null @@ -1,77 +0,0 @@ -<template> -<div> - <div class="_section" style="padding: 0;" v-if="$i"> - <MkTab class="_content" v-model="tab"> - <option value="featured"><i class="fas fa-fire-alt"></i> {{ $ts._channel.featured }}</option> - <option value="following"><i class="fas fa-heart"></i> {{ $ts._channel.following }}</option> - <option value="owned"><i class="fas fa-edit"></i> {{ $ts._channel.owned }}</option> - </MkTab> - </div> - - <div class="_section"> - <div class="_content grwlizim featured" v-if="tab === 'featured'"> - <MkPagination :pagination="featuredPagination" #default="{items}"> - <MkChannelPreview v-for="channel in items" class="_gap" :channel="channel" :key="channel.id"/> - </MkPagination> - </div> - - <div class="_content grwlizim following" v-if="tab === 'following'"> - <MkPagination :pagination="followingPagination" #default="{items}"> - <MkChannelPreview v-for="channel in items" class="_gap" :channel="channel" :key="channel.id"/> - </MkPagination> - </div> - - <div class="_content grwlizim owned" v-if="tab === 'owned'"> - <MkButton class="new" @click="create()"><i class="fas fa-plus"></i></MkButton> - <MkPagination :pagination="ownedPagination" #default="{items}"> - <MkChannelPreview v-for="channel in items" class="_gap" :channel="channel" :key="channel.id"/> - </MkPagination> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import MkChannelPreview from '@client/components/channel-preview.vue'; -import MkPagination from '@client/components/ui/pagination.vue'; -import MkButton from '@client/components/ui/button.vue'; -import MkTab from '@client/components/tab.vue'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - MkChannelPreview, MkPagination, MkButton, MkTab - }, - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.channel, - icon: 'fas fa-satellite-dish', - action: { - icon: 'fas fa-plus', - handler: this.create - } - }, - tab: 'featured', - featuredPagination: { - endpoint: 'channels/featured', - noPaging: true, - }, - followingPagination: { - endpoint: 'channels/followed', - limit: 5, - }, - ownedPagination: { - endpoint: 'channels/owned', - limit: 5, - }, - }; - }, - methods: { - create() { - this.$router.push(`/channels/new`); - } - } -}); -</script> diff --git a/src/client/pages/clip.vue b/src/client/pages/clip.vue deleted file mode 100644 index e4b00d5e28..0000000000 --- a/src/client/pages/clip.vue +++ /dev/null @@ -1,154 +0,0 @@ -<template> -<div v-if="clip" class="_section"> - <div class="okzinsic _content _panel _gap"> - <div class="description" v-if="clip.description"> - <Mfm :text="clip.description" :is-note="false" :i="$i"/> - </div> - <div class="user"> - <MkAvatar :user="clip.user" class="avatar" :show-indicator="true"/> <MkUserName :user="clip.user" :nowrap="false"/> - </div> - </div> - - <XNotes class="_content _gap" :pagination="pagination" :detail="true"/> -</div> -</template> - -<script lang="ts"> -import { computed, defineComponent } from 'vue'; -import MkContainer from '@client/components/ui/container.vue'; -import XPostForm from '@client/components/post-form.vue'; -import XNotes from '@client/components/notes.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - MkContainer, - XPostForm, - XNotes, - }, - - props: { - clipId: { - type: String, - required: true - } - }, - - data() { - return { - [symbols.PAGE_INFO]: computed(() => this.clip ? { - title: this.clip.name, - icon: 'fas fa-paperclip', - action: { - icon: 'fas fa-ellipsis-h', - handler: this.menu - } - } : null), - clip: null, - pagination: { - endpoint: 'clips/notes', - limit: 10, - params: () => ({ - clipId: this.clipId, - }) - }, - }; - }, - - computed: { - isOwned(): boolean { - return this.$i && this.clip && (this.$i.id === this.clip.userId); - } - }, - - watch: { - clipId: { - async handler() { - this.clip = await os.api('clips/show', { - clipId: this.clipId, - }); - }, - immediate: true - } - }, - - created() { - - }, - - methods: { - menu(ev) { - os.popupMenu([this.isOwned ? { - icon: 'fas fa-pencil-alt', - text: this.$ts.edit, - action: async () => { - const { canceled, result } = await os.form(this.clip.name, { - name: { - type: 'string', - label: this.$ts.name, - default: this.clip.name - }, - description: { - type: 'string', - required: false, - multiline: true, - label: this.$ts.description, - default: this.clip.description - }, - isPublic: { - type: 'boolean', - label: this.$ts.public, - default: this.clip.isPublic - } - }); - if (canceled) return; - - os.apiWithDialog('clips/update', { - clipId: this.clip.id, - ...result - }); - } - } : undefined, this.isOwned ? { - icon: 'fas fa-trash-alt', - text: this.$ts.delete, - danger: true, - action: async () => { - const { canceled } = await os.dialog({ - type: 'warning', - text: this.$t('deleteAreYouSure', { x: this.clip.name }), - showCancelButton: true - }); - if (canceled) return; - - await os.apiWithDialog('clips/delete', { - clipId: this.clip.id, - }); - } - } : undefined], ev.currentTarget || ev.target); - } - } -}); -</script> - -<style lang="scss" scoped> -.okzinsic { - position: relative; - - > .description { - padding: 16px; - } - - > .user { - $height: 32px; - padding: 16px; - border-top: solid 0.5px var(--divider); - line-height: $height; - - > .avatar { - width: $height; - height: $height; - } - } -} -</style> diff --git a/src/client/pages/doc.vue b/src/client/pages/doc.vue deleted file mode 100644 index 500d0340b7..0000000000 --- a/src/client/pages/doc.vue +++ /dev/null @@ -1,240 +0,0 @@ -<template> -<div class="qyqbqfal" v-size="{ max: [500] }"> - <div class="main"> - <div class="title">{{ title }}</div> - <div class="body" v-html="body"></div> - <div class="footer"> - <MkLink :url="`https://github.com/misskey-dev/misskey/blob/master/src/docs/${lang}/${doc}.md`" class="at">{{ $ts.docSource }}</MkLink> - <p v-if="lang !== 'ja-JP'">{{ $ts.translateWarn }}</p> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import { computed, defineComponent } from 'vue'; -import MarkdownIt from 'markdown-it'; -import MarkdownItAnchor from 'markdown-it-anchor'; -import { url, lang } from '@client/config'; -import MkLink from '@client/components/link.vue'; -import * as symbols from '@client/symbols'; - -const markdown = MarkdownIt({ - html: true -}); - -markdown.use(MarkdownItAnchor, { - slugify: (s) => encodeURIComponent(String(s).trim().replace(/\s+/g, '-')) -}); - -export default defineComponent({ - components: { - MkLink - }, - - props: { - doc: { - type: String, - required: true - } - }, - - data() { - return { - [symbols.PAGE_INFO]: computed(() => this.title ? { - title: this.title, - icon: 'fas fa-question-circle', - } : null), - title: null, - body: null, - markdown: null, - lang, - } - }, - - watch: { - doc: { - handler() { - this.fetchDoc(); - }, - immediate: true, - } - }, - - methods: { - fetchDoc() { - fetch(`${url}/doc-assets/${lang}/${this.doc}.md`).then(res => res.text()).then(md => { - this.parse(md); - }).catch(() => { - fetch(`${url}/doc-assets/ja-JP/${this.doc}.md`).then(res => res.text()).then(md => { - this.parse(md); - }); - }); - }, - - parse(md: string) { - // ๅคๆฐ็ฝฎๆ - md = md.replace(/\{_URL_\}/g, url); - - // markdown ใฎๅ
จๅฎนใใใผในใใ - const parsed = markdown.parse(md, {}); - if (parsed.length === 0) return; - - const buf = [...parsed]; - const headingTokens = []; - let headingStart = 0; - - // ใใฃใจใไธใซใใ่ฆๅบใใๆฝๅบใใ - while (buf[0].type !== 'heading_open') { - buf.shift(); - headingStart++; - } - buf.shift(); - while (buf[0].type as string !== 'heading_close') { - const token = buf.shift(); - if (token) { - headingTokens.push(token); - } - } - - // ๆฝๅบใใ่ฆๅบใใ้คใ้จๅใbodyใจใใฆๆฝๅบใใ - const bodyTokens = [...parsed]; - bodyTokens.splice(headingStart, headingTokens.length + 2); - - // ๅใ
ใฌใณใใผใใ - this.title = markdown.renderer.render(headingTokens, {}, {}); - this.body = markdown.renderer.render(bodyTokens, {}, {}); - } - } -}); -</script> - -<style lang="scss" scoped> -.qyqbqfal { - padding: 32px; - background: var(--panel); - line-height: 1.5; - - &.max-width_500px { - padding: 24px; - } - - > .main { - max-width: 800px; - margin: 0 auto; - - > .title { - font-size: 1.5em; - font-weight: bold; - padding: 0 0 0.75em 0; - margin: 0 0 1em 0; - border-bottom: solid 2px var(--divider); - } - - > .body { - > *:first-child { - margin-top: 0; - } - - > *:last-child { - margin-bottom: 0; - } - - ::v-deep(a) { - color: var(--link); - } - - ::v-deep(blockquote) { - display: block; - margin: 8px; - padding: 6px 0 6px 12px; - color: var(--fg); - border-left: solid 3px var(--fg); - opacity: 0.7; - - p { - margin: 0; - } - } - - ::v-deep(h2) { - font-size: 1.25em; - padding: 0 0 0.5em 0; - margin: 1.5em 0 1em 0; - border-bottom: solid 0.5px var(--divider); - } - - ::v-deep(h3) { - margin: 1.25em 0 0.5em 0; - } - - ::v-deep(table) { - width: 100%; - max-width: 100%; - overflow: auto; - } - - ::v-deep(kbd.group) { - display: inline-block; - padding: 2px; - border: 1px solid var(--divider); - border-radius: 4px; - box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); - } - - ::v-deep(kbd.key) { - display: inline-block; - padding: 6px 8px; - border: solid 0.5px var(--divider); - border-radius: 4px; - box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); - } - - ::v-deep(code) { - display: inline-block; - font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace; - tab-size: 2; - background: #272822; - color: #f8f8f2; - border-radius: 6px; - padding: 4px 6px; - } - - ::v-deep(pre) { - background: #272822; - color: #f8f8f2; - border-radius: 6px; - padding: 12px 16px; - - > code { - padding: 0; - } - } - - ::v-deep(.info) { - font-size: 90%; - background: var(--infoBg); - color: var(--infoFg); - padding: 1em; - margin: 0.75em 0; - border-radius: 6px; - } - - ::v-deep(.warn) { - font-size: 90%; - background: var(--infoWarnBg); - color: var(--infoWarnFg); - padding: 1em; - margin: 0.75em 0; - border-radius: 6px; - } - } - - > .footer { - padding: 1.5em 0 0 0; - margin: 1.5em 0 0 0; - border-top: solid 2px var(--divider); - } - } -} -</style> diff --git a/src/client/pages/docs.vue b/src/client/pages/docs.vue deleted file mode 100644 index 629dc2be53..0000000000 --- a/src/client/pages/docs.vue +++ /dev/null @@ -1,152 +0,0 @@ -<template> -<div class="vtaihdtm"> - <div class="body"> - <div class="search"> - <MkInput v-model="query" :debounce="true" type="search" class="" :placeholder="$ts.search"> - <template #prefix><i class="fas fa-search"></i></template> - </MkInput> - </div> - <div class="list"> - <MkFolder> - <template #header>{{ $ts._docs.generalTopics }}</template> - <div class="docs"> - <MkA v-for="doc in docs.filter(doc => doc.path.startsWith('general/'))" :key="doc.path" :to="`/docs/${doc.path}`" class="doc"> - <div class="title">{{ doc.title }}</div> - <div class="summary">{{ doc.summary }}</div> - <div class="read">{{ $ts._docs.continueReading }}</div> - </MkA> - </div> - </MkFolder> - <MkFolder> - <template #header>{{ $ts._docs.features }}</template> - <div class="docs"> - <MkA v-for="doc in docs.filter(doc => doc.path.startsWith('features/'))" :key="doc.path" :to="`/docs/${doc.path}`" class="doc"> - <div class="title">{{ doc.title }}</div> - <div class="summary">{{ doc.summary }}</div> - <div class="read">{{ $ts._docs.continueReading }}</div> - </MkA> - </div> - </MkFolder> - <MkFolder> - <template #header>{{ $ts._docs.advancedTopics }}</template> - <div class="docs"> - <MkA v-for="doc in docs.filter(doc => doc.path.startsWith('advanced/'))" :key="doc.path" :to="`/docs/${doc.path}`" class="doc"> - <div class="title">{{ doc.title }}</div> - <div class="summary">{{ doc.summary }}</div> - <div class="read">{{ $ts._docs.continueReading }}</div> - </MkA> - </div> - </MkFolder> - <MkFolder> - <template #header>{{ $ts._docs.admin }}</template> - <div class="docs"> - <MkA v-for="doc in docs.filter(doc => doc.path.startsWith('admin/'))" :key="doc.path" :to="`/docs/${doc.path}`" class="doc"> - <div class="title">{{ doc.title }}</div> - <div class="summary">{{ doc.summary }}</div> - <div class="read">{{ $ts._docs.continueReading }}</div> - </MkA> - </div> - </MkFolder> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import { url, lang } from '@client/config'; -import * as symbols from '@client/symbols'; -import MkFolder from '@client/components/ui/folder.vue'; -import MkInput from '@client/components/form/input.vue'; - -export default defineComponent({ - components: { - MkFolder, - MkInput, - }, - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.help, - icon: 'fas fa-question-circle' - }, - docs: [], - query: null, - } - }, - - watch: { - query() { - fetch(`${url}/docs.json?lang=${lang}&q=${this.query}`).then(res => res.json()).then(docs => { - this.docs = docs; - }); - } - }, - - created() { - fetch(`${url}/docs.json?lang=ja-JP`).then(res => res.json()).then(jaDocs => { - fetch(`${url}/docs.json?lang=${lang}`).then(res => res.json()).then(docs => { - this.docs = jaDocs.map(doc => { - const exist = docs.find(d => d.path === doc.path); - return exist || doc; - }); - }); - }); - }, -}); -</script> - -<style lang="scss" scoped> -.vtaihdtm { - background: var(--panel); - - > .body { - max-width: 900px; - margin: 0 auto; - - > .search { - padding: 16px; - } - - > .list { - padding: 0 16px; - - .docs { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); - grid-gap: 12px; - margin: 0 0 16px 0; - - > .doc { - display: inline-block; - padding: 16px; - border: solid 1px var(--divider); - border-radius: 6px; - - &:hover { - border: solid 1px var(--accent); - text-decoration: none; - } - - > .title { - font-weight: bold; - } - - > .summary { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - font-size: 0.9em; - } - - > .read { - color: var(--link); - font-size: 0.9em; - } - } - } - } - } -} -</style> diff --git a/src/client/pages/drive.vue b/src/client/pages/drive.vue deleted file mode 100644 index 9ee1ea8859..0000000000 --- a/src/client/pages/drive.vue +++ /dev/null @@ -1,28 +0,0 @@ -<template> -<div> - <XDrive ref="drive" @cd="x => folder = x"/> -</div> -</template> - -<script lang="ts"> -import { computed, defineComponent } from 'vue'; -import XDrive from '@client/components/drive.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - XDrive - }, - - data() { - return { - [symbols.PAGE_INFO]: { - title: computed(() => this.folder ? this.folder.name : this.$ts.drive), - icon: 'fas fa-cloud', - }, - folder: null, - }; - }, -}); -</script> diff --git a/src/client/pages/emojis.category.vue b/src/client/pages/emojis.category.vue deleted file mode 100644 index e725bcb31f..0000000000 --- a/src/client/pages/emojis.category.vue +++ /dev/null @@ -1,135 +0,0 @@ -<template> -<div class="driuhtrh"> - <div class="query"> - <MkInput v-model="q" class="" :placeholder="$ts.search"> - <template #prefix><i class="fas fa-search"></i></template> - </MkInput> - - <!-- ใใใใใใใจ้ช้ญ - <div class="tags"> - <span class="tag _button" v-for="tag in tags" :class="{ active: selectedTags.has(tag) }" @click="toggleTag(tag)">{{ tag }}</span> - </div> - --> - </div> - - <MkFolder class="emojis" v-if="searchEmojis"> - <template #header>{{ $ts.searchResult }}</template> - <div class="zuvgdzyt"> - <XEmoji v-for="emoji in searchEmojis" :key="emoji.name" class="emoji" :emoji="emoji"/> - </div> - </MkFolder> - - <MkFolder class="emojis" v-for="category in customEmojiCategories" :key="category"> - <template #header>{{ category || $ts.other }}</template> - <div class="zuvgdzyt"> - <XEmoji v-for="emoji in customEmojis.filter(e => e.category === category)" :key="emoji.name" class="emoji" :emoji="emoji"/> - </div> - </MkFolder> -</div> -</template> - -<script lang="ts"> -import { defineComponent, computed } from 'vue'; -import MkButton from '@client/components/ui/button.vue'; -import MkInput from '@client/components/form/input.vue'; -import MkSelect from '@client/components/form/select.vue'; -import MkFolder from '@client/components/ui/folder.vue'; -import MkTab from '@client/components/tab.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; -import { emojiCategories, emojiTags } from '@client/instance'; -import XEmoji from './emojis.emoji.vue'; - -export default defineComponent({ - components: { - MkButton, - MkInput, - MkSelect, - MkFolder, - MkTab, - XEmoji, - }, - - data() { - return { - q: '', - customEmojiCategories: emojiCategories, - customEmojis: this.$instance.emojis, - tags: emojiTags, - selectedTags: new Set(), - searchEmojis: null, - } - }, - - watch: { - q() { this.search(); }, - selectedTags: { - handler() { - this.search(); - }, - deep: true - }, - }, - - methods: { - search() { - if ((this.q === '' || this.q == null) && this.selectedTags.size === 0) { - this.searchEmojis = null; - return; - } - - if (this.selectedTags.size === 0) { - this.searchEmojis = this.customEmojis.filter(e => e.name.includes(this.q) || e.aliases.includes(this.q)); - } else { - this.searchEmojis = this.customEmojis.filter(e => (e.name.includes(this.q) || e.aliases.includes(this.q)) && [...this.selectedTags].every(t => e.aliases.includes(t))); - } - }, - - toggleTag(tag) { - if (this.selectedTags.has(tag)) { - this.selectedTags.delete(tag); - } else { - this.selectedTags.add(tag); - } - } - } -}); -</script> - -<style lang="scss" scoped> -.driuhtrh { - background: var(--bg); - - > .query { - background: var(--bg); - padding: 16px; - - > .tags { - > .tag { - display: inline-block; - margin: 8px 8px 0 0; - padding: 4px 8px; - font-size: 0.9em; - background: var(--accentedBg); - border-radius: 5px; - - &.active { - background: var(--accent); - color: var(--fgOnAccent); - } - } - } - } - - > .emojis { - --x-padding: 0 16px; - - .zuvgdzyt { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); - grid-gap: 12px; - margin: 0 var(--margin) var(--margin) var(--margin); - } - } -} -</style> diff --git a/src/client/pages/emojis.emoji.vue b/src/client/pages/emojis.emoji.vue deleted file mode 100644 index ca0ef2dbb7..0000000000 --- a/src/client/pages/emojis.emoji.vue +++ /dev/null @@ -1,94 +0,0 @@ -<template> -<button class="zuvgdzyu _button" @click="menu"> - <img :src="emoji.url" class="img" :alt="emoji.name"/> - <div class="body"> - <div class="name _monospace">{{ emoji.name }}</div> - <div class="info">{{ emoji.aliases.join(' ') }}</div> - </div> -</button> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import * as os from '@client/os'; -import copyToClipboard from '@client/scripts/copy-to-clipboard'; -import VanillaTilt from 'vanilla-tilt'; - -export default defineComponent({ - props: { - emoji: { - type: Object, - required: true, - } - }, - - mounted() { - if (this.$store.animation) { - VanillaTilt.init(this.$el, { - reverse: true, - gyroscope: false, - scale: 1.1, - speed: 500, - }); - } - }, - - methods: { - menu(ev) { - os.popupMenu([{ - type: 'label', - text: ':' + this.emoji.name + ':', - }, { - text: this.$ts.copy, - icon: 'fas fa-copy', - action: () => { - copyToClipboard(`:${this.emoji.name}:`); - os.success(); - } - }], ev.currentTarget || ev.target); - } - } -}); -</script> - -<style lang="scss" scoped> -.zuvgdzyu { - display: flex; - align-items: center; - padding: 12px; - text-align: left; - background: var(--panel); - border-radius: 8px; - transform-style: preserve-3d; - transform: perspective(1000px); - - &:hover { - border-color: var(--accent); - } - - > .img { - width: 42px; - height: 42px; - transform: translateZ(20px); - } - - > .body { - padding: 0 0 0 8px; - white-space: nowrap; - overflow: hidden; - transform: translateZ(10px); - - > .name { - text-overflow: ellipsis; - overflow: hidden; - } - - > .info { - opacity: 0.5; - font-size: 0.9em; - text-overflow: ellipsis; - overflow: hidden; - } - } -} -</style> diff --git a/src/client/pages/emojis.vue b/src/client/pages/emojis.vue deleted file mode 100644 index 8918de2338..0000000000 --- a/src/client/pages/emojis.vue +++ /dev/null @@ -1,36 +0,0 @@ -<template> -<div :class="$style.root"> - <XCategory v-if="tab === 'category'"/> -</div> -</template> - -<script lang="ts"> -import { defineComponent, computed } from 'vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; -import XCategory from './emojis.category.vue'; - -export default defineComponent({ - components: { - XCategory, - }, - - data() { - return { - [symbols.PAGE_INFO]: computed(() => ({ - title: this.$ts.customEmojis, - icon: 'fas fa-laugh', - bg: 'var(--bg)', - })), - tab: 'category', - } - }, -}); -</script> - -<style lang="scss" module> -.root { - max-width: 1000px; - margin: 0 auto; -} -</style> diff --git a/src/client/pages/explore.vue b/src/client/pages/explore.vue deleted file mode 100644 index a77b4e53c3..0000000000 --- a/src/client/pages/explore.vue +++ /dev/null @@ -1,261 +0,0 @@ -<template> -<div> - <MkSpacer :content-max="1200"> - <div class="lznhrdub"> - <div v-if="tab === 'local'"> - <div class="localfedi7 _block _isolated" v-if="meta && stats && tag == null" :style="{ backgroundImage: meta.bannerUrl ? `url(${meta.bannerUrl})` : null }"> - <header><span>{{ $t('explore', { host: meta.name || 'Misskey' }) }}</span></header> - <div><span>{{ $t('exploreUsersCount', { count: num(stats.originalUsersCount) }) }}</span></div> - </div> - - <template v-if="tag == null"> - <MkFolder class="_gap" persist-key="explore-pinned-users"> - <template #header><i class="fas fa-bookmark fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.pinnedUsers }}</template> - <XUserList :pagination="pinnedUsers"/> - </MkFolder> - <MkFolder class="_gap" persist-key="explore-popular-users"> - <template #header><i class="fas fa-chart-line fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.popularUsers }}</template> - <XUserList :pagination="popularUsers"/> - </MkFolder> - <MkFolder class="_gap" persist-key="explore-recently-updated-users"> - <template #header><i class="fas fa-comment-alt fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.recentlyUpdatedUsers }}</template> - <XUserList :pagination="recentlyUpdatedUsers"/> - </MkFolder> - <MkFolder class="_gap" persist-key="explore-recently-registered-users"> - <template #header><i class="fas fa-plus fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.recentlyRegisteredUsers }}</template> - <XUserList :pagination="recentlyRegisteredUsers"/> - </MkFolder> - </template> - </div> - <div v-else-if="tab === 'remote'"> - <div class="localfedi7 _block _isolated" v-if="tag == null" :style="{ backgroundImage: `url(/static-assets/client/fedi.jpg)` }"> - <header><span>{{ $ts.exploreFediverse }}</span></header> - </div> - - <MkFolder :foldable="true" :expanded="false" ref="tags" class="_gap"> - <template #header><i class="fas fa-hashtag fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.popularTags }}</template> - - <div class="vxjfqztj"> - <MkA v-for="tag in tagsLocal" :to="`/explore/tags/${tag.tag}`" :key="'local:' + tag.tag" class="local">{{ tag.tag }}</MkA> - <MkA v-for="tag in tagsRemote" :to="`/explore/tags/${tag.tag}`" :key="'remote:' + tag.tag">{{ tag.tag }}</MkA> - </div> - </MkFolder> - - <MkFolder v-if="tag != null" :key="`${tag}`" class="_gap"> - <template #header><i class="fas fa-hashtag fa-fw" style="margin-right: 0.5em;"></i>{{ tag }}</template> - <XUserList :pagination="tagUsers"/> - </MkFolder> - - <template v-if="tag == null"> - <MkFolder class="_gap"> - <template #header><i class="fas fa-chart-line fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.popularUsers }}</template> - <XUserList :pagination="popularUsersF"/> - </MkFolder> - <MkFolder class="_gap"> - <template #header><i class="fas fa-comment-alt fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.recentlyUpdatedUsers }}</template> - <XUserList :pagination="recentlyUpdatedUsersF"/> - </MkFolder> - <MkFolder class="_gap"> - <template #header><i class="fas fa-rocket fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.recentlyDiscoveredUsers }}</template> - <XUserList :pagination="recentlyRegisteredUsersF"/> - </MkFolder> - </template> - </div> - <div v-else-if="tab === 'search'"> - <div class="_isolated"> - <MkInput v-model="searchQuery" :debounce="true" type="search"> - <template #prefix><i class="fas fa-search"></i></template> - <template #label>{{ $ts.searchUser }}</template> - </MkInput> - <MkRadios v-model="searchOrigin"> - <option value="local">{{ $ts.local }}</option> - <option value="remote">{{ $ts.remote }}</option> - <option value="both">{{ $ts.both }}</option> - </MkRadios> - </div> - - <XUserList v-if="searchQuery" class="_gap" :pagination="searchPagination" ref="search"/> - </div> - </div> - </MkSpacer> -</div> -</template> - -<script lang="ts"> -import { computed, defineComponent } from 'vue'; -import XUserList from '@client/components/user-list.vue'; -import MkFolder from '@client/components/ui/folder.vue'; -import MkInput from '@client/components/form/input.vue'; -import MkRadios from '@client/components/form/radios.vue'; -import number from '@client/filters/number'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - XUserList, - MkFolder, - MkInput, - MkRadios, - }, - - props: { - tag: { - type: String, - required: false - } - }, - - data() { - return { - [symbols.PAGE_INFO]: computed(() => ({ - title: this.$ts.explore, - icon: 'fas fa-hashtag', - bg: 'var(--bg)', - tabs: [{ - active: this.tab === 'local', - title: this.$ts.local, - onClick: () => { this.tab = 'local'; }, - }, { - active: this.tab === 'remote', - title: this.$ts.remote, - onClick: () => { this.tab = 'remote'; }, - }, { - active: this.tab === 'search', - title: this.$ts.search, - onClick: () => { this.tab = 'search'; }, - },] - })), - tab: 'local', - pinnedUsers: { endpoint: 'pinned-users' }, - popularUsers: { endpoint: 'users', limit: 10, noPaging: true, params: { - state: 'alive', - origin: 'local', - sort: '+follower', - } }, - recentlyUpdatedUsers: { endpoint: 'users', limit: 10, noPaging: true, params: { - origin: 'local', - sort: '+updatedAt', - } }, - recentlyRegisteredUsers: { endpoint: 'users', limit: 10, noPaging: true, params: { - origin: 'local', - state: 'alive', - sort: '+createdAt', - } }, - popularUsersF: { endpoint: 'users', limit: 10, noPaging: true, params: { - state: 'alive', - origin: 'remote', - sort: '+follower', - } }, - recentlyUpdatedUsersF: { endpoint: 'users', limit: 10, noPaging: true, params: { - origin: 'combined', - sort: '+updatedAt', - } }, - recentlyRegisteredUsersF: { endpoint: 'users', limit: 10, noPaging: true, params: { - origin: 'combined', - sort: '+createdAt', - } }, - searchPagination: { - endpoint: 'users/search', - limit: 10, - params: computed(() => (this.searchQuery && this.searchQuery !== '') ? { - query: this.searchQuery, - origin: this.searchOrigin, - } : null) - }, - tagsLocal: [], - tagsRemote: [], - stats: null, - searchQuery: null, - searchOrigin: 'combined', - num: number, - }; - }, - - computed: { - meta() { - return this.$instance; - }, - tagUsers(): any { - return { - endpoint: 'hashtags/users', - limit: 30, - params: { - tag: this.tag, - origin: 'combined', - sort: '+follower', - } - }; - }, - }, - - watch: { - tag() { - if (this.$refs.tags) this.$refs.tags.toggleContent(this.tag == null); - }, - }, - - created() { - os.api('hashtags/list', { - sort: '+attachedLocalUsers', - attachedToLocalUserOnly: true, - limit: 30 - }).then(tags => { - this.tagsLocal = tags; - }); - os.api('hashtags/list', { - sort: '+attachedRemoteUsers', - attachedToRemoteUserOnly: true, - limit: 30 - }).then(tags => { - this.tagsRemote = tags; - }); - os.api('stats').then(stats => { - this.stats = stats; - }); - }, -}); -</script> - -<style lang="scss" scoped> -.localfedi7 { - color: #fff; - padding: 16px; - height: 80px; - background-position: 50%; - background-size: cover; - margin-bottom: var(--margin); - - > * { - &:not(:last-child) { - margin-bottom: 8px; - } - - > span { - display: inline-block; - padding: 6px 8px; - background: rgba(0, 0, 0, 0.7); - } - } - - > header { - font-size: 20px; - font-weight: bold; - } - - > div { - font-size: 14px; - opacity: 0.8; - } -} - -.vxjfqztj { - > * { - margin-right: 16px; - - &.local { - font-weight: bold; - } - } -} -</style> diff --git a/src/client/pages/favorites.vue b/src/client/pages/favorites.vue deleted file mode 100644 index f13723c2d1..0000000000 --- a/src/client/pages/favorites.vue +++ /dev/null @@ -1,60 +0,0 @@ -<template> -<div class="jmelgwjh"> - <div class="body"> - <XNotes class="notes" :pagination="pagination" :detail="true" :prop="'note'" @before="before()" @after="after()"/> - </div> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import Progress from '@client/scripts/loading'; -import XNotes from '@client/components/notes.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - XNotes - }, - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.favorites, - icon: 'fas fa-star', - bg: 'var(--bg)', - }, - pagination: { - endpoint: 'i/favorites', - limit: 10, - params: () => ({ - }) - }, - }; - }, - - methods: { - before() { - Progress.start(); - }, - - after() { - Progress.done(); - } - } -}); -</script> - -<style lang="scss" scoped> -.jmelgwjh { - background: var(--bg); - - > .body { - box-sizing: border-box; - max-width: 800px; - margin: 0 auto; - padding: 16px; - } -} -</style> diff --git a/src/client/pages/featured.vue b/src/client/pages/featured.vue deleted file mode 100644 index 50df26bfb1..0000000000 --- a/src/client/pages/featured.vue +++ /dev/null @@ -1,43 +0,0 @@ -<template> -<MkSpacer :content-max="800"> - <XNotes ref="notes" :pagination="pagination" @before="before" @after="after"/> -</MkSpacer> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import Progress from '@client/scripts/loading'; -import XNotes from '@client/components/notes.vue'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - XNotes - }, - - 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, - }, - }; - }, - - methods: { - before() { - Progress.start(); - }, - - after() { - Progress.done(); - } - } -}); -</script> diff --git a/src/client/pages/federation.vue b/src/client/pages/federation.vue deleted file mode 100644 index eae6a05367..0000000000 --- a/src/client/pages/federation.vue +++ /dev/null @@ -1,265 +0,0 @@ -<template> -<div class="taeiyria"> - <div class="query"> - <MkInput v-model="host" :debounce="true" class=""> - <template #prefix><i class="fas fa-search"></i></template> - <template #label>{{ $ts.host }}</template> - </MkInput> - <div class="_inputSplit"> - <MkSelect v-model="state"> - <template #label>{{ $ts.state }}</template> - <option value="all">{{ $ts.all }}</option> - <option value="federating">{{ $ts.federating }}</option> - <option value="subscribing">{{ $ts.subscribing }}</option> - <option value="publishing">{{ $ts.publishing }}</option> - <option value="suspended">{{ $ts.suspended }}</option> - <option value="blocked">{{ $ts.blocked }}</option> - <option value="notResponding">{{ $ts.notResponding }}</option> - </MkSelect> - <MkSelect v-model="sort"> - <template #label>{{ $ts.sort }}</template> - <option value="+pubSub">{{ $ts.pubSub }} ({{ $ts.descendingOrder }})</option> - <option value="-pubSub">{{ $ts.pubSub }} ({{ $ts.ascendingOrder }})</option> - <option value="+notes">{{ $ts.notes }} ({{ $ts.descendingOrder }})</option> - <option value="-notes">{{ $ts.notes }} ({{ $ts.ascendingOrder }})</option> - <option value="+users">{{ $ts.users }} ({{ $ts.descendingOrder }})</option> - <option value="-users">{{ $ts.users }} ({{ $ts.ascendingOrder }})</option> - <option value="+following">{{ $ts.following }} ({{ $ts.descendingOrder }})</option> - <option value="-following">{{ $ts.following }} ({{ $ts.ascendingOrder }})</option> - <option value="+followers">{{ $ts.followers }} ({{ $ts.descendingOrder }})</option> - <option value="-followers">{{ $ts.followers }} ({{ $ts.ascendingOrder }})</option> - <option value="+caughtAt">{{ $ts.registeredAt }} ({{ $ts.descendingOrder }})</option> - <option value="-caughtAt">{{ $ts.registeredAt }} ({{ $ts.ascendingOrder }})</option> - <option value="+lastCommunicatedAt">{{ $ts.lastCommunication }} ({{ $ts.descendingOrder }})</option> - <option value="-lastCommunicatedAt">{{ $ts.lastCommunication }} ({{ $ts.ascendingOrder }})</option> - <option value="+driveUsage">{{ $ts.driveUsage }} ({{ $ts.descendingOrder }})</option> - <option value="-driveUsage">{{ $ts.driveUsage }} ({{ $ts.ascendingOrder }})</option> - <option value="+driveFiles">{{ $ts.driveFilesCount }} ({{ $ts.descendingOrder }})</option> - <option value="-driveFiles">{{ $ts.driveFilesCount }} ({{ $ts.ascendingOrder }})</option> - </MkSelect> - </div> - </div> - - <MkPagination :pagination="pagination" #default="{items}" ref="instances" :key="host + state"> - <div class="dqokceoi"> - <MkA class="instance" v-for="instance in items" :key="instance.id" :to="`/instance-info/${instance.host}`"> - <div class="host"><img :src="instance.faviconUrl">{{ instance.host }}</div> - <div class="table"> - <div class="cell"> - <div class="key">{{ $ts.registeredAt }}</div> - <div class="value"><MkTime :time="instance.caughtAt"/></div> - </div> - <div class="cell"> - <div class="key">{{ $ts.software }}</div> - <div class="value">{{ instance.softwareName || `(${$ts.unknown})` }}</div> - </div> - <div class="cell"> - <div class="key">{{ $ts.version }}</div> - <div class="value">{{ instance.softwareVersion || `(${$ts.unknown})` }}</div> - </div> - <div class="cell"> - <div class="key">{{ $ts.users }}</div> - <div class="value">{{ instance.usersCount }}</div> - </div> - <div class="cell"> - <div class="key">{{ $ts.notes }}</div> - <div class="value">{{ instance.notesCount }}</div> - </div> - <div class="cell"> - <div class="key">{{ $ts.sent }}</div> - <div class="value"><MkTime v-if="instance.latestRequestSentAt" :time="instance.latestRequestSentAt"/><span v-else>N/A</span></div> - </div> - <div class="cell"> - <div class="key">{{ $ts.received }}</div> - <div class="value"><MkTime v-if="instance.latestRequestReceivedAt" :time="instance.latestRequestReceivedAt"/><span v-else>N/A</span></div> - </div> - </div> - <div class="footer"> - <span class="status" :class="getStatus(instance)">{{ getStatus(instance) }}</span> - <span class="pubSub"> - <span class="sub" v-if="instance.followersCount > 0"><i class="fas fa-caret-down icon"></i>Sub</span> - <span class="sub" v-else><i class="fas fa-caret-down icon"></i>-</span> - <span class="pub" v-if="instance.followingCount > 0"><i class="fas fa-caret-up icon"></i>Pub</span> - <span class="pub" v-else><i class="fas fa-caret-up icon"></i>-</span> - </span> - <span class="right"> - <span class="latestStatus">{{ instance.latestStatus || '-' }}</span> - <span class="lastCommunicatedAt"><MkTime :time="instance.lastCommunicatedAt"/></span> - </span> - </div> - </MkA> - </div> - </MkPagination> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import MkButton from '@client/components/ui/button.vue'; -import MkInput from '@client/components/form/input.vue'; -import MkSelect from '@client/components/form/select.vue'; -import MkPagination from '@client/components/ui/pagination.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - MkButton, - MkInput, - MkSelect, - MkPagination, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.federation, - icon: 'fas fa-globe', - bg: 'var(--bg)', - }, - host: '', - state: 'federating', - sort: '+pubSub', - pagination: { - endpoint: 'federation/instances', - limit: 10, - offsetMode: true, - params: () => ({ - sort: this.sort, - host: this.host != '' ? this.host : null, - ...( - this.state === 'federating' ? { federating: true } : - this.state === 'subscribing' ? { subscribing: true } : - this.state === 'publishing' ? { publishing: true } : - this.state === 'suspended' ? { suspended: true } : - this.state === 'blocked' ? { blocked: true } : - this.state === 'notResponding' ? { notResponding: true } : - {}) - }) - }, - } - }, - - watch: { - host() { - this.$refs.instances.reload(); - }, - state() { - this.$refs.instances.reload(); - } - }, - - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - methods: { - getStatus(instance) { - if (instance.isSuspended) return 'suspended'; - if (instance.isNotResponding) return 'error'; - return 'alive'; - }, - } -}); -</script> - -<style lang="scss" scoped> -.taeiyria { - > .query { - background: var(--bg); - padding: 16px; - } -} - -.dqokceoi { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); - grid-gap: 12px; - padding: 16px; - - > .instance { - padding: 16px; - border: solid 1px var(--divider); - border-radius: 6px; - - &:hover { - border: solid 1px var(--accent); - text-decoration: none; - } - - > .host { - font-weight: bold; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - - > img { - width: 18px; - height: 18px; - margin-right: 6px; - vertical-align: middle; - } - } - - > .table { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(60px, 1fr)); - grid-gap: 6px; - margin: 6px 0; - font-size: 70%; - - > .cell { - > .key, > .value { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - > .key { - opacity: 0.7; - } - - > .value { - } - } - } - - > .footer { - display: flex; - align-items: center; - font-size: 0.9em; - - > .status { - &.suspended { - opacity: 0.5; - } - - &.error { - color: var(--error); - } - - &.alive { - color: var(--success); - } - } - - > .pubSub { - margin-left: 8px; - } - - > .right { - margin-left: auto; - - > .latestStatus { - border: solid 1px var(--divider); - border-radius: 4px; - margin: 0 8px; - padding: 0 4px; - } - } - } - } -} -</style> diff --git a/src/client/pages/follow-requests.vue b/src/client/pages/follow-requests.vue deleted file mode 100644 index 6115dda454..0000000000 --- a/src/client/pages/follow-requests.vue +++ /dev/null @@ -1,153 +0,0 @@ -<template> -<div> - <MkPagination :pagination="pagination" class="mk-follow-requests" ref="list"> - <template #empty> - <div class="_fullinfo"> - <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> - <div>{{ $ts.noFollowRequests }}</div> - </div> - </template> - <template #default="{items}"> - <div class="user _panel" v-for="req in items" :key="req.id"> - <MkAvatar class="avatar" :user="req.follower" :show-indicator="true"/> - <div class="body"> - <div class="name"> - <MkA class="name" :to="userPage(req.follower)" v-user-preview="req.follower.id"><MkUserName :user="req.follower"/></MkA> - <p class="acct">@{{ acct(req.follower) }}</p> - </div> - <div class="description" v-if="req.follower.description" :title="req.follower.description"> - <Mfm :text="req.follower.description" :is-note="false" :author="req.follower" :i="$i" :custom-emojis="req.follower.emojis" :plain="true" :nowrap="true"/> - </div> - <div class="actions"> - <button class="_button" @click="accept(req.follower)"><i class="fas fa-check"></i></button> - <button class="_button" @click="reject(req.follower)"><i class="fas fa-times"></i></button> - </div> - </div> - </div> - </template> - </MkPagination> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import MkPagination from '@client/components/ui/pagination.vue'; -import { userPage, acct } from '@client/filters/user'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - MkPagination - }, - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.followRequests, - icon: 'fas fa-user-clock', - }, - pagination: { - endpoint: 'following/requests/list', - limit: 10, - }, - }; - }, - - methods: { - accept(user) { - os.api('following/requests/accept', { userId: user.id }).then(() => { - this.$refs.list.reload(); - }); - }, - reject(user) { - os.api('following/requests/reject', { userId: user.id }).then(() => { - this.$refs.list.reload(); - }); - }, - userPage, - acct - } -}); -</script> - -<style lang="scss" scoped> -.mk-follow-requests { - > .user { - display: flex; - padding: 16px; - - > .avatar { - display: block; - flex-shrink: 0; - margin: 0 12px 0 0; - width: 42px; - height: 42px; - border-radius: 8px; - } - - > .body { - display: flex; - width: calc(100% - 54px); - position: relative; - - > .name { - width: 45%; - - @media (max-width: 500px) { - width: 100%; - } - - > .name, - > .acct { - display: block; - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - margin: 0; - } - - > .name { - font-size: 16px; - line-height: 24px; - } - - > .acct { - font-size: 15px; - line-height: 16px; - opacity: 0.7; - } - } - - > .description { - width: 55%; - line-height: 42px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - opacity: 0.7; - font-size: 14px; - padding-right: 40px; - padding-left: 8px; - box-sizing: border-box; - - @media (max-width: 500px) { - display: none; - } - } - - > .actions { - position: absolute; - top: 0; - bottom: 0; - right: 0; - margin: auto 0; - - > button { - padding: 12px; - } - } - } - } -} -</style> diff --git a/src/client/pages/follow.vue b/src/client/pages/follow.vue deleted file mode 100644 index d5247aff1e..0000000000 --- a/src/client/pages/follow.vue +++ /dev/null @@ -1,65 +0,0 @@ -<template> -<div class="mk-follow-page"> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import * as os from '@client/os'; -import { parseAcct } from '@/misc/acct'; - -export default defineComponent({ - created() { - const acct = new URL(location.href).searchParams.get('acct'); - if (acct == null) return; - - let promise; - - if (acct.startsWith('https://')) { - promise = os.api('ap/show', { - uri: acct - }); - promise.then(res => { - if (res.type == 'User') { - this.follow(res.object); - } else if (res.type === 'Note') { - this.$router.push(`/notes/${res.object.id}`); - } else { - os.dialog({ - type: 'error', - text: 'Not a user' - }).then(() => { - window.close(); - }); - } - }); - } else { - promise = os.api('users/show', parseAcct(acct)); - promise.then(user => { - this.follow(user); - }); - } - - os.promiseDialog(promise, null, null, this.$ts.fetchingAsApObject); - }, - - methods: { - async follow(user) { - const { canceled } = await os.dialog({ - type: 'question', - text: this.$t('followConfirm', { name: user.name || user.username }), - showCancelButton: true - }); - - if (canceled) { - window.close(); - return; - } - - os.apiWithDialog('following/create', { - userId: user.id - }); - } - } -}); -</script> diff --git a/src/client/pages/gallery/edit.vue b/src/client/pages/gallery/edit.vue deleted file mode 100644 index 8e74b068ef..0000000000 --- a/src/client/pages/gallery/edit.vue +++ /dev/null @@ -1,168 +0,0 @@ -<template> -<FormBase> - <FormSuspense :p="init"> - <FormInput v-model="title"> - <span>{{ $ts.title }}</span> - </FormInput> - - <FormTextarea v-model="description" :max="500"> - <span>{{ $ts.description }}</span> - </FormTextarea> - - <FormGroup> - <div v-for="file in files" :key="file.id" class="_debobigegoItem _debobigegoPanel wqugxsfx" :style="{ backgroundImage: file ? `url(${ file.thumbnailUrl })` : null }"> - <div class="name">{{ file.name }}</div> - <button class="remove _button" @click="remove(file)" v-tooltip="$ts.remove"><i class="fas fa-times"></i></button> - </div> - <FormButton @click="selectFile" primary><i class="fas fa-plus"></i> {{ $ts.attachFile }}</FormButton> - </FormGroup> - - <FormSwitch v-model="isSensitive">{{ $ts.markAsSensitive }}</FormSwitch> - - <FormButton v-if="postId" @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton> - <FormButton v-else @click="save" primary><i class="fas fa-save"></i> {{ $ts.publish }}</FormButton> - - <FormButton v-if="postId" @click="del" danger><i class="fas fa-trash-alt"></i> {{ $ts.delete }}</FormButton> - </FormSuspense> -</FormBase> -</template> - -<script lang="ts"> -import { computed, defineComponent } from 'vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import FormInput from '@client/components/debobigego/input.vue'; -import FormTextarea from '@client/components/debobigego/textarea.vue'; -import FormSwitch from '@client/components/debobigego/switch.vue'; -import FormTuple from '@client/components/debobigego/tuple.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import FormSuspense from '@client/components/debobigego/suspense.vue'; -import { selectFile } from '@client/scripts/select-file'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - FormButton, - FormInput, - FormTextarea, - FormSwitch, - FormBase, - FormGroup, - FormSuspense, - }, - - props: { - postId: { - type: String, - required: false, - default: null, - } - }, - - data() { - return { - [symbols.PAGE_INFO]: computed(() => this.postId ? { - title: this.$ts.edit, - icon: 'fas fa-pencil-alt' - } : { - title: this.$ts.postToGallery, - icon: 'fas fa-pencil-alt' - }), - init: null, - files: [], - description: null, - title: null, - isSensitive: false, - } - }, - - watch: { - postId: { - handler() { - this.init = () => this.postId ? os.api('gallery/posts/show', { - postId: this.postId - }).then(post => { - this.files = post.files; - this.title = post.title; - this.description = post.description; - this.isSensitive = post.isSensitive; - }) : Promise.resolve(null); - }, - immediate: true, - } - }, - - methods: { - selectFile(e) { - selectFile(e.currentTarget || e.target, null, true).then(files => { - this.files = this.files.concat(files); - }); - }, - - remove(file) { - this.files = this.files.filter(f => f.id !== file.id); - }, - - async save() { - if (this.postId) { - await os.apiWithDialog('gallery/posts/update', { - postId: this.postId, - title: this.title, - description: this.description, - fileIds: this.files.map(file => file.id), - isSensitive: this.isSensitive, - }); - this.$router.push(`/gallery/${this.postId}`); - } else { - const post = await os.apiWithDialog('gallery/posts/create', { - title: this.title, - description: this.description, - fileIds: this.files.map(file => file.id), - isSensitive: this.isSensitive, - }); - this.$router.push(`/gallery/${post.id}`); - } - }, - - async del() { - const { canceled } = await os.dialog({ - type: 'warning', - text: this.$ts.deleteConfirm, - showCancelButton: true - }); - if (canceled) return; - await os.apiWithDialog('gallery/posts/delete', { - postId: this.postId, - }); - this.$router.push(`/gallery`); - } - } -}); -</script> - -<style lang="scss" scoped> -.wqugxsfx { - height: 200px; - background-size: contain; - background-position: center; - background-repeat: no-repeat; - position: relative; - - > .name { - position: absolute; - top: 8px; - left: 9px; - padding: 8px; - background: var(--panel); - } - - > .remove { - position: absolute; - top: 8px; - right: 9px; - padding: 8px; - background: var(--panel); - } -} -</style> diff --git a/src/client/pages/gallery/index.vue b/src/client/pages/gallery/index.vue deleted file mode 100644 index ffc599513e..0000000000 --- a/src/client/pages/gallery/index.vue +++ /dev/null @@ -1,152 +0,0 @@ -<template> -<div class="xprsixdl _root"> - <MkTab v-model="tab" v-if="$i"> - <option value="explore"><i class="fas fa-icons"></i> {{ $ts.gallery }}</option> - <option value="liked"><i class="fas fa-heart"></i> {{ $ts._gallery.liked }}</option> - <option value="my"><i class="fas fa-edit"></i> {{ $ts._gallery.my }}</option> - </MkTab> - - <div v-if="tab === 'explore'"> - <MkFolder class="_gap"> - <template #header><i class="fas fa-clock"></i>{{ $ts.recentPosts }}</template> - <MkPagination :pagination="recentPostsPagination" #default="{items}" :disable-auto-load="true"> - <div class="vfpdbgtk"> - <MkGalleryPostPreview v-for="post in items" :post="post" :key="post.id" class="post"/> - </div> - </MkPagination> - </MkFolder> - <MkFolder class="_gap"> - <template #header><i class="fas fa-fire-alt"></i>{{ $ts.popularPosts }}</template> - <MkPagination :pagination="popularPostsPagination" #default="{items}" :disable-auto-load="true"> - <div class="vfpdbgtk"> - <MkGalleryPostPreview v-for="post in items" :post="post" :key="post.id" class="post"/> - </div> - </MkPagination> - </MkFolder> - </div> - <div v-else-if="tab === 'liked'"> - <MkPagination :pagination="likedPostsPagination" #default="{items}"> - <div class="vfpdbgtk"> - <MkGalleryPostPreview v-for="like in items" :post="like.post" :key="like.id" class="post"/> - </div> - </MkPagination> - </div> - <div v-else-if="tab === 'my'"> - <MkA to="/gallery/new" class="_link" style="margin: 16px;"><i class="fas fa-plus"></i> {{ $ts.postToGallery }}</MkA> - <MkPagination :pagination="myPostsPagination" #default="{items}"> - <div class="vfpdbgtk"> - <MkGalleryPostPreview v-for="post in items" :post="post" :key="post.id" class="post"/> - </div> - </MkPagination> - </div> -</div> -</template> - -<script lang="ts"> -import { computed, defineComponent } from 'vue'; -import XUserList from '@client/components/user-list.vue'; -import MkFolder from '@client/components/ui/folder.vue'; -import MkInput from '@client/components/form/input.vue'; -import MkButton from '@client/components/ui/button.vue'; -import MkTab from '@client/components/tab.vue'; -import MkPagination from '@client/components/ui/pagination.vue'; -import MkGalleryPostPreview from '@client/components/gallery-post-preview.vue'; -import number from '@client/filters/number'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - XUserList, - MkFolder, - MkInput, - MkButton, - MkTab, - MkPagination, - MkGalleryPostPreview, - }, - - props: { - tag: { - type: String, - required: false - } - }, - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.gallery, - icon: 'fas fa-icons' - }, - tab: 'explore', - recentPostsPagination: { - endpoint: 'gallery/posts', - limit: 6, - }, - popularPostsPagination: { - endpoint: 'gallery/featured', - limit: 5, - }, - myPostsPagination: { - endpoint: 'i/gallery/posts', - limit: 5, - }, - likedPostsPagination: { - endpoint: 'i/gallery/likes', - limit: 5, - }, - tags: [], - }; - }, - - computed: { - meta() { - return this.$instance; - }, - tagUsers(): any { - return { - endpoint: 'hashtags/users', - limit: 30, - params: { - tag: this.tag, - origin: 'combined', - sort: '+follower', - } - }; - }, - }, - - watch: { - tag() { - if (this.$refs.tags) this.$refs.tags.toggleContent(this.tag == null); - }, - }, - - created() { - - }, - - methods: { - - } -}); -</script> - -<style lang="scss" scoped> -.xprsixdl { - max-width: 1400px; - margin: 0 auto; -} - -.vfpdbgtk { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); - grid-gap: 12px; - margin: 0 var(--margin); - - > .post { - - } -} -</style> diff --git a/src/client/pages/gallery/post.vue b/src/client/pages/gallery/post.vue deleted file mode 100644 index dbac003e38..0000000000 --- a/src/client/pages/gallery/post.vue +++ /dev/null @@ -1,282 +0,0 @@ -<template> -<div class="_root"> - <transition name="fade" mode="out-in"> - <div v-if="post" class="rkxwuolj"> - <div class="files"> - <div class="file" v-for="file in post.files" :key="file.id"> - <img :src="file.url"/> - </div> - </div> - <div class="body _block"> - <div class="title">{{ post.title }}</div> - <div class="description"><Mfm :text="post.description"/></div> - <div class="info"> - <i class="fas fa-clock"></i> <MkTime :time="post.createdAt" mode="detail"/> - </div> - <div class="actions"> - <div class="like"> - <MkButton class="button" @click="unlike()" v-if="post.isLiked" v-tooltip="$ts._gallery.unlike" primary><i class="fas fa-heart"></i><span class="count" v-if="post.likedCount > 0">{{ post.likedCount }}</span></MkButton> - <MkButton class="button" @click="like()" v-else v-tooltip="$ts._gallery.like"><i class="far fa-heart"></i><span class="count" v-if="post.likedCount > 0">{{ post.likedCount }}</span></MkButton> - </div> - <div class="other"> - <button v-if="$i && $i.id === post.user.id" class="_button" @click="edit" v-tooltip="$ts.edit" v-click-anime><i class="fas fa-pencil-alt fa-fw"></i></button> - <button class="_button" @click="shareWithNote" v-tooltip="$ts.shareWithNote" v-click-anime><i class="fas fa-retweet fa-fw"></i></button> - <button class="_button" @click="share" v-tooltip="$ts.share" v-click-anime><i class="fas fa-share-alt fa-fw"></i></button> - </div> - </div> - <div class="user"> - <MkAvatar :user="post.user" class="avatar"/> - <div class="name"> - <MkUserName :user="post.user" style="display: block;"/> - <MkAcct :user="post.user"/> - </div> - <MkFollowButton v-if="!$i || $i.id != post.user.id" :user="post.user" :inline="true" :transparent="false" :full="true" large class="koudoku"/> - </div> - </div> - <MkAd :prefer="['horizontal', 'horizontal-big']"/> - <MkContainer :max-height="300" :foldable="true" class="other"> - <template #header><i class="fas fa-clock"></i> {{ $ts.recentPosts }}</template> - <MkPagination :pagination="otherPostsPagination" #default="{items}"> - <div class="sdrarzaf"> - <MkGalleryPostPreview v-for="post in items" :post="post" :key="post.id" class="post"/> - </div> - </MkPagination> - </MkContainer> - </div> - <MkError v-else-if="error" @retry="fetch()"/> - <MkLoading v-else/> - </transition> -</div> -</template> - -<script lang="ts"> -import { computed, defineComponent } from 'vue'; -import MkButton from '@client/components/ui/button.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; -import MkContainer from '@client/components/ui/container.vue'; -import ImgWithBlurhash from '@client/components/img-with-blurhash.vue'; -import MkPagination from '@client/components/ui/pagination.vue'; -import MkGalleryPostPreview from '@client/components/gallery-post-preview.vue'; -import MkFollowButton from '@client/components/follow-button.vue'; -import { url } from '@client/config'; - -export default defineComponent({ - components: { - MkContainer, - ImgWithBlurhash, - MkPagination, - MkGalleryPostPreview, - MkButton, - MkFollowButton, - }, - props: { - postId: { - type: String, - required: true - } - }, - data() { - return { - [symbols.PAGE_INFO]: computed(() => this.post ? { - title: this.post.title, - avatar: this.post.user, - path: `/gallery/${this.post.id}`, - share: { - title: this.post.title, - text: this.post.description, - }, - actions: [{ - icon: 'fas fa-pencil-alt', - text: this.$ts.edit, - handler: this.edit - }] - } : null), - otherPostsPagination: { - endpoint: 'users/gallery/posts', - limit: 6, - params: () => ({ - userId: this.post.user.id - }) - }, - post: null, - error: null, - }; - }, - - watch: { - postId: 'fetch' - }, - - created() { - this.fetch(); - }, - - methods: { - fetch() { - this.post = null; - os.api('gallery/posts/show', { - postId: this.postId - }).then(post => { - this.post = post; - }).catch(e => { - this.error = e; - }); - }, - - share() { - navigator.share({ - title: this.post.title, - text: this.post.description, - url: `${url}/gallery/${this.post.id}` - }); - }, - - shareWithNote() { - os.post({ - initialText: `${this.post.title} ${url}/gallery/${this.post.id}` - }); - }, - - like() { - os.apiWithDialog('gallery/posts/like', { - postId: this.postId, - }).then(() => { - this.post.isLiked = true; - this.post.likedCount++; - }); - }, - - async unlike() { - const confirm = await os.dialog({ - type: 'warning', - showCancelButton: true, - text: this.$ts.unlikeConfirm, - }); - if (confirm.canceled) return; - os.apiWithDialog('gallery/posts/unlike', { - postId: this.postId, - }).then(() => { - this.post.isLiked = false; - this.post.likedCount--; - }); - }, - - edit() { - this.$router.push(`/gallery/${this.post.id}/edit`); - } - } -}); -</script> - -<style lang="scss" scoped> -.fade-enter-active, -.fade-leave-active { - transition: opacity 0.125s ease; -} -.fade-enter-from, -.fade-leave-to { - opacity: 0; -} - -.rkxwuolj { - > .files { - > .file { - > img { - display: block; - max-width: 100%; - max-height: 500px; - margin: 0 auto; - } - - & + .file { - margin-top: 16px; - } - } - } - - > .body { - padding: 32px; - - > .title { - font-weight: bold; - font-size: 1.2em; - margin-bottom: 16px; - } - - > .info { - margin-top: 16px; - font-size: 90%; - opacity: 0.7; - } - - > .actions { - display: flex; - align-items: center; - margin-top: 16px; - padding: 16px 0 0 0; - border-top: solid 0.5px var(--divider); - - > .like { - > .button { - --accent: rgb(241 97 132); - --X8: rgb(241 92 128); - --buttonBg: rgb(216 71 106 / 5%); - --buttonHoverBg: rgb(216 71 106 / 10%); - color: #ff002f; - - ::v-deep(.count) { - margin-left: 0.5em; - } - } - } - - > .other { - margin-left: auto; - - > button { - padding: 8px; - margin: 0 8px; - - &:hover { - color: var(--fgHighlighted); - } - } - } - } - - > .user { - margin-top: 16px; - padding: 16px 0 0 0; - border-top: solid 0.5px var(--divider); - display: flex; - align-items: center; - - > .avatar { - width: 52px; - height: 52px; - } - - > .name { - margin: 0 0 0 12px; - font-size: 90%; - } - - > .koudoku { - margin-left: auto; - } - } - } -} - -.sdrarzaf { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); - grid-gap: 12px; - margin: var(--margin); - - > .post { - - } -} -</style> diff --git a/src/client/pages/instance-info.vue b/src/client/pages/instance-info.vue deleted file mode 100644 index 291ceb5dfd..0000000000 --- a/src/client/pages/instance-info.vue +++ /dev/null @@ -1,238 +0,0 @@ -<template> -<FormBase> - <FormGroup v-if="instance"> - <template #label>{{ instance.host }}</template> - <FormGroup> - <div class="_debobigegoItem"> - <div class="_debobigegoPanel fnfelxur"> - <img :src="instance.iconUrl || instance.faviconUrl" alt="" class="icon"/> - </div> - </div> - <FormKeyValueView> - <template #key>Name</template> - <template #value><span class="_monospace">{{ instance.name || `(${$ts.unknown})` }}</span></template> - </FormKeyValueView> - </FormGroup> - - <FormButton v-if="$i.isAdmin || $i.isModerator" @click="info" primary>{{ $ts.settings }}</FormButton> - - <FormTextarea readonly :value="instance.description"> - <span>{{ $ts.description }}</span> - </FormTextarea> - - <FormGroup> - <FormKeyValueView> - <template #key>{{ $ts.software }}</template> - <template #value><span class="_monospace">{{ instance.softwareName || `(${$ts.unknown})` }}</span></template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>{{ $ts.version }}</template> - <template #value><span class="_monospace">{{ instance.softwareVersion || `(${$ts.unknown})` }}</span></template> - </FormKeyValueView> - </FormGroup> - <FormGroup> - <FormKeyValueView> - <template #key>{{ $ts.administrator }}</template> - <template #value><span class="_monospace">{{ instance.maintainerName || `(${$ts.unknown})` }}</span></template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>{{ $ts.contact }}</template> - <template #value><span class="_monospace">{{ instance.maintainerEmail || `(${$ts.unknown})` }}</span></template> - </FormKeyValueView> - </FormGroup> - <FormGroup> - <FormKeyValueView> - <template #key>{{ $ts.latestRequestSentAt }}</template> - <template #value><MkTime v-if="instance.latestRequestSentAt" :time="instance.latestRequestSentAt"/><span v-else>N/A</span></template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>{{ $ts.latestStatus }}</template> - <template #value>{{ instance.latestStatus ? instance.latestStatus : 'N/A' }}</template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>{{ $ts.latestRequestReceivedAt }}</template> - <template #value><MkTime v-if="instance.latestRequestReceivedAt" :time="instance.latestRequestReceivedAt"/><span v-else>N/A</span></template> - </FormKeyValueView> - </FormGroup> - <FormGroup> - <FormKeyValueView> - <template #key>Open Registrations</template> - <template #value>{{ instance.openRegistrations ? $ts.yes : $ts.no }}</template> - </FormKeyValueView> - </FormGroup> - <div class="_debobigegoItem"> - <div class="_debobigegoLabel">{{ $ts.statistics }}</div> - <div class="_debobigegoPanel cmhjzshl"> - <div class="selects"> - <MkSelect v-model="chartSrc" style="margin: 0; flex: 1;"> - <option value="instance-requests">{{ $ts._instanceCharts.requests }}</option> - <option value="instance-users">{{ $ts._instanceCharts.users }}</option> - <option value="instance-users-total">{{ $ts._instanceCharts.usersTotal }}</option> - <option value="instance-notes">{{ $ts._instanceCharts.notes }}</option> - <option value="instance-notes-total">{{ $ts._instanceCharts.notesTotal }}</option> - <option value="instance-ff">{{ $ts._instanceCharts.ff }}</option> - <option value="instance-ff-total">{{ $ts._instanceCharts.ffTotal }}</option> - <option value="instance-drive-usage">{{ $ts._instanceCharts.cacheSize }}</option> - <option value="instance-drive-usage-total">{{ $ts._instanceCharts.cacheSizeTotal }}</option> - <option value="instance-drive-files">{{ $ts._instanceCharts.files }}</option> - <option value="instance-drive-files-total">{{ $ts._instanceCharts.filesTotal }}</option> - </MkSelect> - <MkSelect v-model="chartSpan" style="margin: 0;"> - <option value="hour">{{ $ts.perHour }}</option> - <option value="day">{{ $ts.perDay }}</option> - </MkSelect> - </div> - <div class="chart"> - <MkChart :src="chartSrc" :span="chartSpan" :limit="90" :detailed="true"></MkChart> - </div> - </div> - </div> - <FormGroup> - <FormKeyValueView> - <template #key>{{ $ts.registeredAt }}</template> - <template #value><MkTime mode="detail" :time="instance.caughtAt"/></template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>{{ $ts.updatedAt }}</template> - <template #value><MkTime mode="detail" :time="instance.infoUpdatedAt"/></template> - </FormKeyValueView> - </FormGroup> - <FormObjectView tall :value="instance"> - <span>Raw</span> - </FormObjectView> - <FormGroup> - <template #label>Well-known resources</template> - <FormLink :to="`https://${host}/.well-known/host-meta`" external>host-meta</FormLink> - <FormLink :to="`https://${host}/.well-known/host-meta.json`" external>host-meta.json</FormLink> - <FormLink :to="`https://${host}/.well-known/nodeinfo`" external>nodeinfo</FormLink> - <FormLink :to="`https://${host}/robots.txt`" external>robots.txt</FormLink> - <FormLink :to="`https://${host}/manifest.json`" external>manifest.json</FormLink> - </FormGroup> - <FormSuspense :p="dnsPromiseFactory" v-slot="{ result: dns }"> - <FormGroup> - <template #label>DNS</template> - <FormKeyValueView v-for="record in dns.a" :key="record"> - <template #key>A</template> - <template #value><span class="_monospace">{{ record }}</span></template> - </FormKeyValueView> - <FormKeyValueView v-for="record in dns.aaaa" :key="record"> - <template #key>AAAA</template> - <template #value><span class="_monospace">{{ record }}</span></template> - </FormKeyValueView> - <FormKeyValueView v-for="record in dns.cname" :key="record"> - <template #key>CNAME</template> - <template #value><span class="_monospace">{{ record }}</span></template> - </FormKeyValueView> - <FormKeyValueView v-for="record in dns.txt"> - <template #key>TXT</template> - <template #value><span class="_monospace">{{ record[0] }}</span></template> - </FormKeyValueView> - </FormGroup> - </FormSuspense> - </FormGroup> -</FormBase> -</template> - -<script lang="ts"> -import { defineAsyncComponent, defineComponent } from 'vue'; -import MkChart from '@client/components/chart.vue'; -import FormObjectView from '@client/components/debobigego/object-view.vue'; -import FormTextarea from '@client/components/debobigego/textarea.vue'; -import FormLink from '@client/components/debobigego/link.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import FormKeyValueView from '@client/components/debobigego/key-value-view.vue'; -import FormSuspense from '@client/components/debobigego/suspense.vue'; -import MkSelect from '@client/components/form/select.vue'; -import * as os from '@client/os'; -import number from '@client/filters/number'; -import bytes from '@client/filters/bytes'; -import * as symbols from '@client/symbols'; -import MkInstanceInfo from '@client/pages/admin/instance.vue'; - -export default defineComponent({ - components: { - FormBase, - FormTextarea, - FormObjectView, - FormButton, - FormLink, - FormGroup, - FormKeyValueView, - FormSuspense, - MkSelect, - MkChart, - }, - - props: { - host: { - type: String, - required: true - } - }, - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.instanceInfo, - icon: 'fas fa-info-circle', - actions: [{ - text: `https://${this.host}`, - icon: 'fas fa-external-link-alt', - handler: () => { - window.open(`https://${this.host}`, '_blank'); - } - }], - }, - instance: null, - dnsPromiseFactory: () => os.api('federation/dns', { - host: this.host - }), - chartSrc: 'instance-requests', - chartSpan: 'hour', - } - }, - - mounted() { - this.fetch(); - }, - - methods: { - number, - bytes, - - async fetch() { - this.instance = await os.api('federation/show-instance', { - host: this.host - }); - }, - - info() { - os.popup(MkInstanceInfo, { - instance: this.instance - }, {}, 'closed'); - } - } -}); -</script> - -<style lang="scss" scoped> -.fnfelxur { - padding: 16px; - - > .icon { - display: block; - margin: auto; - height: 64px; - border-radius: 8px; - } -} - -.cmhjzshl { - > .selects { - display: flex; - padding: 16px; - } -} -</style> diff --git a/src/client/pages/mentions.vue b/src/client/pages/mentions.vue deleted file mode 100644 index 04682a856a..0000000000 --- a/src/client/pages/mentions.vue +++ /dev/null @@ -1,42 +0,0 @@ -<template> -<MkSpacer :content-max="800"> - <XNotes :pagination="pagination" @before="before()" @after="after()"/> -</MkSpacer> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import Progress from '@client/scripts/loading'; -import XNotes from '@client/components/notes.vue'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - XNotes - }, - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.mentions, - icon: 'fas fa-at', - bg: 'var(--bg)', - }, - pagination: { - endpoint: 'notes/mentions', - limit: 10, - }, - }; - }, - - methods: { - before() { - Progress.start(); - }, - - after() { - Progress.done(); - } - } -}); -</script> diff --git a/src/client/pages/messages.vue b/src/client/pages/messages.vue deleted file mode 100644 index e3d668cf45..0000000000 --- a/src/client/pages/messages.vue +++ /dev/null @@ -1,45 +0,0 @@ -<template> -<MkSpacer :content-max="800"> - <XNotes :pagination="pagination" @before="before()" @after="after()"/> -</MkSpacer> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import Progress from '@client/scripts/loading'; -import XNotes from '@client/components/notes.vue'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - XNotes - }, - - 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' - }) - }, - }; - }, - - methods: { - before() { - Progress.start(); - }, - - after() { - Progress.done(); - } - } -}); -</script> diff --git a/src/client/pages/messaging/index.vue b/src/client/pages/messaging/index.vue deleted file mode 100644 index 5b4fd51e55..0000000000 --- a/src/client/pages/messaging/index.vue +++ /dev/null @@ -1,307 +0,0 @@ -<template> -<MkSpacer :content-max="800"> - <div class="yweeujhr" v-size="{ max: [400] }"> - <MkButton @click="start" primary class="start"><i class="fas fa-plus"></i> {{ $ts.startMessaging }}</MkButton> - - <div class="history" v-if="messages.length > 0"> - <MkA v-for="(message, i) in messages" - class="message _block" - :class="{ isMe: isMe(message), isRead: message.groupId ? message.reads.includes($i.id) : message.isRead }" - :to="message.groupId ? `/my/messaging/group/${message.groupId}` : `/my/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`" - :data-index="i" - :key="message.id" - v-anim="i" - > - <div> - <MkAvatar class="avatar" :user="message.groupId ? message.user : isMe(message) ? message.recipient : message.user" :show-indicator="true"/> - <header v-if="message.groupId"> - <span class="name">{{ message.group.name }}</span> - <MkTime :time="message.createdAt" class="time"/> - </header> - <header v-else> - <span class="name"><MkUserName :user="isMe(message) ? message.recipient : message.user"/></span> - <span class="username">@{{ acct(isMe(message) ? message.recipient : message.user) }}</span> - <MkTime :time="message.createdAt" class="time"/> - </header> - <div class="body"> - <p class="text"><span class="me" v-if="isMe(message)">{{ $ts.you }}:</span>{{ message.text }}</p> - </div> - </div> - </MkA> - </div> - <div class="_fullinfo" v-if="!fetching && messages.length == 0"> - <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> - <div>{{ $ts.noHistory }}</div> - </div> - <MkLoading v-if="fetching"/> - </div> -</MkSpacer> -</template> - -<script lang="ts"> -import { defineAsyncComponent, defineComponent, markRaw } from 'vue'; -import { getAcct } from '@/misc/acct'; -import MkButton from '@client/components/ui/button.vue'; -import { acct } from '@client/filters/user'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - MkButton - }, - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.messaging, - icon: 'fas fa-comments', - bg: 'var(--bg)', - }, - fetching: true, - moreFetching: false, - messages: [], - connection: null, - }; - }, - - mounted() { - this.connection = markRaw(os.stream.useChannel('messagingIndex')); - - this.connection.on('message', this.onMessage); - this.connection.on('read', this.onRead); - - os.api('messaging/history', { group: false }).then(userMessages => { - os.api('messaging/history', { group: true }).then(groupMessages => { - const messages = userMessages.concat(groupMessages); - messages.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); - this.messages = messages; - this.fetching = false; - }); - }); - }, - - beforeUnmount() { - this.connection.dispose(); - }, - - methods: { - getAcct, - - isMe(message) { - return message.userId == this.$i.id; - }, - - onMessage(message) { - if (message.recipientId) { - this.messages = this.messages.filter(m => !( - (m.recipientId == message.recipientId && m.userId == message.userId) || - (m.recipientId == message.userId && m.userId == message.recipientId))); - - this.messages.unshift(message); - } else if (message.groupId) { - this.messages = this.messages.filter(m => m.groupId !== message.groupId); - this.messages.unshift(message); - } - }, - - onRead(ids) { - for (const id of ids) { - const found = this.messages.find(m => m.id == id); - if (found) { - if (found.recipientId) { - found.isRead = true; - } else if (found.groupId) { - found.reads.push(this.$i.id); - } - } - } - }, - - start(ev) { - os.popupMenu([{ - text: this.$ts.messagingWithUser, - icon: 'fas fa-user', - action: () => { this.startUser() } - }, { - text: this.$ts.messagingWithGroup, - icon: 'fas fa-users', - action: () => { this.startGroup() } - }], ev.currentTarget || ev.target); - }, - - async startUser() { - os.selectUser().then(user => { - this.$router.push(`/my/messaging/${getAcct(user)}`); - }); - }, - - async startGroup() { - const groups1 = await os.api('users/groups/owned'); - const groups2 = await os.api('users/groups/joined'); - if (groups1.length === 0 && groups2.length === 0) { - os.dialog({ - type: 'warning', - title: this.$ts.youHaveNoGroups, - text: this.$ts.joinOrCreateGroup, - }); - return; - } - const { canceled, result: group } = await os.dialog({ - type: null, - title: this.$ts.group, - select: { - items: groups1.concat(groups2).map(group => ({ - value: group, text: group.name - })) - }, - showCancelButton: true - }); - if (canceled) return; - this.$router.push(`/my/messaging/group/${group.id}`); - }, - - acct - } -}); -</script> - -<style lang="scss" scoped> -.yweeujhr { - - > .start { - margin: 0 auto var(--margin) auto; - } - - > .history { - > .message { - display: block; - text-decoration: none; - margin-bottom: var(--margin); - - * { - pointer-events: none; - user-select: none; - } - - &:hover { - .avatar { - filter: saturate(200%); - } - } - - &:active { - } - - &.isRead, - &.isMe { - opacity: 0.8; - } - - &:not(.isMe):not(.isRead) { - > div { - background-image: url("/static-assets/client/unread.svg"); - background-repeat: no-repeat; - background-position: 0 center; - } - } - - &:after { - content: ""; - display: block; - clear: both; - } - - > div { - padding: 20px 30px; - - &:after { - content: ""; - display: block; - clear: both; - } - - > header { - display: flex; - align-items: center; - margin-bottom: 2px; - white-space: nowrap; - overflow: hidden; - - > .name { - margin: 0; - padding: 0; - overflow: hidden; - text-overflow: ellipsis; - font-size: 1em; - font-weight: bold; - transition: all 0.1s ease; - } - - > .username { - margin: 0 8px; - } - - > .time { - margin: 0 0 0 auto; - } - } - - > .avatar { - float: left; - width: 54px; - height: 54px; - margin: 0 16px 0 0; - border-radius: 8px; - transition: all 0.1s ease; - } - - > .body { - - > .text { - display: block; - margin: 0 0 0 0; - padding: 0; - overflow: hidden; - overflow-wrap: break-word; - font-size: 1.1em; - color: var(--faceText); - - .me { - opacity: 0.7; - } - } - - > .image { - display: block; - max-width: 100%; - max-height: 512px; - } - } - } - } - } - - &.max-width_400px { - > .history { - > .message { - &:not(.isMe):not(.isRead) { - > div { - background-image: none; - border-left: solid 4px #3aa2dc; - } - } - - > div { - padding: 16px; - font-size: 0.9em; - - > .avatar { - margin: 0 12px 0 0; - } - } - } - } - } -} -</style> diff --git a/src/client/pages/messaging/messaging-room.form.vue b/src/client/pages/messaging/messaging-room.form.vue deleted file mode 100644 index 31c42e4ab3..0000000000 --- a/src/client/pages/messaging/messaging-room.form.vue +++ /dev/null @@ -1,348 +0,0 @@ -<template> -<div class="pemppnzi _block" - @dragover.stop="onDragover" - @drop.stop="onDrop" -> - <textarea - v-model="text" - ref="text" - @keypress="onKeypress" - @compositionupdate="onCompositionUpdate" - @paste="onPaste" - :placeholder="$ts.inputMessageHere" - ></textarea> - <div class="file" @click="file = null" v-if="file">{{ file.name }}</div> - <button class="send _button" @click="send" :disabled="!canSend || sending" :title="$ts.send"> - <template v-if="!sending"><i class="fas fa-paper-plane"></i></template><template v-if="sending"><i class="fas fa-spinner fa-pulse fa-fw"></i></template> - </button> - <button class="_button" @click="chooseFile"><i class="fas fa-photo-video"></i></button> - <button class="_button" @click="insertEmoji"><i class="fas fa-laugh-squint"></i></button> - <input ref="file" type="file" @change="onChangeFile"/> -</div> -</template> - -<script lang="ts"> -import { defineComponent, defineAsyncComponent } from 'vue'; -import insertTextAtCursor from 'insert-text-at-cursor'; -import * as autosize from 'autosize'; -import { formatTimeString } from '@/misc/format-time-string'; -import { selectFile } from '@client/scripts/select-file'; -import * as os from '@client/os'; -import { Autocomplete } from '@client/scripts/autocomplete'; -import { throttle } from 'throttle-debounce'; - -export default defineComponent({ - props: { - user: { - type: Object, - requird: false, - }, - group: { - type: Object, - requird: false, - }, - }, - data() { - return { - text: null, - file: null, - sending: false, - typing: throttle(3000, () => { - os.stream.send('typingOnMessaging', this.user ? { partner: this.user.id } : { group: this.group.id }); - }), - }; - }, - computed: { - draftKey(): string { - return this.user ? 'user:' + this.user.id : 'group:' + this.group.id; - }, - canSend(): boolean { - return (this.text != null && this.text != '') || this.file != null; - }, - room(): any { - return this.$parent; - } - }, - watch: { - text() { - this.saveDraft(); - }, - file() { - this.saveDraft(); - } - }, - mounted() { - autosize(this.$refs.text); - - // TODO: detach when unmount - new Autocomplete(this.$refs.text, this, { model: 'text' }); - - // ๆธใใใใฎๆ็จฟใๅพฉๅ
- const draft = JSON.parse(localStorage.getItem('message_drafts') || '{}')[this.draftKey]; - if (draft) { - this.text = draft.data.text; - this.file = draft.data.file; - } - }, - methods: { - async onPaste(e: ClipboardEvent) { - const data = e.clipboardData; - const items = data.items; - - if (items.length == 1) { - if (items[0].kind == 'file') { - const file = items[0].getAsFile(); - const lio = file.name.lastIndexOf('.'); - const ext = lio >= 0 ? file.name.slice(lio) : ''; - const formatted = `${formatTimeString(new Date(file.lastModified), this.$store.state.pastedFileName).replace(/{{number}}/g, '1')}${ext}`; - const name = this.$store.state.pasteDialog - ? await os.dialog({ - title: this.$ts.enterFileName, - input: { - default: formatted - }, - allowEmpty: false - }).then(({ canceled, result }) => canceled ? false : result) - : formatted; - if (name) this.upload(file, name); - } - } else { - if (items[0].kind == 'file') { - os.dialog({ - type: 'error', - text: this.$ts.onlyOneFileCanBeAttached - }); - } - } - }, - - onDragover(e) { - const isFile = e.dataTransfer.items[0].kind == 'file'; - const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_; - if (isFile || isDriveFile) { - e.preventDefault(); - e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move'; - } - }, - - onDrop(e): void { - // ใใกใคใซใ ใฃใใ - if (e.dataTransfer.files.length == 1) { - e.preventDefault(); - this.upload(e.dataTransfer.files[0]); - return; - } else if (e.dataTransfer.files.length > 1) { - e.preventDefault(); - os.dialog({ - type: 'error', - text: this.$ts.onlyOneFileCanBeAttached - }); - return; - } - - //#region ใใฉใคใใฎใใกใคใซ - const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_); - if (driveFile != null && driveFile != '') { - this.file = JSON.parse(driveFile); - e.preventDefault(); - } - //#endregion - }, - - onKeypress(e) { - this.typing(); - if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey) && this.canSend) { - this.send(); - } - }, - - onCompositionUpdate() { - this.typing(); - }, - - chooseFile(e) { - selectFile(e.currentTarget || e.target, this.$ts.selectFile, false).then(file => { - this.file = file; - }); - }, - - onChangeFile() { - this.upload((this.$refs.file as any).files[0]); - }, - - upload(file: File, name?: string) { - os.upload(file, this.$store.state.uploadFolder, name).then(res => { - this.file = res; - }); - }, - - send() { - this.sending = true; - os.api('messaging/messages/create', { - userId: this.user ? this.user.id : undefined, - groupId: this.group ? this.group.id : undefined, - text: this.text ? this.text : undefined, - fileId: this.file ? this.file.id : undefined - }).then(message => { - this.clear(); - }).catch(err => { - console.error(err); - }).then(() => { - this.sending = false; - }); - }, - - clear() { - this.text = ''; - this.file = null; - this.deleteDraft(); - }, - - saveDraft() { - const data = JSON.parse(localStorage.getItem('message_drafts') || '{}'); - - data[this.draftKey] = { - updatedAt: new Date(), - data: { - text: this.text, - file: this.file - } - } - - localStorage.setItem('message_drafts', JSON.stringify(data)); - }, - - deleteDraft() { - const data = JSON.parse(localStorage.getItem('message_drafts') || '{}'); - - delete data[this.draftKey]; - - localStorage.setItem('message_drafts', JSON.stringify(data)); - }, - - async insertEmoji(ev) { - os.openEmojiPicker(ev.currentTarget || ev.target, {}, this.$refs.text); - } - } -}); -</script> - -<style lang="scss" scoped> -.pemppnzi { - position: relative; - - > textarea { - cursor: auto; - display: block; - width: 100%; - min-width: 100%; - max-width: 100%; - height: 80px; - margin: 0; - padding: 16px 16px 0 16px; - resize: none; - font-size: 1em; - font-family: inherit; - outline: none; - border: none; - border-radius: 0; - box-shadow: none; - background: transparent; - box-sizing: border-box; - color: var(--fg); - } - - > .file { - padding: 8px; - color: #444; - background: #eee; - cursor: pointer; - } - - > .send { - position: absolute; - bottom: 0; - right: 0; - margin: 0; - padding: 16px; - font-size: 1em; - transition: color 0.1s ease; - color: var(--accent); - - &:active { - color: var(--accentDarken); - transition: color 0s ease; - } - } - - .files { - display: block; - margin: 0; - padding: 0 8px; - list-style: none; - - &:after { - content: ''; - display: block; - clear: both; - } - - > li { - display: block; - float: left; - margin: 4px; - padding: 0; - width: 64px; - height: 64px; - background-color: #eee; - background-repeat: no-repeat; - background-position: center center; - background-size: cover; - cursor: move; - - &:hover { - > .remove { - display: block; - } - } - - > .remove { - display: none; - position: absolute; - right: -6px; - top: -6px; - margin: 0; - padding: 0; - background: transparent; - outline: none; - border: none; - border-radius: 0; - box-shadow: none; - cursor: pointer; - } - } - } - - ._button { - margin: 0; - padding: 16px; - font-size: 1em; - font-weight: normal; - text-decoration: none; - transition: color 0.1s ease; - - &:hover { - color: var(--accent); - } - - &:active { - color: var(--accentDarken); - transition: color 0s ease; - } - } - - input[type=file] { - display: none; - } -} -</style> diff --git a/src/client/pages/messaging/messaging-room.message.vue b/src/client/pages/messaging/messaging-room.message.vue deleted file mode 100644 index a2740c0bdc..0000000000 --- a/src/client/pages/messaging/messaging-room.message.vue +++ /dev/null @@ -1,350 +0,0 @@ -<template> -<div class="thvuemwp" :class="{ isMe }" v-size="{ max: [400, 500] }"> - <MkAvatar class="avatar" :user="message.user" :show-indicator="true"/> - <div class="content"> - <div class="balloon" :class="{ noText: message.text == null }" > - <button class="delete-button" v-if="isMe" :title="$ts.delete" @click="del"> - <img src="/static-assets/client/remove.png" alt="Delete"/> - </button> - <div class="content" v-if="!message.isDeleted"> - <Mfm class="text" v-if="message.text" ref="text" :text="message.text" :i="$i"/> - <div class="file" v-if="message.file"> - <a :href="message.file.url" rel="noopener" target="_blank" :title="message.file.name"> - <img v-if="message.file.type.split('/')[0] == 'image'" :src="message.file.url" :alt="message.file.name"/> - <p v-else>{{ message.file.name }}</p> - </a> - </div> - </div> - <div class="content" v-else> - <p class="is-deleted">{{ $ts.deleted }}</p> - </div> - </div> - <div></div> - <MkUrlPreview v-for="url in urls" :url="url" :key="url" style="margin: 8px 0;"/> - <footer> - <template v-if="isGroup"> - <span class="read" v-if="message.reads.length > 0">{{ $ts.messageRead }} {{ message.reads.length }}</span> - </template> - <template v-else> - <span class="read" v-if="isMe && message.isRead">{{ $ts.messageRead }}</span> - </template> - <MkTime :time="message.createdAt"/> - <template v-if="message.is_edited"><i class="fas fa-pencil-alt"></i></template> - </footer> - </div> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import * as mfm from 'mfm-js'; -import { extractUrlFromMfm } from '@/misc/extract-url-from-mfm'; -import MkUrlPreview from '@client/components/url-preview.vue'; -import * as os from '@client/os'; - -export default defineComponent({ - components: { - MkUrlPreview - }, - props: { - message: { - required: true - }, - isGroup: { - required: false - } - }, - computed: { - isMe(): boolean { - return this.message.userId === this.$i.id; - }, - urls(): string[] { - if (this.message.text) { - return extractUrlFromMfm(mfm.parse(this.message.text)); - } else { - return []; - } - } - }, - methods: { - del() { - os.api('messaging/messages/delete', { - messageId: this.message.id - }); - } - } -}); -</script> - -<style lang="scss" scoped> -.thvuemwp { - $me-balloon-color: var(--accent); - - position: relative; - background-color: transparent; - display: flex; - - > .avatar { - position: sticky; - top: calc(var(--stickyTop, 0px) + 16px); - display: block; - width: 54px; - height: 54px; - transition: all 0.1s ease; - } - - > .content { - min-width: 0; - - > .balloon { - position: relative; - display: inline-flex; - align-items: center; - padding: 0; - min-height: 38px; - border-radius: 16px; - max-width: 100%; - - &:before { - content: ""; - pointer-events: none; - display: block; - position: absolute; - top: 12px; - } - - & + * { - clear: both; - } - - &:hover { - > .delete-button { - display: block; - } - } - - > .delete-button { - display: none; - position: absolute; - z-index: 1; - top: -4px; - right: -4px; - margin: 0; - padding: 0; - cursor: pointer; - outline: none; - border: none; - border-radius: 0; - box-shadow: none; - background: transparent; - - > img { - vertical-align: bottom; - width: 16px; - height: 16px; - cursor: pointer; - } - } - - > .content { - max-width: 100%; - - > .is-deleted { - display: block; - margin: 0; - padding: 0; - overflow: hidden; - overflow-wrap: break-word; - font-size: 1em; - color: rgba(#000, 0.5); - } - - > .text { - display: block; - margin: 0; - padding: 12px 18px; - overflow: hidden; - overflow-wrap: break-word; - word-break: break-word; - font-size: 1em; - color: rgba(#000, 0.8); - - & + .file { - > a { - border-radius: 0 0 16px 16px; - } - } - } - - > .file { - > a { - display: block; - max-width: 100%; - border-radius: 16px; - overflow: hidden; - text-decoration: none; - - &:hover { - text-decoration: none; - - > p { - background: #ccc; - } - } - - > * { - display: block; - margin: 0; - width: 100%; - max-height: 512px; - object-fit: contain; - box-sizing: border-box; - } - - > p { - padding: 30px; - text-align: center; - color: #555; - background: #ddd; - } - } - } - } - } - - > footer { - display: block; - margin: 2px 0 0 0; - font-size: 0.65em; - - > .read { - margin: 0 8px; - } - - > i { - margin-left: 4px; - } - } - } - - &:not(.isMe) { - padding-left: var(--margin); - - > .content { - padding-left: 16px; - padding-right: 32px; - - > .balloon { - $color: var(--messageBg); - background: $color; - - &.noText { - background: transparent; - } - - &:not(.noText):before { - left: -14px; - border-top: solid 8px transparent; - border-right: solid 8px $color; - border-bottom: solid 8px transparent; - border-left: solid 8px transparent; - } - - > .content { - > .text { - color: var(--fg); - } - } - } - - > footer { - text-align: left; - } - } - } - - &.isMe { - flex-direction: row-reverse; - padding-right: var(--margin); - - > .content { - padding-right: 16px; - padding-left: 32px; - text-align: right; - - > .balloon { - background: $me-balloon-color; - text-align: left; - - ::selection { - color: var(--accent); - background-color: #fff; - } - - &.noText { - background: transparent; - } - - &:not(.noText):before { - right: -14px; - left: auto; - border-top: solid 8px transparent; - border-right: solid 8px transparent; - border-bottom: solid 8px transparent; - border-left: solid 8px $me-balloon-color; - } - - > .content { - - > p.is-deleted { - color: rgba(#fff, 0.5); - } - - > .text { - &, ::v-deep(*) { - color: var(--fgOnAccent) !important; - } - } - } - } - - > footer { - text-align: right; - - > .read { - user-select: none; - } - } - } - } - - &.max-width_400px { - > .avatar { - width: 48px; - height: 48px; - } - - > .content { - > .balloon { - > .content { - > .text { - font-size: 0.9em; - } - } - } - } - } - - &.max-width_500px { - > .content { - > .balloon { - > .content { - > .text { - padding: 8px 16px; - } - } - } - } - } -} -</style> diff --git a/src/client/pages/messaging/messaging-room.vue b/src/client/pages/messaging/messaging-room.vue deleted file mode 100644 index 76e58d5bc9..0000000000 --- a/src/client/pages/messaging/messaging-room.vue +++ /dev/null @@ -1,470 +0,0 @@ -<template> -<div class="_section" - @dragover.prevent.stop="onDragover" - @drop.prevent.stop="onDrop" -> - <div class="_content mk-messaging-room"> - <div class="body"> - <MkLoading v-if="fetching"/> - <p class="empty" v-if="!fetching && messages.length == 0"><i class="fas fa-info-circle"></i>{{ $ts.noMessagesYet }}</p> - <p class="no-history" v-if="!fetching && messages.length > 0 && !existMoreMessages"><i class="fas fa-flag"></i>{{ $ts.noMoreHistory }}</p> - <button class="more _button" ref="loadMore" :class="{ fetching: fetchingMoreMessages }" v-show="existMoreMessages" @click="fetchMoreMessages" :disabled="fetchingMoreMessages"> - <template v-if="fetchingMoreMessages"><i class="fas fa-spinner fa-pulse fa-fw"></i></template>{{ fetchingMoreMessages ? $ts.loading : $ts.loadMore }} - </button> - <XList class="messages" :items="messages" v-slot="{ item: message }" direction="up" reversed> - <XMessage :message="message" :is-group="group != null" :key="message.id"/> - </XList> - </div> - <footer> - <div class="typers" v-if="typers.length > 0"> - <I18n :src="$ts.typingUsers" text-tag="span" class="users"> - <template #users> - <b v-for="user in typers" :key="user.id" class="user">{{ user.username }}</b> - </template> - </I18n> - <MkEllipsis/> - </div> - <transition name="fade"> - <div class="new-message" v-show="showIndicator"> - <button class="_buttonPrimary" @click="onIndicatorClick"><i class="fas fa-arrow-circle-down"></i>{{ $ts.newMessageExists }}</button> - </div> - </transition> - <XForm v-if="!fetching" :user="user" :group="group" ref="form" class="form"/> - </footer> - </div> -</div> -</template> - -<script lang="ts"> -import { computed, defineComponent, markRaw } from 'vue'; -import XList from '@client/components/date-separated-list.vue'; -import XMessage from './messaging-room.message.vue'; -import XForm from './messaging-room.form.vue'; -import { parseAcct } from '@/misc/acct'; -import { isBottom, onScrollBottom, scroll } from '@client/scripts/scroll'; -import * as os from '@client/os'; -import { popout } from '@client/scripts/popout'; -import * as sound from '@client/scripts/sound'; -import * as symbols from '@client/symbols'; - -const Component = defineComponent({ - components: { - XMessage, - XForm, - XList, - }, - - inject: ['inWindow'], - - props: { - userAcct: { - type: String, - required: false, - }, - groupId: { - type: String, - required: false, - }, - }, - - data() { - return { - [symbols.PAGE_INFO]: computed(() => !this.fetching ? this.user ? { - userName: this.user, - avatar: this.user, - action: { - icon: 'fas fa-ellipsis-h', - handler: this.menu, - }, - } : { - title: this.group.name, - icon: 'fas fa-users', - action: { - icon: 'fas fa-ellipsis-h', - handler: this.menu, - }, - } : null), - fetching: true, - user: null, - group: null, - fetchingMoreMessages: false, - messages: [], - existMoreMessages: false, - connection: null, - showIndicator: false, - timer: null, - typers: [], - ilObserver: new IntersectionObserver( - (entries) => entries.some((entry) => entry.isIntersecting) - && !this.fetching - && !this.fetchingMoreMessages - && this.existMoreMessages - && this.fetchMoreMessages() - ), - }; - }, - - computed: { - form(): any { - return this.$refs.form; - } - }, - - watch: { - userAcct: 'fetch', - groupId: 'fetch', - }, - - mounted() { - this.fetch(); - if (this.$store.state.enableInfiniteScroll) { - this.$nextTick(() => this.ilObserver.observe(this.$refs.loadMore as Element)); - } - }, - - beforeUnmount() { - this.connection.dispose(); - - document.removeEventListener('visibilitychange', this.onVisibilitychange); - - this.ilObserver.disconnect(); - }, - - methods: { - async fetch() { - this.fetching = true; - if (this.userAcct) { - const user = await os.api('users/show', parseAcct(this.userAcct)); - this.user = user; - } else { - const group = await os.api('users/groups/show', { groupId: this.groupId }); - this.group = group; - } - - this.connection = markRaw(os.stream.useChannel('messaging', { - otherparty: this.user ? this.user.id : undefined, - group: this.group ? this.group.id : undefined, - })); - - this.connection.on('message', this.onMessage); - this.connection.on('read', this.onRead); - this.connection.on('deleted', this.onDeleted); - this.connection.on('typers', typers => { - this.typers = typers.filter(u => u.id !== this.$i.id); - }); - - document.addEventListener('visibilitychange', this.onVisibilitychange); - - this.fetchMessages().then(() => { - this.scrollToBottom(); - - // ใใฃใจ่ฆใใฎไบคๅทฎๆค็ฅใ็บ็ซใใใชใใใใซfetchใฏ - // ในใฏใญใผใซใ็ตใใใพใงfalseใซใใฆใใ - // scrollendใฎใใใชใคใใณใใฏใชใใฎใงsetTimeoutใง - setTimeout(() => this.fetching = false, 300); - }); - }, - - onDragover(e) { - const isFile = e.dataTransfer.items[0].kind == 'file'; - const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_; - - if (isFile || isDriveFile) { - e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move'; - } else { - e.dataTransfer.dropEffect = 'none'; - } - }, - - onDrop(e): void { - // ใใกใคใซใ ใฃใใ - if (e.dataTransfer.files.length == 1) { - this.form.upload(e.dataTransfer.files[0]); - return; - } else if (e.dataTransfer.files.length > 1) { - os.dialog({ - type: 'error', - text: this.$ts.onlyOneFileCanBeAttached - }); - return; - } - - //#region ใใฉใคใใฎใใกใคใซ - const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_); - if (driveFile != null && driveFile != '') { - const file = JSON.parse(driveFile); - this.form.file = file; - } - //#endregion - }, - - fetchMessages() { - return new Promise((resolve, reject) => { - const max = this.existMoreMessages ? 20 : 10; - - os.api('messaging/messages', { - userId: this.user ? this.user.id : undefined, - groupId: this.group ? this.group.id : undefined, - limit: max + 1, - untilId: this.existMoreMessages ? this.messages[0].id : undefined - }).then(messages => { - if (messages.length == max + 1) { - this.existMoreMessages = true; - messages.pop(); - } else { - this.existMoreMessages = false; - } - - this.messages.unshift.apply(this.messages, messages.reverse()); - resolve(); - }); - }); - }, - - fetchMoreMessages() { - this.fetchingMoreMessages = true; - this.fetchMessages().then(() => { - this.fetchingMoreMessages = false; - }); - }, - - onMessage(message) { - sound.play('chat'); - - const _isBottom = isBottom(this.$el, 64); - - this.messages.push(message); - if (message.userId != this.$i.id && !document.hidden) { - this.connection.send('read', { - id: message.id - }); - } - - if (_isBottom) { - // Scroll to bottom - this.$nextTick(() => { - this.scrollToBottom(); - }); - } else if (message.userId != this.$i.id) { - // Notify - this.notifyNewMessage(); - } - }, - - onRead(x) { - if (this.user) { - if (!Array.isArray(x)) x = [x]; - for (const id of x) { - if (this.messages.some(x => x.id == id)) { - const exist = this.messages.map(x => x.id).indexOf(id); - this.messages[exist] = { - ...this.messages[exist], - isRead: true, - }; - } - } - } else if (this.group) { - for (const id of x.ids) { - if (this.messages.some(x => x.id == id)) { - const exist = this.messages.map(x => x.id).indexOf(id); - this.messages[exist] = { - ...this.messages[exist], - reads: [...this.messages[exist].reads, x.userId] - }; - } - } - } - }, - - onDeleted(id) { - const msg = this.messages.find(m => m.id === id); - if (msg) { - this.messages = this.messages.filter(m => m.id !== msg.id); - } - }, - - scrollToBottom() { - scroll(this.$el, { top: this.$el.offsetHeight }); - }, - - onIndicatorClick() { - this.showIndicator = false; - this.scrollToBottom(); - }, - - notifyNewMessage() { - this.showIndicator = true; - - onScrollBottom(this.$el, () => { - this.showIndicator = false; - }); - - if (this.timer) clearTimeout(this.timer); - - this.timer = setTimeout(() => { - this.showIndicator = false; - }, 4000); - }, - - onVisibilitychange() { - if (document.hidden) return; - for (const message of this.messages) { - if (message.userId !== this.$i.id && !message.isRead) { - this.connection.send('read', { - id: message.id - }); - } - } - }, - - menu(ev) { - const path = this.groupId ? `/my/messaging/group/${this.groupId}` : `/my/messaging/${this.userAcct}`; - - os.popupMenu([this.inWindow ? undefined : { - text: this.$ts.openInWindow, - icon: 'fas fa-window-maximize', - action: () => { - os.pageWindow(path); - this.$router.back(); - }, - }, this.inWindow ? undefined : { - text: this.$ts.popout, - icon: 'fas fa-external-link-alt', - action: () => { - popout(path); - this.$router.back(); - }, - }], ev.currentTarget || ev.target); - } - } -}); - -export default Component; -</script> - -<style lang="scss" scoped> -.mk-messaging-room { - > .body { - > .empty { - width: 100%; - margin: 0; - padding: 16px 8px 8px 8px; - text-align: center; - font-size: 0.8em; - opacity: 0.5; - - i { - margin-right: 4px; - } - } - - > .no-history { - display: block; - margin: 0; - padding: 16px; - text-align: center; - font-size: 0.8em; - color: var(--messagingRoomInfo); - opacity: 0.5; - - i { - margin-right: 4px; - } - } - - > .more { - display: block; - margin: 16px auto; - padding: 0 12px; - line-height: 24px; - color: #fff; - background: rgba(#000, 0.3); - border-radius: 12px; - - &:hover { - background: rgba(#000, 0.4); - } - - &:active { - background: rgba(#000, 0.5); - } - - &.fetching { - cursor: wait; - } - - > i { - margin-right: 4px; - } - } - - > .messages { - > ::v-deep(*) { - margin-bottom: 16px; - } - } - } - - > footer { - width: 100%; - position: relative; - - > .new-message { - position: absolute; - top: -48px; - width: 100%; - padding: 8px 0; - text-align: center; - - > button { - display: inline-block; - margin: 0; - padding: 0 12px 0 30px; - line-height: 32px; - font-size: 12px; - border-radius: 16px; - - > i { - position: absolute; - top: 0; - left: 10px; - line-height: 32px; - font-size: 16px; - } - } - } - - > .typers { - position: absolute; - bottom: 100%; - padding: 0 8px 0 8px; - font-size: 0.9em; - color: var(--fgTransparentWeak); - - > .users { - > .user + .user:before { - content: ", "; - font-weight: normal; - } - - > .user:last-of-type:after { - content: " "; - } - } - } - - > .form { - border-top: solid 0.5px var(--divider); - } - } -} - -.fade-enter-active, .fade-leave-active { - transition: opacity 0.1s; -} - -.fade-enter-from, .fade-leave-to { - transition: opacity 0.5s; - opacity: 0; -} -</style> diff --git a/src/client/pages/mfm-cheat-sheet.vue b/src/client/pages/mfm-cheat-sheet.vue deleted file mode 100644 index 5ff4317627..0000000000 --- a/src/client/pages/mfm-cheat-sheet.vue +++ /dev/null @@ -1,365 +0,0 @@ -<template> -<div class="mwysmxbg"> - <div class="_isolated">{{ $ts._mfm.intro }}</div> - <div class="section _block"> - <div class="title">{{ $ts._mfm.mention }}</div> - <div class="content"> - <p>{{ $ts._mfm.mentionDescription }}</p> - <div class="preview"> - <Mfm :text="preview_mention"/> - <MkTextarea v-model="preview_mention"><template #label>MFM</template></MkTextarea> - </div> - </div> - </div> - <div class="section _block"> - <div class="title">{{ $ts._mfm.hashtag }}</div> - <div class="content"> - <p>{{ $ts._mfm.hashtagDescription }}</p> - <div class="preview"> - <Mfm :text="preview_hashtag"/> - <MkTextarea v-model="preview_hashtag"><template #label>MFM</template></MkTextarea> - </div> - </div> - </div> - <div class="section _block"> - <div class="title">{{ $ts._mfm.url }}</div> - <div class="content"> - <p>{{ $ts._mfm.urlDescription }}</p> - <div class="preview"> - <Mfm :text="preview_url"/> - <MkTextarea v-model="preview_url"><template #label>MFM</template></MkTextarea> - </div> - </div> - </div> - <div class="section _block"> - <div class="title">{{ $ts._mfm.link }}</div> - <div class="content"> - <p>{{ $ts._mfm.linkDescription }}</p> - <div class="preview"> - <Mfm :text="preview_link"/> - <MkTextarea v-model="preview_link"><template #label>MFM</template></MkTextarea> - </div> - </div> - </div> - <div class="section _block"> - <div class="title">{{ $ts._mfm.emoji }}</div> - <div class="content"> - <p>{{ $ts._mfm.emojiDescription }}</p> - <div class="preview"> - <Mfm :text="preview_emoji"/> - <MkTextarea v-model="preview_emoji"><template #label>MFM</template></MkTextarea> - </div> - </div> - </div> - <div class="section _block"> - <div class="title">{{ $ts._mfm.bold }}</div> - <div class="content"> - <p>{{ $ts._mfm.boldDescription }}</p> - <div class="preview"> - <Mfm :text="preview_bold"/> - <MkTextarea v-model="preview_bold"><template #label>MFM</template></MkTextarea> - </div> - </div> - </div> - <div class="section _block"> - <div class="title">{{ $ts._mfm.small }}</div> - <div class="content"> - <p>{{ $ts._mfm.smallDescription }}</p> - <div class="preview"> - <Mfm :text="preview_small"/> - <MkTextarea v-model="preview_small"><template #label>MFM</template></MkTextarea> - </div> - </div> - </div> - <div class="section _block"> - <div class="title">{{ $ts._mfm.quote }}</div> - <div class="content"> - <p>{{ $ts._mfm.quoteDescription }}</p> - <div class="preview"> - <Mfm :text="preview_quote"/> - <MkTextarea v-model="preview_quote"><template #label>MFM</template></MkTextarea> - </div> - </div> - </div> - <div class="section _block"> - <div class="title">{{ $ts._mfm.center }}</div> - <div class="content"> - <p>{{ $ts._mfm.centerDescription }}</p> - <div class="preview"> - <Mfm :text="preview_center"/> - <MkTextarea v-model="preview_center"><template #label>MFM</template></MkTextarea> - </div> - </div> - </div> - <div class="section _block"> - <div class="title">{{ $ts._mfm.inlineCode }}</div> - <div class="content"> - <p>{{ $ts._mfm.inlineCodeDescription }}</p> - <div class="preview"> - <Mfm :text="preview_inlineCode"/> - <MkTextarea v-model="preview_inlineCode"><template #label>MFM</template></MkTextarea> - </div> - </div> - </div> - <div class="section _block"> - <div class="title">{{ $ts._mfm.blockCode }}</div> - <div class="content"> - <p>{{ $ts._mfm.blockCodeDescription }}</p> - <div class="preview"> - <Mfm :text="preview_blockCode"/> - <MkTextarea v-model="preview_blockCode"><template #label>MFM</template></MkTextarea> - </div> - </div> - </div> - <div class="section _block"> - <div class="title">{{ $ts._mfm.inlineMath }}</div> - <div class="content"> - <p>{{ $ts._mfm.inlineMathDescription }}</p> - <div class="preview"> - <Mfm :text="preview_inlineMath"/> - <MkTextarea v-model="preview_inlineMath"><template #label>MFM</template></MkTextarea> - </div> - </div> - </div> - <div class="section _block"> - <div class="title">{{ $ts._mfm.search }}</div> - <div class="content"> - <p>{{ $ts._mfm.searchDescription }}</p> - <div class="preview"> - <Mfm :text="preview_search"/> - <MkTextarea v-model="preview_search"><template #label>MFM</template></MkTextarea> - </div> - </div> - </div> - <div class="section _block"> - <div class="title">{{ $ts._mfm.flip }}</div> - <div class="content"> - <p>{{ $ts._mfm.flipDescription }}</p> - <div class="preview"> - <Mfm :text="preview_flip"/> - <MkTextarea v-model="preview_flip"><template #label>MFM</template></MkTextarea> - </div> - </div> - </div> - <div class="section _block"> - <div class="title">{{ $ts._mfm.font }}</div> - <div class="content"> - <p>{{ $ts._mfm.fontDescription }}</p> - <div class="preview"> - <Mfm :text="preview_font"/> - <MkTextarea v-model="preview_font"><template #label>MFM</template></MkTextarea> - </div> - </div> - </div> - <div class="section _block"> - <div class="title">{{ $ts._mfm.x2 }}</div> - <div class="content"> - <p>{{ $ts._mfm.x2Description }}</p> - <div class="preview"> - <Mfm :text="preview_x2"/> - <MkTextarea v-model="preview_x2"><template #label>MFM</template></MkTextarea> - </div> - </div> - </div> - <div class="section _block"> - <div class="title">{{ $ts._mfm.x3 }}</div> - <div class="content"> - <p>{{ $ts._mfm.x3Description }}</p> - <div class="preview"> - <Mfm :text="preview_x3"/> - <MkTextarea v-model="preview_x3"><template #label>MFM</template></MkTextarea> - </div> - </div> - </div> - <div class="section _block"> - <div class="title">{{ $ts._mfm.x4 }}</div> - <div class="content"> - <p>{{ $ts._mfm.x4Description }}</p> - <div class="preview"> - <Mfm :text="preview_x4"/> - <MkTextarea v-model="preview_x4"><template #label>MFM</template></MkTextarea> - </div> - </div> - </div> - <div class="section _block"> - <div class="title">{{ $ts._mfm.blur }}</div> - <div class="content"> - <p>{{ $ts._mfm.blurDescription }}</p> - <div class="preview"> - <Mfm :text="preview_blur"/> - <MkTextarea v-model="preview_blur"><template #label>MFM</template></MkTextarea> - </div> - </div> - </div> - <div class="section _block"> - <div class="title">{{ $ts._mfm.jelly }}</div> - <div class="content"> - <p>{{ $ts._mfm.jellyDescription }}</p> - <div class="preview"> - <Mfm :text="preview_jelly"/> - <MkTextarea v-model="preview_jelly"><template #label>MFM</template></MkTextarea> - </div> - </div> - </div> - <div class="section _block"> - <div class="title">{{ $ts._mfm.tada }}</div> - <div class="content"> - <p>{{ $ts._mfm.tadaDescription }}</p> - <div class="preview"> - <Mfm :text="preview_tada"/> - <MkTextarea v-model="preview_tada"><template #label>MFM</template></MkTextarea> - </div> - </div> - </div> - <div class="section _block"> - <div class="title">{{ $ts._mfm.jump }}</div> - <div class="content"> - <p>{{ $ts._mfm.jumpDescription }}</p> - <div class="preview"> - <Mfm :text="preview_jump"/> - <MkTextarea v-model="preview_jump"><template #label>MFM</template></MkTextarea> - </div> - </div> - </div> - <div class="section _block"> - <div class="title">{{ $ts._mfm.bounce }}</div> - <div class="content"> - <p>{{ $ts._mfm.bounceDescription }}</p> - <div class="preview"> - <Mfm :text="preview_bounce"/> - <MkTextarea v-model="preview_bounce"><template #label>MFM</template></MkTextarea> - </div> - </div> - </div> - <div class="section _block"> - <div class="title">{{ $ts._mfm.spin }}</div> - <div class="content"> - <p>{{ $ts._mfm.spinDescription }}</p> - <div class="preview"> - <Mfm :text="preview_spin"/> - <MkTextarea v-model="preview_spin"><template #label>MFM</template></MkTextarea> - </div> - </div> - </div> - <div class="section _block"> - <div class="title">{{ $ts._mfm.shake }}</div> - <div class="content"> - <p>{{ $ts._mfm.shakeDescription }}</p> - <div class="preview"> - <Mfm :text="preview_shake"/> - <MkTextarea v-model="preview_shake"><template #label>MFM</template></MkTextarea> - </div> - </div> - </div> - <div class="section _block"> - <div class="title">{{ $ts._mfm.twitch }}</div> - <div class="content"> - <p>{{ $ts._mfm.twitchDescription }}</p> - <div class="preview"> - <Mfm :text="preview_twitch"/> - <MkTextarea v-model="preview_twitch"><template #label>MFM</template></MkTextarea> - </div> - </div> - </div> - <div class="section _block"> - <div class="title">{{ $ts._mfm.rainbow }}</div> - <div class="content"> - <p>{{ $ts._mfm.rainbowDescription }}</p> - <div class="preview"> - <Mfm :text="preview_rainbow"/> - <MkTextarea v-model="preview_rainbow"><template #label>MFM</template></MkTextarea> - </div> - </div> - </div> - <div class="section _block"> - <div class="title">{{ $ts._mfm.sparkle }}</div> - <div class="content"> - <p>{{ $ts._mfm.sparkleDescription }}</p> - <div class="preview"> - <Mfm :text="preview_sparkle"/> - <MkTextarea v-model="preview_sparkle"><span>MFM</span></MkTextarea> - </div> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import MkTextarea from '@client/components/form/textarea.vue'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - MkTextarea - }, - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts._mfm.cheatSheet, - icon: 'fas fa-question-circle', - }, - preview_mention: '@example', - preview_hashtag: '#test', - preview_url: `https://example.com`, - preview_link: `[${this.$ts._mfm.dummy}](https://example.com)`, - preview_emoji: this.$instance.emojis.length ? `:${this.$instance.emojis[0].name}:` : `:emojiname:`, - preview_bold: `**${this.$ts._mfm.dummy}**`, - preview_small: `<small>${this.$ts._mfm.dummy}</small>`, - preview_center: `<center>${this.$ts._mfm.dummy}</center>`, - preview_inlineCode: '`<: "Hello, world!"`', - preview_blockCode: '```\n~ (#i, 100) {\n\t<: ? ((i % 15) = 0) "FizzBuzz"\n\t\t.? ((i % 3) = 0) "Fizz"\n\t\t.? ((i % 5) = 0) "Buzz"\n\t\t. i\n}\n```', - preview_inlineMath: '\\(x= \\frac{-b\' \\pm \\sqrt{(b\')^2-ac}}{a}\\)', - preview_quote: `> ${this.$ts._mfm.dummy}`, - preview_search: `${this.$ts._mfm.dummy} ๆค็ดข`, - preview_jelly: `$[jelly ๐ฎ]`, - preview_tada: `$[tada ๐ฎ]`, - preview_jump: `$[jump ๐ฎ]`, - preview_bounce: `$[bounce ๐ฎ]`, - preview_shake: `$[shake ๐ฎ]`, - preview_twitch: `$[twitch ๐ฎ]`, - preview_spin: `$[spin ๐ฎ] $[spin.left ๐ฎ] $[spin.alternate ๐ฎ]\n$[spin.x ๐ฎ] $[spin.x,left ๐ฎ] $[spin.x,alternate ๐ฎ]\n$[spin.y ๐ฎ] $[spin.y,left ๐ฎ] $[spin.y,alternate ๐ฎ]`, - preview_flip: `$[flip ${this.$ts._mfm.dummy}]\n$[flip.v ${this.$ts._mfm.dummy}]\n$[flip.h,v ${this.$ts._mfm.dummy}]`, - preview_font: `$[font.serif ${this.$ts._mfm.dummy}]\n$[font.monospace ${this.$ts._mfm.dummy}]\n$[font.cursive ${this.$ts._mfm.dummy}]\n$[font.fantasy ${this.$ts._mfm.dummy}]`, - preview_x2: `$[x2 ๐ฎ]`, - preview_x3: `$[x3 ๐ฎ]`, - preview_x4: `$[x4 ๐ฎ]`, - preview_blur: `$[blur ${this.$ts._mfm.dummy}]`, - preview_rainbow: `$[rainbow ๐ฎ]`, - preview_sparkle: `$[sparkle ๐ฎ]`, - } - }, -}); -</script> - -<style lang="scss" scoped> -.mwysmxbg { - background: var(--bg); - - > .section { - > .title { - position: sticky; - z-index: 1; - top: var(--stickyTop, 0px); - padding: 16px; - font-weight: bold; - -webkit-backdrop-filter: var(--blur, blur(10px)); - backdrop-filter: var(--blur, blur(10px)); - background-color: var(--X16); - } - - > .content { - > p { - margin: 0; - padding: 16px; - } - - > .preview { - border-top: solid 0.5px var(--divider); - padding: 16px; - } - } - } -} -</style> diff --git a/src/client/pages/miauth.vue b/src/client/pages/miauth.vue deleted file mode 100644 index 39cd832838..0000000000 --- a/src/client/pages/miauth.vue +++ /dev/null @@ -1,100 +0,0 @@ -<template> -<div v-if="$i"> - <div class="waiting _section" v-if="state == 'waiting'"> - <div class="_content"> - <MkLoading/> - </div> - </div> - <div class="denied _section" v-if="state == 'denied'"> - <div class="_content"> - <p>{{ $ts._auth.denied }}</p> - </div> - </div> - <div class="accepted _section" v-else-if="state == 'accepted'"> - <div class="_content"> - <p v-if="callback">{{ $ts._auth.callback }}<MkEllipsis/></p> - <p v-else>{{ $ts._auth.pleaseGoBack }}</p> - </div> - </div> - <div class="_section" v-else> - <div class="_title" v-if="name">{{ $t('_auth.shareAccess', { name: name }) }}</div> - <div class="_title" v-else>{{ $ts._auth.shareAccessAsk }}</div> - <div class="_content"> - <p>{{ $ts._auth.permissionAsk }}</p> - <ul> - <li v-for="p in permission" :key="p">{{ $t(`_permissions.${p}`) }}</li> - </ul> - </div> - <div class="_footer"> - <MkButton @click="deny" inline>{{ $ts.cancel }}</MkButton> - <MkButton @click="accept" inline primary>{{ $ts.accept }}</MkButton> - </div> - </div> -</div> -<div class="signin" v-else> - <MkSignin @login="onLogin"/> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import MkSignin from '@client/components/signin.vue'; -import MkButton from '@client/components/ui/button.vue'; -import * as os from '@client/os'; -import { login } from '@client/account'; - -export default defineComponent({ - components: { - MkSignin, - MkButton, - }, - data() { - return { - state: null - }; - }, - computed: { - session(): string { - return this.$route.params.session; - }, - callback(): string { - return this.$route.query.callback; - }, - name(): string { - return this.$route.query.name; - }, - icon(): string { - return this.$route.query.icon; - }, - permission(): string[] { - return this.$route.query.permission ? this.$route.query.permission.split(',') : []; - }, - }, - methods: { - async accept() { - this.state = 'waiting'; - await os.api('miauth/gen-token', { - session: this.session, - name: this.name, - iconUrl: this.icon, - permission: this.permission, - }); - - this.state = 'accepted'; - if (this.callback) { - location.href = `${this.callback}?session=${this.session}`; - } - }, - deny() { - this.state = 'denied'; - }, - onLogin(res) { - login(res.i); - } - } -}); -</script> - -<style lang="scss" scoped> - -</style> diff --git a/src/client/pages/my-antennas/create.vue b/src/client/pages/my-antennas/create.vue deleted file mode 100644 index d4762411e7..0000000000 --- a/src/client/pages/my-antennas/create.vue +++ /dev/null @@ -1,51 +0,0 @@ -<template> -<div class="geegznzt"> - <XAntenna :antenna="draft" @created="onAntennaCreated"/> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import MkButton from '@client/components/ui/button.vue'; -import XAntenna from './editor.vue'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - MkButton, - XAntenna, - }, - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.manageAntennas, - icon: 'fas fa-satellite', - }, - draft: { - name: '', - src: 'all', - userListId: null, - userGroupId: null, - users: [], - keywords: [], - excludeKeywords: [], - withReplies: false, - caseSensitive: false, - withFile: false, - notify: false - }, - }; - }, - - methods: { - onAntennaCreated() { - this.$router.push('/my/antennas'); - }, - } -}); -</script> - -<style lang="scss" scoped> - -</style> diff --git a/src/client/pages/my-antennas/edit.vue b/src/client/pages/my-antennas/edit.vue deleted file mode 100644 index 9deafb4235..0000000000 --- a/src/client/pages/my-antennas/edit.vue +++ /dev/null @@ -1,56 +0,0 @@ -<template> -<div class=""> - <XAntenna v-if="antenna" :antenna="antenna" @updated="onAntennaUpdated"/> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import MkButton from '@client/components/ui/button.vue'; -import XAntenna from './editor.vue'; -import * as symbols from '@client/symbols'; -import * as os from '@client/os'; - -export default defineComponent({ - components: { - MkButton, - XAntenna, - }, - - props: { - antennaId: { - type: String, - required: true, - } - }, - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.manageAntennas, - icon: 'fas fa-satellite', - }, - antenna: null, - }; - }, - - watch: { - antennaId: { - async handler() { - this.antenna = await os.api('antennas/show', { antennaId: this.antennaId }); - }, - immediate: true, - } - }, - - methods: { - onAntennaUpdated() { - this.$router.push('/my/antennas'); - }, - } -}); -</script> - -<style lang="scss" scoped> - -</style> diff --git a/src/client/pages/my-antennas/editor.vue b/src/client/pages/my-antennas/editor.vue deleted file mode 100644 index 93ab640030..0000000000 --- a/src/client/pages/my-antennas/editor.vue +++ /dev/null @@ -1,190 +0,0 @@ -<template> -<div class="shaynizk"> - <div class="form"> - <MkInput v-model="name" class="_formBlock"> - <template #label>{{ $ts.name }}</template> - </MkInput> - <MkSelect v-model="src" class="_formBlock"> - <template #label>{{ $ts.antennaSource }}</template> - <option value="all">{{ $ts._antennaSources.all }}</option> - <option value="home">{{ $ts._antennaSources.homeTimeline }}</option> - <option value="users">{{ $ts._antennaSources.users }}</option> - <option value="list">{{ $ts._antennaSources.userList }}</option> - <option value="group">{{ $ts._antennaSources.userGroup }}</option> - </MkSelect> - <MkSelect v-model="userListId" v-if="src === 'list'" class="_formBlock"> - <template #label>{{ $ts.userList }}</template> - <option v-for="list in userLists" :value="list.id" :key="list.id">{{ list.name }}</option> - </MkSelect> - <MkSelect v-model="userGroupId" v-else-if="src === 'group'" class="_formBlock"> - <template #label>{{ $ts.userGroup }}</template> - <option v-for="group in userGroups" :value="group.id" :key="group.id">{{ group.name }}</option> - </MkSelect> - <MkTextarea v-model="users" v-else-if="src === 'users'" class="_formBlock"> - <template #label>{{ $ts.users }}</template> - <template #caption>{{ $ts.antennaUsersDescription }} <button class="_textButton" @click="addUser">{{ $ts.addUser }}</button></template> - </MkTextarea> - <MkSwitch v-model="withReplies" class="_formBlock">{{ $ts.withReplies }}</MkSwitch> - <MkTextarea v-model="keywords" class="_formBlock"> - <template #label>{{ $ts.antennaKeywords }}</template> - <template #caption>{{ $ts.antennaKeywordsDescription }}</template> - </MkTextarea> - <MkTextarea v-model="excludeKeywords" class="_formBlock"> - <template #label>{{ $ts.antennaExcludeKeywords }}</template> - <template #caption>{{ $ts.antennaKeywordsDescription }}</template> - </MkTextarea> - <MkSwitch v-model="caseSensitive" class="_formBlock">{{ $ts.caseSensitive }}</MkSwitch> - <MkSwitch v-model="withFile" class="_formBlock">{{ $ts.withFileAntenna }}</MkSwitch> - <MkSwitch v-model="notify" class="_formBlock">{{ $ts.notifyAntenna }}</MkSwitch> - </div> - <div class="actions"> - <MkButton inline @click="saveAntenna()" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton> - <MkButton inline @click="deleteAntenna()" v-if="antenna.id != null" danger><i class="fas fa-trash"></i> {{ $ts.delete }}</MkButton> - </div> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import MkButton from '@client/components/ui/button.vue'; -import MkInput from '@client/components/form/input.vue'; -import MkTextarea from '@client/components/form/textarea.vue'; -import MkSelect from '@client/components/form/select.vue'; -import MkSwitch from '@client/components/form/switch.vue'; -import { getAcct } from '@/misc/acct'; -import * as os from '@client/os'; - -export default defineComponent({ - components: { - MkButton, MkInput, MkTextarea, MkSelect, MkSwitch - }, - - props: { - antenna: { - type: Object, - required: true - } - }, - - data() { - return { - name: '', - src: '', - userListId: null, - userGroupId: null, - users: '', - keywords: '', - excludeKeywords: '', - caseSensitive: false, - withReplies: false, - withFile: false, - notify: false, - userLists: null, - userGroups: null, - }; - }, - - watch: { - async src() { - if (this.src === 'list' && this.userLists === null) { - this.userLists = await os.api('users/lists/list'); - } - - if (this.src === 'group' && this.userGroups === null) { - const groups1 = await os.api('users/groups/owned'); - const groups2 = await os.api('users/groups/joined'); - - this.userGroups = [...groups1, ...groups2]; - } - } - }, - - created() { - this.name = this.antenna.name; - this.src = this.antenna.src; - this.userListId = this.antenna.userListId; - this.userGroupId = this.antenna.userGroupId; - this.users = this.antenna.users.join('\n'); - this.keywords = this.antenna.keywords.map(x => x.join(' ')).join('\n'); - this.excludeKeywords = this.antenna.excludeKeywords.map(x => x.join(' ')).join('\n'); - this.caseSensitive = this.antenna.caseSensitive; - this.withReplies = this.antenna.withReplies; - this.withFile = this.antenna.withFile; - this.notify = this.antenna.notify; - }, - - methods: { - async saveAntenna() { - if (this.antenna.id == null) { - await os.apiWithDialog('antennas/create', { - name: this.name, - src: this.src, - userListId: this.userListId, - userGroupId: this.userGroupId, - withReplies: this.withReplies, - withFile: this.withFile, - notify: this.notify, - caseSensitive: this.caseSensitive, - users: this.users.trim().split('\n').map(x => x.trim()), - keywords: this.keywords.trim().split('\n').map(x => x.trim().split(' ')), - excludeKeywords: this.excludeKeywords.trim().split('\n').map(x => x.trim().split(' ')), - }); - this.$emit('created'); - } else { - await os.apiWithDialog('antennas/update', { - antennaId: this.antenna.id, - name: this.name, - src: this.src, - userListId: this.userListId, - userGroupId: this.userGroupId, - withReplies: this.withReplies, - withFile: this.withFile, - notify: this.notify, - caseSensitive: this.caseSensitive, - users: this.users.trim().split('\n').map(x => x.trim()), - keywords: this.keywords.trim().split('\n').map(x => x.trim().split(' ')), - excludeKeywords: this.excludeKeywords.trim().split('\n').map(x => x.trim().split(' ')), - }); - this.$emit('updated'); - } - }, - - async deleteAntenna() { - const { canceled } = await os.dialog({ - type: 'warning', - text: this.$t('removeAreYouSure', { x: this.antenna.name }), - showCancelButton: true - }); - if (canceled) return; - - await os.api('antennas/delete', { - antennaId: this.antenna.id, - }); - - os.success(); - this.$emit('deleted'); - }, - - addUser() { - os.selectUser().then(user => { - this.users = this.users.trim(); - this.users += '\n@' + getAcct(user); - this.users = this.users.trim(); - }); - } - } -}); -</script> - -<style lang="scss" scoped> -.shaynizk { - > .form { - padding: 32px; - } - - > .actions { - padding: 24px 32px; - border-top: solid 0.5px var(--divider); - } -} -</style> diff --git a/src/client/pages/my-antennas/index.vue b/src/client/pages/my-antennas/index.vue deleted file mode 100644 index c27bb2c15e..0000000000 --- a/src/client/pages/my-antennas/index.vue +++ /dev/null @@ -1,71 +0,0 @@ -<template> -<div class="ieepwinx _section"> - <MkButton :link="true" to="/my/antennas/create" primary class="add"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton> - - <div class="_content"> - <MkPagination :pagination="pagination" #default="{items}" ref="list"> - <MkA class="ljoevbzj" v-for="antenna in items" :key="antenna.id" :to="`/my/antennas/${antenna.id}`"> - <div class="name">{{ antenna.name }}</div> - </MkA> - </MkPagination> - </div> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import MkPagination from '@client/components/ui/pagination.vue'; -import MkButton from '@client/components/ui/button.vue'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - MkPagination, - MkButton, - }, - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.manageAntennas, - icon: 'fas fa-satellite', - action: { - icon: 'fas fa-plus', - handler: this.create - } - }, - pagination: { - endpoint: 'antennas/list', - limit: 10, - }, - }; - }, -}); -</script> - -<style lang="scss" scoped> -.ieepwinx { - padding: 16px; - - > .add { - margin: 0 auto 16px auto; - } - - .ljoevbzj { - display: block; - padding: 16px; - margin-bottom: 8px; - border: solid 1px var(--divider); - border-radius: 6px; - - &:hover { - border: solid 1px var(--accent); - text-decoration: none; - } - - > .name { - font-weight: bold; - } - } -} -</style> diff --git a/src/client/pages/my-clips/index.vue b/src/client/pages/my-clips/index.vue deleted file mode 100644 index c4ca474748..0000000000 --- a/src/client/pages/my-clips/index.vue +++ /dev/null @@ -1,104 +0,0 @@ -<template> -<div class="_section qtcaoidl"> - <MkButton @click="create" primary class="add"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton> - - <div class="_content"> - <MkPagination :pagination="pagination" #default="{items}" ref="list" class="list"> - <MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _gap"> - <b>{{ item.name }}</b> - <div v-if="item.description" class="description">{{ item.description }}</div> - </MkA> - </MkPagination> - </div> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import MkPagination from '@client/components/ui/pagination.vue'; -import MkButton from '@client/components/ui/button.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - MkPagination, - MkButton, - }, - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.clip, - icon: 'fas fa-paperclip', - action: { - icon: 'fas fa-plus', - handler: this.create - } - }, - pagination: { - endpoint: 'clips/list', - limit: 10, - }, - draft: null, - }; - }, - - methods: { - async create() { - const { canceled, result } = await os.form(this.$ts.createNewClip, { - name: { - type: 'string', - label: this.$ts.name - }, - description: { - type: 'string', - required: false, - multiline: true, - label: this.$ts.description - }, - isPublic: { - type: 'boolean', - label: this.$ts.public, - default: false - } - }); - if (canceled) return; - - os.apiWithDialog('clips/create', result); - }, - - onClipCreated() { - this.$refs.list.reload(); - this.draft = null; - }, - - onClipDeleted() { - this.$refs.list.reload(); - }, - } -}); -</script> - -<style lang="scss" scoped> -.qtcaoidl { - > .add { - margin: 0 auto 16px auto; - } - - > ._content { - > .list { - > .item { - display: block; - padding: 16px; - - > .description { - margin-top: 8px; - padding-top: 8px; - border-top: solid 0.5px var(--divider); - } - } - } - } -} -</style> diff --git a/src/client/pages/my-groups/group.vue b/src/client/pages/my-groups/group.vue deleted file mode 100644 index bd5537cbfa..0000000000 --- a/src/client/pages/my-groups/group.vue +++ /dev/null @@ -1,184 +0,0 @@ -<template> -<div class="mk-group-page"> - <transition name="zoom" mode="out-in"> - <div v-if="group" class="_section"> - <div class="_content"> - <MkButton inline @click="invite()">{{ $ts.invite }}</MkButton> - <MkButton inline @click="renameGroup()">{{ $ts.rename }}</MkButton> - <MkButton inline @click="transfer()">{{ $ts.transfer }}</MkButton> - <MkButton inline @click="deleteGroup()">{{ $ts.delete }}</MkButton> - </div> - </div> - </transition> - - <transition name="zoom" mode="out-in"> - <div v-if="group" class="_section members _gap"> - <div class="_title">{{ $ts.members }}</div> - <div class="_content"> - <div class="users"> - <div class="user _panel" v-for="user in users" :key="user.id"> - <MkAvatar :user="user" class="avatar" :show-indicator="true"/> - <div class="body"> - <MkUserName :user="user" class="name"/> - <MkAcct :user="user" class="acct"/> - </div> - <div class="action"> - <button class="_button" @click="removeUser(user)"><i class="fas fa-times"></i></button> - </div> - </div> - </div> - </div> - </div> - </transition> -</div> -</template> - -<script lang="ts"> -import { computed, defineComponent } from 'vue'; -import Progress from '@client/scripts/loading'; -import MkButton from '@client/components/ui/button.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - MkButton - }, - - props: { - groupId: { - type: String, - required: true, - }, - }, - - data() { - return { - [symbols.PAGE_INFO]: computed(() => this.group ? { - title: this.group.name, - icon: 'fas fa-users', - } : null), - group: null, - users: [], - }; - }, - - watch: { - groupId: 'fetch', - }, - - created() { - this.fetch(); - }, - - methods: { - fetch() { - Progress.start(); - os.api('users/groups/show', { - groupId: this.groupId - }).then(group => { - this.group = group; - os.api('users/show', { - userIds: this.group.userIds - }).then(users => { - this.users = users; - Progress.done(); - }); - }); - }, - - invite() { - os.selectUser().then(user => { - os.apiWithDialog('users/groups/invite', { - groupId: this.group.id, - userId: user.id - }); - }); - }, - - removeUser(user) { - os.api('users/groups/pull', { - groupId: this.group.id, - userId: user.id - }).then(() => { - this.users = this.users.filter(x => x.id !== user.id); - }); - }, - - async renameGroup() { - const { canceled, result: name } = await os.dialog({ - title: this.$ts.groupName, - input: { - default: this.group.name - } - }); - if (canceled) return; - - await os.api('users/groups/update', { - groupId: this.group.id, - name: name - }); - - this.group.name = name; - }, - - transfer() { - os.selectUser().then(user => { - os.apiWithDialog('users/groups/transfer', { - groupId: this.group.id, - userId: user.id - }); - }); - }, - - async deleteGroup() { - const { canceled } = await os.dialog({ - type: 'warning', - text: this.$t('removeAreYouSure', { x: this.group.name }), - showCancelButton: true - }); - if (canceled) return; - - await os.apiWithDialog('users/groups/delete', { - groupId: this.group.id - }); - this.$router.push('/my/groups'); - } - } -}); -</script> - -<style lang="scss" scoped> -.mk-group-page { - > .members { - > ._content { - > .users { - > .user { - display: flex; - align-items: center; - padding: 16px; - - > .avatar { - width: 50px; - height: 50px; - } - - > .body { - flex: 1; - padding: 8px; - - > .name { - display: block; - font-weight: bold; - } - - > .acct { - opacity: 0.5; - } - } - } - } - } - } -} -</style> diff --git a/src/client/pages/my-groups/index.vue b/src/client/pages/my-groups/index.vue deleted file mode 100644 index 34f82f8a71..0000000000 --- a/src/client/pages/my-groups/index.vue +++ /dev/null @@ -1,121 +0,0 @@ -<template> -<div class=""> - <div class="_section" style="padding: 0;"> - <MkTab v-model="tab"> - <option value="owned">{{ $ts.ownedGroups }}</option> - <option value="joined">{{ $ts.joinedGroups }}</option> - <option value="invites"><i class="fas fa-envelope-open-text"></i> {{ $ts.invites }}</option> - </MkTab> - </div> - - <div class="_section"> - <div class="_content" v-if="tab === 'owned'"> - <MkButton @click="create" primary style="margin: 0 auto var(--margin) auto;"><i class="fas fa-plus"></i> {{ $ts.createGroup }}</MkButton> - - <MkPagination :pagination="ownedPagination" #default="{items}" ref="owned"> - <div class="_card" v-for="group in items" :key="group.id"> - <div class="_title"><MkA :to="`/my/groups/${ group.id }`" class="_link">{{ group.name }}</MkA></div> - <div class="_content"><MkAvatars :user-ids="group.userIds"/></div> - </div> - </MkPagination> - </div> - - <div class="_content" v-else-if="tab === 'joined'"> - <MkPagination :pagination="joinedPagination" #default="{items}" ref="joined"> - <div class="_card" v-for="group in items" :key="group.id"> - <div class="_title">{{ group.name }}</div> - <div class="_content"><MkAvatars :user-ids="group.userIds"/></div> - </div> - </MkPagination> - </div> - - <div class="_content" v-else-if="tab === 'invites'"> - <MkPagination :pagination="invitationPagination" #default="{items}" ref="invitations"> - <div class="_card" v-for="invitation in items" :key="invitation.id"> - <div class="_title">{{ invitation.group.name }}</div> - <div class="_content"><MkAvatars :user-ids="invitation.group.userIds"/></div> - <div class="_footer"> - <MkButton @click="acceptInvite(invitation)" primary inline><i class="fas fa-check"></i> {{ $ts.accept }}</MkButton> - <MkButton @click="rejectInvite(invitation)" primary inline><i class="fas fa-ban"></i> {{ $ts.reject }}</MkButton> - </div> - </div> - </MkPagination> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import MkPagination from '@client/components/ui/pagination.vue'; -import MkButton from '@client/components/ui/button.vue'; -import MkContainer from '@client/components/ui/container.vue'; -import MkAvatars from '@client/components/avatars.vue'; -import MkTab from '@client/components/tab.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - MkPagination, - MkButton, - MkContainer, - MkTab, - MkAvatars, - }, - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.groups, - icon: 'fas fa-users' - }, - tab: 'owned', - ownedPagination: { - endpoint: 'users/groups/owned', - limit: 10, - }, - joinedPagination: { - endpoint: 'users/groups/joined', - limit: 10, - }, - invitationPagination: { - endpoint: 'i/user-group-invites', - limit: 10, - }, - }; - }, - - methods: { - async create() { - const { canceled, result: name } = await os.dialog({ - title: this.$ts.groupName, - input: true - }); - if (canceled) return; - await os.api('users/groups/create', { name: name }); - this.$refs.owned.reload(); - os.success(); - }, - acceptInvite(invitation) { - os.api('users/groups/invitations/accept', { - invitationId: invitation.id - }).then(() => { - os.success(); - this.$refs.invitations.reload(); - this.$refs.joined.reload(); - }); - }, - rejectInvite(invitation) { - os.api('users/groups/invitations/reject', { - invitationId: invitation.id - }).then(() => { - this.$refs.invitations.reload(); - }); - } - } -}); -</script> - -<style lang="scss" scoped> -</style> diff --git a/src/client/pages/my-lists/index.vue b/src/client/pages/my-lists/index.vue deleted file mode 100644 index 687e9e630e..0000000000 --- a/src/client/pages/my-lists/index.vue +++ /dev/null @@ -1,88 +0,0 @@ -<template> -<div class="qkcjvfiv"> - <MkButton @click="create" primary class="add"><i class="fas fa-plus"></i> {{ $ts.createList }}</MkButton> - - <MkPagination :pagination="pagination" #default="{items}" class="lists _content" ref="list"> - <MkA v-for="list in items" :key="list.id" class="list _panel" :to="`/my/lists/${ list.id }`"> - <div class="name">{{ list.name }}</div> - <MkAvatars :user-ids="list.userIds"/> - </MkA> - </MkPagination> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import MkPagination from '@client/components/ui/pagination.vue'; -import MkButton from '@client/components/ui/button.vue'; -import MkAvatars from '@client/components/avatars.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - MkPagination, - MkButton, - MkAvatars, - }, - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.manageLists, - icon: 'fas fa-list-ul', - bg: 'var(--bg)', - action: { - icon: 'fas fa-plus', - handler: this.create - }, - }, - pagination: { - endpoint: 'users/lists/list', - limit: 10, - }, - }; - }, - - methods: { - async create() { - const { canceled, result: name } = await os.dialog({ - title: this.$ts.enterListName, - input: true - }); - if (canceled) return; - await os.api('users/lists/create', { name: name }); - this.$refs.list.reload(); - os.success(); - }, - } -}); -</script> - -<style lang="scss" scoped> -.qkcjvfiv { - padding: 16px; - - > .add { - margin: 0 auto var(--margin) auto; - } - - > .lists { - > .list { - display: block; - padding: 16px; - border: solid 1px var(--divider); - border-radius: 6px; - - &:hover { - border: solid 1px var(--accent); - text-decoration: none; - } - - > .name { - margin-bottom: 4px; - } - } - } -} -</style> diff --git a/src/client/pages/my-lists/list.vue b/src/client/pages/my-lists/list.vue deleted file mode 100644 index 049d370b4e..0000000000 --- a/src/client/pages/my-lists/list.vue +++ /dev/null @@ -1,170 +0,0 @@ -<template> -<div class="mk-list-page"> - <transition name="zoom" mode="out-in"> - <div v-if="list" class="_section"> - <div class="_content"> - <MkButton inline @click="addUser()">{{ $ts.addUser }}</MkButton> - <MkButton inline @click="renameList()">{{ $ts.rename }}</MkButton> - <MkButton inline @click="deleteList()">{{ $ts.delete }}</MkButton> - </div> - </div> - </transition> - - <transition name="zoom" mode="out-in"> - <div v-if="list" class="_section members _gap"> - <div class="_title">{{ $ts.members }}</div> - <div class="_content"> - <div class="users"> - <div class="user _panel" v-for="user in users" :key="user.id"> - <MkAvatar :user="user" class="avatar" :show-indicator="true"/> - <div class="body"> - <MkUserName :user="user" class="name"/> - <MkAcct :user="user" class="acct"/> - </div> - <div class="action"> - <button class="_button" @click="removeUser(user)"><i class="fas fa-times"></i></button> - </div> - </div> - </div> - </div> - </div> - </transition> -</div> -</template> - -<script lang="ts"> -import { computed, defineComponent } from 'vue'; -import Progress from '@client/scripts/loading'; -import MkButton from '@client/components/ui/button.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - MkButton - }, - - data() { - return { - [symbols.PAGE_INFO]: computed(() => this.list ? { - title: this.list.name, - icon: 'fas fa-list-ul', - } : null), - list: null, - users: [], - }; - }, - - watch: { - $route: 'fetch' - }, - - created() { - this.fetch(); - }, - - methods: { - fetch() { - Progress.start(); - os.api('users/lists/show', { - listId: this.$route.params.list - }).then(list => { - this.list = list; - os.api('users/show', { - userIds: this.list.userIds - }).then(users => { - this.users = users; - Progress.done(); - }); - }); - }, - - addUser() { - os.selectUser().then(user => { - os.apiWithDialog('users/lists/push', { - listId: this.list.id, - userId: user.id - }).then(() => { - this.users.push(user); - }); - }); - }, - - removeUser(user) { - os.api('users/lists/pull', { - listId: this.list.id, - userId: user.id - }).then(() => { - this.users = this.users.filter(x => x.id !== user.id); - }); - }, - - async renameList() { - const { canceled, result: name } = await os.dialog({ - title: this.$ts.enterListName, - input: { - default: this.list.name - } - }); - if (canceled) return; - - await os.api('users/lists/update', { - listId: this.list.id, - name: name - }); - - this.list.name = name; - }, - - async deleteList() { - const { canceled } = await os.dialog({ - type: 'warning', - text: this.$t('removeAreYouSure', { x: this.list.name }), - showCancelButton: true - }); - if (canceled) return; - - await os.api('users/lists/delete', { - listId: this.list.id - }); - os.success(); - this.$router.push('/my/lists'); - } - } -}); -</script> - -<style lang="scss" scoped> -.mk-list-page { - > .members { - > ._content { - > .users { - > .user { - display: flex; - align-items: center; - padding: 16px; - - > .avatar { - width: 50px; - height: 50px; - } - - > .body { - flex: 1; - padding: 8px; - - > .name { - display: block; - font-weight: bold; - } - - > .acct { - opacity: 0.5; - } - } - } - } - } - } -} -</style> diff --git a/src/client/pages/not-found.vue b/src/client/pages/not-found.vue deleted file mode 100644 index 5e7fe17f75..0000000000 --- a/src/client/pages/not-found.vue +++ /dev/null @@ -1,25 +0,0 @@ -<template> -<div class="ipledcug"> - <div class="_fullinfo"> - <img src="https://xn--931a.moe/assets/not-found.jpg" class="_ghost"/> - <div>{{ $ts.notFoundDescription }}</div> - </div> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.notFound, - icon: 'fas fa-exclamation-triangle' - }, - } - }, -}); -</script> diff --git a/src/client/pages/note.vue b/src/client/pages/note.vue deleted file mode 100644 index 8e95430d67..0000000000 --- a/src/client/pages/note.vue +++ /dev/null @@ -1,209 +0,0 @@ -<template> -<MkSpacer :content-max="800"> - <div class="fcuexfpr"> - <transition name="fade" mode="out-in"> - <div v-if="note" class="note"> - <div class="_gap" v-if="showNext"> - <XNotes class="_content" :pagination="next" :no-gap="true"/> - </div> - - <div class="main _gap"> - <MkButton v-if="!showNext && hasNext" class="load next" @click="showNext = true"><i class="fas fa-chevron-up"></i></MkButton> - <div class="note _gap"> - <MkRemoteCaution v-if="note.user.host != null" :href="note.url || note.uri" class="_isolated"/> - <XNoteDetailed v-model:note="note" :key="note.id" class="_isolated note"/> - </div> - <div class="_content clips _gap" v-if="clips && clips.length > 0"> - <div class="title">{{ $ts.clip }}</div> - <MkA v-for="item in clips" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _gap"> - <b>{{ item.name }}</b> - <div v-if="item.description" class="description">{{ item.description }}</div> - <div class="user"> - <MkAvatar :user="item.user" class="avatar" :show-indicator="true"/> <MkUserName :user="item.user" :nowrap="false"/> - </div> - </MkA> - </div> - <MkButton v-if="!showPrev && hasPrev" class="load prev" @click="showPrev = true"><i class="fas fa-chevron-down"></i></MkButton> - </div> - - <div class="_gap" v-if="showPrev"> - <XNotes class="_content" :pagination="prev" :no-gap="true"/> - </div> - </div> - <MkError v-else-if="error" @retry="fetch()"/> - <MkLoading v-else/> - </transition> - </div> -</MkSpacer> -</template> - -<script lang="ts"> -import { computed, defineComponent } from 'vue'; -import XNote from '@client/components/note.vue'; -import XNoteDetailed from '@client/components/note-detailed.vue'; -import XNotes from '@client/components/notes.vue'; -import MkRemoteCaution from '@client/components/remote-caution.vue'; -import MkButton from '@client/components/ui/button.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - XNote, - XNoteDetailed, - XNotes, - MkRemoteCaution, - MkButton, - }, - props: { - noteId: { - type: String, - required: true - } - }, - data() { - return { - [symbols.PAGE_INFO]: computed(() => this.note ? { - title: this.$ts.note, - subtitle: new Date(this.note.createdAt).toLocaleString(), - avatar: this.note.user, - path: `/notes/${this.note.id}`, - share: { - title: this.$t('noteOf', { user: this.note.user.name }), - text: this.note.text, - }, - bg: 'var(--bg)', - } : null), - note: null, - clips: null, - hasPrev: false, - hasNext: false, - showPrev: false, - showNext: false, - error: null, - prev: { - endpoint: 'users/notes', - limit: 10, - params: init => ({ - userId: this.note.userId, - untilId: this.note.id, - }) - }, - next: { - reversed: true, - endpoint: 'users/notes', - limit: 10, - params: init => ({ - userId: this.note.userId, - sinceId: this.note.id, - }) - }, - }; - }, - watch: { - noteId: 'fetch' - }, - created() { - this.fetch(); - }, - methods: { - fetch() { - this.note = null; - os.api('notes/show', { - noteId: this.noteId - }).then(note => { - this.note = note; - Promise.all([ - os.api('notes/clips', { - noteId: note.id, - }), - os.api('users/notes', { - userId: note.userId, - untilId: note.id, - limit: 1, - }), - os.api('users/notes', { - userId: note.userId, - sinceId: note.id, - limit: 1, - }), - ]).then(([clips, prev, next]) => { - this.clips = clips; - this.hasPrev = prev.length !== 0; - this.hasNext = next.length !== 0; - }); - }).catch(e => { - this.error = e; - }); - } - } -}); -</script> - -<style lang="scss" scoped> -.fade-enter-active, -.fade-leave-active { - transition: opacity 0.125s ease; -} -.fade-enter-from, -.fade-leave-to { - opacity: 0; -} - -.fcuexfpr { - background: var(--bg); - - > .note { - > .main { - > .load { - min-width: 0; - margin: 0 auto; - border-radius: 999px; - - &.next { - margin-bottom: var(--margin); - } - - &.prev { - margin-top: var(--margin); - } - } - - > .note { - > .note { - border-radius: var(--radius); - background: var(--panel); - } - } - - > .clips { - > .title { - font-weight: bold; - padding: 12px; - } - - > .item { - display: block; - padding: 16px; - - > .description { - padding: 8px 0; - } - - > .user { - $height: 32px; - padding-top: 16px; - border-top: solid 0.5px var(--divider); - line-height: $height; - - > .avatar { - width: $height; - height: $height; - } - } - } - } - } - } -} -</style> diff --git a/src/client/pages/notifications.vue b/src/client/pages/notifications.vue deleted file mode 100644 index 8d6adec48d..0000000000 --- a/src/client/pages/notifications.vue +++ /dev/null @@ -1,88 +0,0 @@ -<template> -<MkSpacer :content-max="800"> - <div class="clupoqwt"> - <XNotifications class="notifications" @before="before" @after="after" :include-types="includeTypes" :unread-only="tab === 'unread'"/> - </div> -</MkSpacer> -</template> - -<script lang="ts"> -import { computed, defineComponent } from 'vue'; -import Progress from '@client/scripts/loading'; -import XNotifications from '@client/components/notifications.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; -import { notificationTypes } from '@/types'; - -export default defineComponent({ - components: { - XNotifications - }, - - data() { - return { - [symbols.PAGE_INFO]: computed(() => ({ - title: this.$ts.notifications, - icon: 'fas fa-bell', - bg: 'var(--bg)', - actions: [{ - text: this.$ts.filter, - icon: 'fas fa-filter', - highlighted: this.includeTypes != null, - handler: this.setFilter, - }, { - text: this.$ts.markAllAsRead, - icon: 'fas fa-check', - handler: () => { - os.apiWithDialog('notifications/mark-all-as-read'); - }, - }], - tabs: [{ - active: this.tab === 'all', - title: this.$ts.all, - onClick: () => { this.tab = 'all'; }, - }, { - active: this.tab === 'unread', - title: this.$ts.unread, - onClick: () => { this.tab = 'unread'; }, - },] - })), - tab: 'all', - includeTypes: null, - }; - }, - - methods: { - before() { - Progress.start(); - }, - - after() { - Progress.done(); - }, - - setFilter(ev) { - const typeItems = notificationTypes.map(t => ({ - text: this.$t(`_notification._types.${t}`), - active: this.includeTypes && this.includeTypes.includes(t), - action: () => { - this.includeTypes = [t]; - } - })); - const items = this.includeTypes != null ? [{ - icon: 'fas fa-times', - text: this.$ts.clear, - action: () => { - this.includeTypes = null; - } - }, null, ...typeItems] : typeItems; - os.popupMenu(items, ev.currentTarget || ev.target); - } - } -}); -</script> - -<style lang="scss" scoped> -.clupoqwt { -} -</style> diff --git a/src/client/pages/page-editor/els/page-editor.el.button.vue b/src/client/pages/page-editor/els/page-editor.el.button.vue deleted file mode 100644 index 85e9d7e711..0000000000 --- a/src/client/pages/page-editor/els/page-editor.el.button.vue +++ /dev/null @@ -1,84 +0,0 @@ -<template> -<XContainer @remove="() => $emit('remove')" :draggable="true"> - <template #header><i class="fas fa-bolt"></i> {{ $ts._pages.blocks.button }}</template> - - <section class="xfhsjczc"> - <MkInput v-model="value.text"><template #label>{{ $ts._pages.blocks._button.text }}</template></MkInput> - <MkSwitch v-model="value.primary"><span>{{ $ts._pages.blocks._button.colored }}</span></MkSwitch> - <MkSelect v-model="value.action"> - <template #label>{{ $ts._pages.blocks._button.action }}</template> - <option value="dialog">{{ $ts._pages.blocks._button._action.dialog }}</option> - <option value="resetRandom">{{ $ts._pages.blocks._button._action.resetRandom }}</option> - <option value="pushEvent">{{ $ts._pages.blocks._button._action.pushEvent }}</option> - <option value="callAiScript">{{ $ts._pages.blocks._button._action.callAiScript }}</option> - </MkSelect> - <template v-if="value.action === 'dialog'"> - <MkInput v-model="value.content"><template #label>{{ $ts._pages.blocks._button._action._dialog.content }}</template></MkInput> - </template> - <template v-else-if="value.action === 'pushEvent'"> - <MkInput v-model="value.event"><template #label>{{ $ts._pages.blocks._button._action._pushEvent.event }}</template></MkInput> - <MkInput v-model="value.message"><template #label>{{ $ts._pages.blocks._button._action._pushEvent.message }}</template></MkInput> - <MkSelect v-model="value.var"> - <template #label>{{ $ts._pages.blocks._button._action._pushEvent.variable }}</template> - <option :value="null">{{ $t('_pages.blocks._button._action._pushEvent.no-variable') }}</option> - <option v-for="v in hpml.getVarsByType()" :value="v.name">{{ v.name }}</option> - <optgroup :label="$ts._pages.script.pageVariables"> - <option v-for="v in hpml.getPageVarsByType()" :value="v">{{ v }}</option> - </optgroup> - <optgroup :label="$ts._pages.script.enviromentVariables"> - <option v-for="v in hpml.getEnvVarsByType()" :value="v">{{ v }}</option> - </optgroup> - </MkSelect> - </template> - <template v-else-if="value.action === 'callAiScript'"> - <MkInput v-model="value.fn"><template #label>{{ $ts._pages.blocks._button._action._callAiScript.functionName }}</template></MkInput> - </template> - </section> -</XContainer> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import XContainer from '../page-editor.container.vue'; -import MkSelect from '@client/components/form/select.vue'; -import MkInput from '@client/components/form/input.vue'; -import MkSwitch from '@client/components/form/switch.vue'; -import * as os from '@client/os'; - -export default defineComponent({ - components: { - XContainer, MkSelect, MkInput, MkSwitch - }, - - props: { - value: { - required: true - }, - hpml: { - required: true, - }, - }, - - data() { - return { - }; - }, - - created() { - if (this.value.text == null) this.value.text = ''; - if (this.value.action == null) this.value.action = 'dialog'; - if (this.value.content == null) this.value.content = null; - if (this.value.event == null) this.value.event = null; - if (this.value.message == null) this.value.message = null; - if (this.value.primary == null) this.value.primary = false; - if (this.value.var == null) this.value.var = null; - if (this.value.fn == null) this.value.fn = null; - }, -}); -</script> - -<style lang="scss" scoped> -.xfhsjczc { - padding: 0 16px 0 16px; -} -</style> diff --git a/src/client/pages/page-editor/els/page-editor.el.canvas.vue b/src/client/pages/page-editor/els/page-editor.el.canvas.vue deleted file mode 100644 index c40d69a7c1..0000000000 --- a/src/client/pages/page-editor/els/page-editor.el.canvas.vue +++ /dev/null @@ -1,50 +0,0 @@ -<template> -<XContainer @remove="() => $emit('remove')" :draggable="true"> - <template #header><i class="fas fa-paint-brush"></i> {{ $ts._pages.blocks.canvas }}</template> - - <section style="padding: 0 16px 0 16px;"> - <MkInput v-model="value.name"> - <template #prefix><i class="fas fa-magic"></i></template> - <template #label>{{ $ts._pages.blocks._canvas.id }}</template> - </MkInput> - <MkInput v-model="value.width" type="number"> - <template #label>{{ $ts._pages.blocks._canvas.width }}</template> - <template #suffix>px</template> - </MkInput> - <MkInput v-model="value.height" type="number"> - <template #label>{{ $ts._pages.blocks._canvas.height }}</template> - <template #suffix>px</template> - </MkInput> - </section> -</XContainer> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import XContainer from '../page-editor.container.vue'; -import MkInput from '@client/components/form/input.vue'; -import * as os from '@client/os'; - -export default defineComponent({ - components: { - XContainer, MkInput - }, - - props: { - value: { - required: true - }, - }, - - data() { - return { - }; - }, - - created() { - if (this.value.name == null) this.value.name = ''; - if (this.value.width == null) this.value.width = 300; - if (this.value.height == null) this.value.height = 200; - }, -}); -</script> diff --git a/src/client/pages/page-editor/els/page-editor.el.counter.vue b/src/client/pages/page-editor/els/page-editor.el.counter.vue deleted file mode 100644 index de7994e3ba..0000000000 --- a/src/client/pages/page-editor/els/page-editor.el.counter.vue +++ /dev/null @@ -1,46 +0,0 @@ -<template> -<XContainer @remove="() => $emit('remove')" :draggable="true"> - <template #header><i class="fas fa-bolt"></i> {{ $ts._pages.blocks.counter }}</template> - - <section style="padding: 0 16px 0 16px;"> - <MkInput v-model="value.name"> - <template #prefix><i class="fas fa-magic"></i></template> - <template #label>{{ $ts._pages.blocks._counter.name }}</template> - </MkInput> - <MkInput v-model="value.text"> - <template #label>{{ $ts._pages.blocks._counter.text }}</template> - </MkInput> - <MkInput v-model="value.inc" type="number"> - <template #label>{{ $ts._pages.blocks._counter.inc }}</template> - </MkInput> - </section> -</XContainer> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import XContainer from '../page-editor.container.vue'; -import MkInput from '@client/components/form/input.vue'; -import * as os from '@client/os'; - -export default defineComponent({ - components: { - XContainer, MkInput - }, - - props: { - value: { - required: true - }, - }, - - data() { - return { - }; - }, - - created() { - if (this.value.name == null) this.value.name = ''; - }, -}); -</script> diff --git a/src/client/pages/page-editor/els/page-editor.el.if.vue b/src/client/pages/page-editor/els/page-editor.el.if.vue deleted file mode 100644 index 52f4dac22e..0000000000 --- a/src/client/pages/page-editor/els/page-editor.el.if.vue +++ /dev/null @@ -1,84 +0,0 @@ -<template> -<XContainer @remove="() => $emit('remove')" :draggable="true"> - <template #header><i class="fas fa-question"></i> {{ $ts._pages.blocks.if }}</template> - <template #func> - <button @click="add()" class="_button"> - <i class="fas fa-plus"></i> - </button> - </template> - - <section class="romcojzs"> - <MkSelect v-model="value.var"> - <template #label>{{ $ts._pages.blocks._if.variable }}</template> - <option v-for="v in hpml.getVarsByType('boolean')" :value="v.name">{{ v.name }}</option> - <optgroup :label="$ts._pages.script.pageVariables"> - <option v-for="v in hpml.getPageVarsByType('boolean')" :value="v">{{ v }}</option> - </optgroup> - <optgroup :label="$ts._pages.script.enviromentVariables"> - <option v-for="v in hpml.getEnvVarsByType('boolean')" :value="v">{{ v }}</option> - </optgroup> - </MkSelect> - - <XBlocks class="children" v-model="value.children" :hpml="hpml"/> - </section> -</XContainer> -</template> - -<script lang="ts"> -import { defineComponent, defineAsyncComponent } from 'vue'; -import { v4 as uuid } from 'uuid'; -import XContainer from '../page-editor.container.vue'; -import MkSelect from '@client/components/form/select.vue'; -import * as os from '@client/os'; - -export default defineComponent({ - components: { - XContainer, MkSelect, - XBlocks: defineAsyncComponent(() => import('../page-editor.blocks.vue')), - }, - - inject: ['getPageBlockList'], - - props: { - value: { - required: true - }, - hpml: { - required: true, - }, - }, - - data() { - return { - }; - }, - - created() { - if (this.value.children == null) this.value.children = []; - if (this.value.var === undefined) this.value.var = null; - }, - - methods: { - async add() { - const { canceled, result: type } = await os.dialog({ - type: null, - title: this.$ts._pages.chooseBlock, - select: { - groupedItems: this.getPageBlockList() - }, - showCancelButton: true - }); - if (canceled) return; - - const id = uuid(); - this.value.children.push({ id, type }); - }, - } -}); -</script> - -<style lang="scss" scoped> -.romcojzs { - padding: 0 16px 16px 16px; -} -</style> diff --git a/src/client/pages/page-editor/els/page-editor.el.image.vue b/src/client/pages/page-editor/els/page-editor.el.image.vue deleted file mode 100644 index d96879f50d..0000000000 --- a/src/client/pages/page-editor/els/page-editor.el.image.vue +++ /dev/null @@ -1,72 +0,0 @@ -<template> -<XContainer @remove="() => $emit('remove')" :draggable="true"> - <template #header><i class="fas fa-image"></i> {{ $ts._pages.blocks.image }}</template> - <template #func> - <button @click="choose()"> - <i class="fas fa-folder-open"></i> - </button> - </template> - - <section class="oyyftmcf"> - <MkDriveFileThumbnail class="preview" v-if="file" :file="file" fit="contain" @click="choose()"/> - </section> -</XContainer> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import XContainer from '../page-editor.container.vue'; -import MkDriveFileThumbnail from '@client/components/drive-file-thumbnail.vue'; -import * as os from '@client/os'; - -export default defineComponent({ - components: { - XContainer, MkDriveFileThumbnail - }, - - props: { - value: { - required: true - }, - }, - - data() { - return { - file: null, - }; - }, - - created() { - if (this.value.fileId === undefined) this.value.fileId = null; - }, - - mounted() { - if (this.value.fileId == null) { - this.choose(); - } else { - os.api('drive/files/show', { - fileId: this.value.fileId - }).then(file => { - this.file = file; - }); - } - }, - - methods: { - async choose() { - os.selectDriveFile(false).then(file => { - this.file = file; - this.value.fileId = file.id; - }); - }, - } -}); -</script> - -<style lang="scss" scoped> -.oyyftmcf { - > .preview { - height: 150px; - } -} -</style> diff --git a/src/client/pages/page-editor/els/page-editor.el.note.vue b/src/client/pages/page-editor/els/page-editor.el.note.vue deleted file mode 100644 index 9feec395b7..0000000000 --- a/src/client/pages/page-editor/els/page-editor.el.note.vue +++ /dev/null @@ -1,65 +0,0 @@ -<template> -<XContainer @remove="() => $emit('remove')" :draggable="true"> - <template #header><i class="fas fa-sticky-note"></i> {{ $ts._pages.blocks.note }}</template> - - <section style="padding: 0 16px 0 16px;"> - <MkInput v-model="id"> - <template #label>{{ $ts._pages.blocks._note.id }}</template> - <template #caption>{{ $ts._pages.blocks._note.idDescription }}</template> - </MkInput> - <MkSwitch v-model="value.detailed"><span>{{ $ts._pages.blocks._note.detailed }}</span></MkSwitch> - - <XNote v-if="note && !value.detailed" v-model:note="note" :key="note.id + ':normal'" style="margin-bottom: 16px;"/> - <XNoteDetailed v-if="note && value.detailed" v-model:note="note" :key="note.id + ':detail'" style="margin-bottom: 16px;"/> - </section> -</XContainer> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import XContainer from '../page-editor.container.vue'; -import MkInput from '@client/components/form/input.vue'; -import MkSwitch from '@client/components/form/switch.vue'; -import XNote from '@client/components/note.vue'; -import XNoteDetailed from '@client/components/note-detailed.vue'; -import * as os from '@client/os'; - -export default defineComponent({ - components: { - XContainer, MkInput, MkSwitch, XNote, XNoteDetailed, - }, - - props: { - value: { - required: true - }, - }, - - data() { - return { - id: this.value.note, - note: null, - }; - }, - - watch: { - id: { - async handler() { - if (this.id && (this.id.startsWith('http://') || this.id.startsWith('https://'))) { - this.value.note = this.id.endsWith('/') ? this.id.substr(0, this.id.length - 1).split('/').pop() : this.id.split('/').pop(); - } else { - this.value.note = this.id; - } - - this.note = await os.api('notes/show', { noteId: this.value.note }); - }, - immediate: true - }, - }, - - created() { - if (this.value.note == null) this.value.note = null; - if (this.value.detailed == null) this.value.detailed = false; - }, -}); -</script> diff --git a/src/client/pages/page-editor/els/page-editor.el.number-input.vue b/src/client/pages/page-editor/els/page-editor.el.number-input.vue deleted file mode 100644 index 57b1397824..0000000000 --- a/src/client/pages/page-editor/els/page-editor.el.number-input.vue +++ /dev/null @@ -1,46 +0,0 @@ -<template> -<XContainer @remove="() => $emit('remove')" :draggable="true"> - <template #header><i class="fas fa-bolt"></i> {{ $ts._pages.blocks.numberInput }}</template> - - <section style="padding: 0 16px 0 16px;"> - <MkInput v-model="value.name"> - <template #prefix><i class="fas fa-magic"></i></template> - <template #label>{{ $ts._pages.blocks._numberInput.name }}</template> - </MkInput> - <MkInput v-model="value.text"> - <template #label>{{ $ts._pages.blocks._numberInput.text }}</template> - </MkInput> - <MkInput v-model="value.default" type="number"> - <template #label>{{ $ts._pages.blocks._numberInput.default }}</template> - </MkInput> - </section> -</XContainer> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import XContainer from '../page-editor.container.vue'; -import MkInput from '@client/components/form/input.vue'; -import * as os from '@client/os'; - -export default defineComponent({ - components: { - XContainer, MkInput - }, - - props: { - value: { - required: true - }, - }, - - data() { - return { - }; - }, - - created() { - if (this.value.name == null) this.value.name = ''; - }, -}); -</script> diff --git a/src/client/pages/page-editor/els/page-editor.el.post.vue b/src/client/pages/page-editor/els/page-editor.el.post.vue deleted file mode 100644 index e21ccfd345..0000000000 --- a/src/client/pages/page-editor/els/page-editor.el.post.vue +++ /dev/null @@ -1,43 +0,0 @@ -<template> -<XContainer @remove="() => $emit('remove')" :draggable="true"> - <template #header><i class="fas fa-paper-plane"></i> {{ $ts._pages.blocks.post }}</template> - - <section style="padding: 16px;"> - <MkTextarea v-model="value.text"><template #label>{{ $ts._pages.blocks._post.text }}</template></MkTextarea> - <MkSwitch v-model="value.attachCanvasImage"><span>{{ $ts._pages.blocks._post.attachCanvasImage }}</span></MkSwitch> - <MkInput v-if="value.attachCanvasImage" v-model="value.canvasId"><template #label>{{ $ts._pages.blocks._post.canvasId }}</template></MkInput> - </section> -</XContainer> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import XContainer from '../page-editor.container.vue'; -import MkTextarea from '@client/components/form/textarea.vue'; -import MkInput from '@client/components/form/input.vue'; -import MkSwitch from '@client/components/form/switch.vue'; -import * as os from '@client/os'; - -export default defineComponent({ - components: { - XContainer, MkTextarea, MkInput, MkSwitch - }, - - props: { - value: { - required: true - }, - }, - - data() { - return { - }; - }, - - created() { - if (this.value.text == null) this.value.text = ''; - if (this.value.attachCanvasImage == null) this.value.attachCanvasImage = false; - if (this.value.canvasId == null) this.value.canvasId = ''; - }, -}); -</script> diff --git a/src/client/pages/page-editor/els/page-editor.el.radio-button.vue b/src/client/pages/page-editor/els/page-editor.el.radio-button.vue deleted file mode 100644 index 62fb231f79..0000000000 --- a/src/client/pages/page-editor/els/page-editor.el.radio-button.vue +++ /dev/null @@ -1,50 +0,0 @@ -<template> -<XContainer @remove="() => $emit('remove')" :draggable="true"> - <template #header><i class="fas fa-bolt"></i> {{ $ts._pages.blocks.radioButton }}</template> - - <section style="padding: 0 16px 16px 16px;"> - <MkInput v-model="value.name"><template #prefix><i class="fas fa-magic"></i></template><template #label>{{ $ts._pages.blocks._radioButton.name }}</template></MkInput> - <MkInput v-model="value.title"><template #label>{{ $ts._pages.blocks._radioButton.title }}</template></MkInput> - <MkTextarea v-model="values"><template #label>{{ $ts._pages.blocks._radioButton.values }}</template></MkTextarea> - <MkInput v-model="value.default"><template #label>{{ $ts._pages.blocks._radioButton.default }}</template></MkInput> - </section> -</XContainer> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import XContainer from '../page-editor.container.vue'; -import MkTextarea from '@client/components/form/textarea.vue'; -import MkInput from '@client/components/form/input.vue'; -import * as os from '@client/os'; - -export default defineComponent({ - components: { - XContainer, MkTextarea, MkInput - }, - props: { - value: { - required: true - }, - }, - data() { - return { - values: '', - }; - }, - watch: { - values: { - handler() { - this.value.values = this.values.split('\n'); - }, - deep: true - } - }, - created() { - if (this.value.name == null) this.value.name = ''; - if (this.value.title == null) this.value.title = ''; - if (this.value.values == null) this.value.values = []; - this.values = this.value.values.join('\n'); - }, -}); -</script> diff --git a/src/client/pages/page-editor/els/page-editor.el.section.vue b/src/client/pages/page-editor/els/page-editor.el.section.vue deleted file mode 100644 index 75bdf120c0..0000000000 --- a/src/client/pages/page-editor/els/page-editor.el.section.vue +++ /dev/null @@ -1,96 +0,0 @@ -<template> -<XContainer @remove="() => $emit('remove')" :draggable="true"> - <template #header><i class="fas fa-sticky-note"></i> {{ value.title }}</template> - <template #func> - <button @click="rename()" class="_button"> - <i class="fas fa-pencil-alt"></i> - </button> - <button @click="add()" class="_button"> - <i class="fas fa-plus"></i> - </button> - </template> - - <section class="ilrvjyvi"> - <XBlocks class="children" v-model="value.children" :hpml="hpml"/> - </section> -</XContainer> -</template> - -<script lang="ts"> -import { defineComponent, defineAsyncComponent } from 'vue'; -import { v4 as uuid } from 'uuid'; -import XContainer from '../page-editor.container.vue'; -import * as os from '@client/os'; - -export default defineComponent({ - components: { - XContainer, - XBlocks: defineAsyncComponent(() => import('../page-editor.blocks.vue')), - }, - - inject: ['getPageBlockList'], - - props: { - value: { - required: true - }, - hpml: { - required: true, - }, - }, - - data() { - return { - }; - }, - - created() { - if (this.value.title == null) this.value.title = null; - if (this.value.children == null) this.value.children = []; - }, - - mounted() { - if (this.value.title == null) { - this.rename(); - } - }, - - methods: { - async rename() { - const { canceled, result: title } = await os.dialog({ - title: 'Enter title', - input: { - type: 'text', - default: this.value.title - }, - showCancelButton: true - }); - if (canceled) return; - this.value.title = title; - }, - - async add() { - const { canceled, result: type } = await os.dialog({ - type: null, - title: this.$ts._pages.chooseBlock, - select: { - groupedItems: this.getPageBlockList() - }, - showCancelButton: true - }); - if (canceled) return; - - const id = uuid(); - this.value.children.push({ id, type }); - }, - } -}); -</script> - -<style lang="scss" scoped> -.ilrvjyvi { - > .children { - padding: 16px; - } -} -</style> diff --git a/src/client/pages/page-editor/els/page-editor.el.switch.vue b/src/client/pages/page-editor/els/page-editor.el.switch.vue deleted file mode 100644 index cf15f58c82..0000000000 --- a/src/client/pages/page-editor/els/page-editor.el.switch.vue +++ /dev/null @@ -1,46 +0,0 @@ -<template> -<XContainer @remove="() => $emit('remove')" :draggable="true"> - <template #header><i class="fas fa-bolt"></i> {{ $ts._pages.blocks.switch }}</template> - - <section class="kjuadyyj"> - <MkInput v-model="value.name"><template #prefix><i class="fas fa-magic"></i></template><template #label>{{ $ts._pages.blocks._switch.name }}</template></MkInput> - <MkInput v-model="value.text"><template #label>{{ $ts._pages.blocks._switch.text }}</template></MkInput> - <MkSwitch v-model="value.default"><span>{{ $ts._pages.blocks._switch.default }}</span></MkSwitch> - </section> -</XContainer> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import XContainer from '../page-editor.container.vue'; -import MkSwitch from '@client/components/form/switch.vue'; -import MkInput from '@client/components/form/input.vue'; -import * as os from '@client/os'; - -export default defineComponent({ - components: { - XContainer, MkSwitch, MkInput - }, - - props: { - value: { - required: true - }, - }, - - data() { - return { - }; - }, - - created() { - if (this.value.name == null) this.value.name = ''; - }, -}); -</script> - -<style lang="scss" scoped> -.kjuadyyj { - padding: 0 16px 16px 16px; -} -</style> diff --git a/src/client/pages/page-editor/els/page-editor.el.text-input.vue b/src/client/pages/page-editor/els/page-editor.el.text-input.vue deleted file mode 100644 index 210199befd..0000000000 --- a/src/client/pages/page-editor/els/page-editor.el.text-input.vue +++ /dev/null @@ -1,39 +0,0 @@ -<template> -<XContainer @remove="() => $emit('remove')" :draggable="true"> - <template #header><i class="fas fa-bolt"></i> {{ $ts._pages.blocks.textInput }}</template> - - <section style="padding: 0 16px 0 16px;"> - <MkInput v-model="value.name"><template #prefix><i class="fas fa-magic"></i></template><template #label>{{ $ts._pages.blocks._textInput.name }}</template></MkInput> - <MkInput v-model="value.text"><template #label>{{ $ts._pages.blocks._textInput.text }}</template></MkInput> - <MkInput v-model="value.default" type="text"><template #label>{{ $ts._pages.blocks._textInput.default }}</template></MkInput> - </section> -</XContainer> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import XContainer from '../page-editor.container.vue'; -import MkInput from '@client/components/form/input.vue'; -import * as os from '@client/os'; - -export default defineComponent({ - components: { - XContainer, MkInput - }, - - props: { - value: { - required: true - }, - }, - - data() { - return { - }; - }, - - created() { - if (this.value.name == null) this.value.name = ''; - }, -}); -</script> diff --git a/src/client/pages/page-editor/els/page-editor.el.text.vue b/src/client/pages/page-editor/els/page-editor.el.text.vue deleted file mode 100644 index 668dd5f52d..0000000000 --- a/src/client/pages/page-editor/els/page-editor.el.text.vue +++ /dev/null @@ -1,57 +0,0 @@ -<template> -<XContainer @remove="() => $emit('remove')" :draggable="true"> - <template #header><i class="fas fa-align-left"></i> {{ $ts._pages.blocks.text }}</template> - - <section class="vckmsadr"> - <textarea v-model="value.text"></textarea> - </section> -</XContainer> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import XContainer from '../page-editor.container.vue'; -import * as os from '@client/os'; - -export default defineComponent({ - components: { - XContainer - }, - - props: { - value: { - required: true - }, - }, - - data() { - return { - }; - }, - - created() { - if (this.value.text == null) this.value.text = ''; - }, -}); -</script> - -<style lang="scss" scoped> -.vckmsadr { - > textarea { - display: block; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - width: 100%; - min-width: 100%; - min-height: 150px; - border: none; - box-shadow: none; - padding: 16px; - background: transparent; - color: var(--fg); - font-size: 14px; - box-sizing: border-box; - } -} -</style> diff --git a/src/client/pages/page-editor/els/page-editor.el.textarea-input.vue b/src/client/pages/page-editor/els/page-editor.el.textarea-input.vue deleted file mode 100644 index 14f36db2a1..0000000000 --- a/src/client/pages/page-editor/els/page-editor.el.textarea-input.vue +++ /dev/null @@ -1,40 +0,0 @@ -<template> -<XContainer @remove="() => $emit('remove')" :draggable="true"> - <template #header><i class="fas fa-bolt"></i> {{ $ts._pages.blocks.textareaInput }}</template> - - <section style="padding: 0 16px 16px 16px;"> - <MkInput v-model="value.name"><template #prefix><i class="fas fa-magic"></i></template><template #label>{{ $ts._pages.blocks._textareaInput.name }}</template></MkInput> - <MkInput v-model="value.text"><template #label>{{ $ts._pages.blocks._textareaInput.text }}</template></MkInput> - <MkTextarea v-model="value.default"><template #label>{{ $ts._pages.blocks._textareaInput.default }}</template></MkTextarea> - </section> -</XContainer> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import XContainer from '../page-editor.container.vue'; -import MkTextarea from '@client/components/form/textarea.vue'; -import MkInput from '@client/components/form/input.vue'; -import * as os from '@client/os'; - -export default defineComponent({ - components: { - XContainer, MkTextarea, MkInput - }, - - props: { - value: { - required: true - }, - }, - - data() { - return { - }; - }, - - created() { - if (this.value.name == null) this.value.name = ''; - }, -}); -</script> diff --git a/src/client/pages/page-editor/els/page-editor.el.textarea.vue b/src/client/pages/page-editor/els/page-editor.el.textarea.vue deleted file mode 100644 index a29d5bd3f2..0000000000 --- a/src/client/pages/page-editor/els/page-editor.el.textarea.vue +++ /dev/null @@ -1,57 +0,0 @@ -<template> -<XContainer @remove="() => $emit('remove')" :draggable="true"> - <template #header><i class="fas fa-align-left"></i> {{ $ts._pages.blocks.textarea }}</template> - - <section class="ihymsbbe"> - <textarea v-model="value.text"></textarea> - </section> -</XContainer> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import XContainer from '../page-editor.container.vue'; -import * as os from '@client/os'; - -export default defineComponent({ - components: { - XContainer - }, - - props: { - value: { - required: true - }, - }, - - data() { - return { - }; - }, - - created() { - if (this.value.text == null) this.value.text = ''; - }, -}); -</script> - -<style lang="scss" scoped> -.ihymsbbe { - > textarea { - display: block; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - width: 100%; - min-width: 100%; - min-height: 150px; - border: none; - box-shadow: none; - padding: 16px; - background: transparent; - color: var(--fg); - font-size: 14px; - box-sizing: border-box; - } -} -</style> diff --git a/src/client/pages/page-editor/page-editor.blocks.vue b/src/client/pages/page-editor/page-editor.blocks.vue deleted file mode 100644 index c27162a26e..0000000000 --- a/src/client/pages/page-editor/page-editor.blocks.vue +++ /dev/null @@ -1,78 +0,0 @@ -<template> -<XDraggable tag="div" v-model="blocks" item-key="id" handle=".drag-handle" :group="{ name: 'blocks' }" animation="150" swap-threshold="0.5"> - <template #item="{element}"> - <component :is="'x-' + element.type" :value="element" @update:value="updateItem" @remove="() => removeItem(element)" :hpml="hpml"/> - </template> -</XDraggable> -</template> - -<script lang="ts"> -import { defineComponent, defineAsyncComponent } from 'vue'; -import XSection from './els/page-editor.el.section.vue'; -import XText from './els/page-editor.el.text.vue'; -import XTextarea from './els/page-editor.el.textarea.vue'; -import XImage from './els/page-editor.el.image.vue'; -import XButton from './els/page-editor.el.button.vue'; -import XTextInput from './els/page-editor.el.text-input.vue'; -import XTextareaInput from './els/page-editor.el.textarea-input.vue'; -import XNumberInput from './els/page-editor.el.number-input.vue'; -import XSwitch from './els/page-editor.el.switch.vue'; -import XIf from './els/page-editor.el.if.vue'; -import XPost from './els/page-editor.el.post.vue'; -import XCounter from './els/page-editor.el.counter.vue'; -import XRadioButton from './els/page-editor.el.radio-button.vue'; -import XCanvas from './els/page-editor.el.canvas.vue'; -import XNote from './els/page-editor.el.note.vue'; -import * as os from '@client/os'; - -export default defineComponent({ - components: { - XDraggable: defineAsyncComponent(() => import('vuedraggable').then(x => x.default)), - XSection, XText, XImage, XButton, XTextarea, XTextInput, XTextareaInput, XNumberInput, XSwitch, XIf, XPost, XCounter, XRadioButton, XCanvas, XNote - }, - - props: { - modelValue: { - type: Array, - required: true - }, - hpml: { - required: true, - }, - }, - - emits: ['update:modelValue'], - - computed: { - blocks: { - get() { - return this.modelValue; - }, - set(value) { - this.$emit('update:modelValue', value); - } - } - }, - - methods: { - updateItem(v) { - const i = this.blocks.findIndex(x => x.id === v.id); - const newValue = [ - ...this.blocks.slice(0, i), - v, - ...this.blocks.slice(i + 1) - ]; - this.$emit('update:modelValue', newValue); - }, - - removeItem(el) { - const i = this.blocks.findIndex(x => x.id === el.id); - const newValue = [ - ...this.blocks.slice(0, i), - ...this.blocks.slice(i + 1) - ]; - this.$emit('update:modelValue', newValue); - }, - } -}); -</script> diff --git a/src/client/pages/page-editor/page-editor.container.vue b/src/client/pages/page-editor/page-editor.container.vue deleted file mode 100644 index afd261fac7..0000000000 --- a/src/client/pages/page-editor/page-editor.container.vue +++ /dev/null @@ -1,159 +0,0 @@ -<template> -<div class="cpjygsrt" :class="{ error: error != null, warn: warn != null }"> - <header> - <div class="title"><slot name="header"></slot></div> - <div class="buttons"> - <slot name="func"></slot> - <button v-if="removable" @click="remove()" class="_button"> - <i class="fas fa-trash-alt"></i> - </button> - <button v-if="draggable" class="drag-handle _button"> - <i class="fas fa-bars"></i> - </button> - <button @click="toggleContent(!showBody)" class="_button"> - <template v-if="showBody"><i class="fas fa-angle-up"></i></template> - <template v-else><i class="fas fa-angle-down"></i></template> - </button> - </div> - </header> - <p v-show="showBody" class="error" v-if="error != null">{{ $t('_pages.script.typeError', { slot: error.arg + 1, expect: $t(`script.types.${error.expect}`), actual: $t(`script.types.${error.actual}`) }) }}</p> - <p v-show="showBody" class="warn" v-if="warn != null">{{ $t('_pages.script.thereIsEmptySlot', { slot: warn.slot + 1 }) }}</p> - <div v-show="showBody" class="body"> - <slot></slot> - </div> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; - -export default defineComponent({ - props: { - expanded: { - type: Boolean, - default: true - }, - removable: { - type: Boolean, - default: true - }, - draggable: { - type: Boolean, - default: false - }, - error: { - required: false, - default: null - }, - warn: { - required: false, - default: null - } - }, - emits: ['toggle', 'remove'], - data() { - return { - showBody: this.expanded, - }; - }, - methods: { - toggleContent(show: boolean) { - this.showBody = show; - this.$emit('toggle', show); - }, - remove() { - this.$emit('remove'); - } - } -}); -</script> - -<style lang="scss" scoped> -.cpjygsrt { - position: relative; - overflow: hidden; - background: var(--panel); - border: solid 2px var(--X12); - border-radius: 6px; - - &:hover { - border: solid 2px var(--X13); - } - - &.warn { - border: solid 2px #dec44c; - } - - &.error { - border: solid 2px #f00; - } - - & + .cpjygsrt { - margin-top: 16px; - } - - > header { - > .title { - z-index: 1; - margin: 0; - padding: 0 16px; - line-height: 42px; - font-size: 0.9em; - font-weight: bold; - box-shadow: 0 1px rgba(#000, 0.07); - - > i { - margin-right: 6px; - } - - &:empty { - display: none; - } - } - - > .buttons { - position: absolute; - z-index: 2; - top: 0; - right: 0; - - > button { - padding: 0; - width: 42px; - font-size: 0.9em; - line-height: 42px; - } - - .drag-handle { - cursor: move; - } - } - } - - > .warn { - color: #b19e49; - margin: 0; - padding: 16px 16px 0 16px; - font-size: 14px; - } - - > .error { - color: #f00; - margin: 0; - padding: 16px 16px 0 16px; - font-size: 14px; - } - - > .body { - ::v-deep(.juejbjww), ::v-deep(.eiipwacr) { - &:not(.inline):first-child { - margin-top: 28px; - } - - &:not(.inline):last-child { - margin-bottom: 20px; - } - } - } -} -</style> diff --git a/src/client/pages/page-editor/page-editor.script-block.vue b/src/client/pages/page-editor/page-editor.script-block.vue deleted file mode 100644 index 3313fc1ba9..0000000000 --- a/src/client/pages/page-editor/page-editor.script-block.vue +++ /dev/null @@ -1,281 +0,0 @@ -<template> -<XContainer :removable="removable" @remove="() => $emit('remove')" :error="error" :warn="warn" :draggable="draggable"> - <template #header><i v-if="icon" :class="icon"></i> <template v-if="title">{{ title }} <span class="turmquns" v-if="typeText">({{ typeText }})</span></template><template v-else-if="typeText">{{ typeText }}</template></template> - <template #func> - <button @click="changeType()" class="_button"> - <i class="fas fa-pencil-alt"></i> - </button> - </template> - - <section v-if="modelValue.type === null" class="pbglfege" @click="changeType()"> - {{ $ts._pages.script.emptySlot }} - </section> - <section v-else-if="modelValue.type === 'text'" class="tbwccoaw"> - <input v-model="modelValue.value"/> - </section> - <section v-else-if="modelValue.type === 'multiLineText'" class="tbwccoaw"> - <textarea v-model="modelValue.value"></textarea> - </section> - <section v-else-if="modelValue.type === 'textList'" class="tbwccoaw"> - <textarea v-model="modelValue.value" :placeholder="$ts._pages.script.blocks._textList.info"></textarea> - </section> - <section v-else-if="modelValue.type === 'number'" class="tbwccoaw"> - <input v-model="modelValue.value" type="number"/> - </section> - <section v-else-if="modelValue.type === 'ref'" class="hpdwcrvs"> - <select v-model="modelValue.value"> - <option v-for="v in hpml.getVarsByType(getExpectedType ? getExpectedType() : null).filter(x => x.name !== name)" :value="v.name">{{ v.name }}</option> - <optgroup :label="$ts._pages.script.argVariables"> - <option v-for="v in fnSlots" :value="v.name">{{ v.name }}</option> - </optgroup> - <optgroup :label="$ts._pages.script.pageVariables"> - <option v-for="v in hpml.getPageVarsByType(getExpectedType ? getExpectedType() : null)" :value="v">{{ v }}</option> - </optgroup> - <optgroup :label="$ts._pages.script.enviromentVariables"> - <option v-for="v in hpml.getEnvVarsByType(getExpectedType ? getExpectedType() : null)" :value="v">{{ v }}</option> - </optgroup> - </select> - </section> - <section v-else-if="modelValue.type === 'aiScriptVar'" class="tbwccoaw"> - <input v-model="modelValue.value"/> - </section> - <section v-else-if="modelValue.type === 'fn'" class="" style="padding:0 16px 16px 16px;"> - <MkTextarea v-model="slots"> - <template #label>{{ $ts._pages.script.blocks._fn.slots }}</template> - <template #caption>{{ $t('_pages.script.blocks._fn.slots-info') }}</template> - </MkTextarea> - <XV v-if="modelValue.value.expression" v-model="modelValue.value.expression" :title="$t(`_pages.script.blocks._fn.arg1`)" :get-expected-type="() => null" :hpml="hpml" :fn-slots="value.value.slots" :name="name"/> - </section> - <section v-else-if="modelValue.type.startsWith('fn:')" class="" style="padding:16px;"> - <XV v-for="(x, i) in modelValue.args" v-model="value.args[i]" :title="hpml.getVarByName(modelValue.type.split(':')[1]).value.slots[i].name" :get-expected-type="() => null" :hpml="hpml" :name="name" :key="i"/> - </section> - <section v-else class="" style="padding:16px;"> - <XV v-for="(x, i) in modelValue.args" v-model="modelValue.args[i]" :title="$t(`_pages.script.blocks._${modelValue.type}.arg${i + 1}`)" :get-expected-type="() => _getExpectedType(i)" :hpml="hpml" :name="name" :fn-slots="fnSlots" :key="i"/> - </section> -</XContainer> -</template> - -<script lang="ts"> -import { defineAsyncComponent, defineComponent } from 'vue'; -import { v4 as uuid } from 'uuid'; -import XContainer from './page-editor.container.vue'; -import MkTextarea from '@client/components/form/textarea.vue'; -import { blockDefs } from '@client/scripts/hpml/index'; -import * as os from '@client/os'; -import { isLiteralValue } from '@client/scripts/hpml/expr'; -import { funcDefs } from '@client/scripts/hpml/lib'; - -export default defineComponent({ - components: { - XContainer, MkTextarea, - XV: defineAsyncComponent(() => import('./page-editor.script-block.vue')), - }, - - inject: ['getScriptBlockList'], - - props: { - getExpectedType: { - required: false, - default: null - }, - modelValue: { - required: true - }, - title: { - required: false - }, - removable: { - required: false, - default: false - }, - hpml: { - required: true, - }, - name: { - required: true, - }, - fnSlots: { - required: false, - }, - draggable: { - required: false, - default: false - } - }, - - data() { - return { - error: null, - warn: null, - slots: '', - }; - }, - - computed: { - icon(): any { - if (this.modelValue.type === null) return null; - if (this.modelValue.type.startsWith('fn:')) return 'fas fa-plug'; - return blockDefs.find(x => x.type === this.modelValue.type).icon; - }, - typeText(): any { - if (this.modelValue.type === null) return null; - if (this.modelValue.type.startsWith('fn:')) return this.modelValue.type.split(':')[1]; - return this.$t(`_pages.script.blocks.${this.modelValue.type}`); - }, - }, - - watch: { - slots: { - handler() { - this.modelValue.value.slots = this.slots.split('\n').map(x => ({ - name: x, - type: null - })); - }, - deep: true - } - }, - - created() { - if (this.modelValue.value == null) this.modelValue.value = null; - - if (this.modelValue.value && this.modelValue.value.slots) this.slots = this.modelValue.value.slots.map(x => x.name).join('\n'); - - this.$watch(() => this.modelValue.type, (t) => { - this.warn = null; - - if (this.modelValue.type === 'fn') { - const id = uuid(); - this.modelValue.value = { - slots: [], - expression: { id, type: null } - }; - return; - } - - if (this.modelValue.type && this.modelValue.type.startsWith('fn:')) { - const fnName = this.modelValue.type.split(':')[1]; - const fn = this.hpml.getVarByName(fnName); - - const empties = []; - for (let i = 0; i < fn.value.slots.length; i++) { - const id = uuid(); - empties.push({ id, type: null }); - } - this.modelValue.args = empties; - return; - } - - if (isLiteralValue(this.modelValue)) return; - - const empties = []; - for (let i = 0; i < funcDefs[this.modelValue.type].in.length; i++) { - const id = uuid(); - empties.push({ id, type: null }); - } - this.modelValue.args = empties; - - for (let i = 0; i < funcDefs[this.modelValue.type].in.length; i++) { - const inType = funcDefs[this.modelValue.type].in[i]; - if (typeof inType !== 'number') { - if (inType === 'number') this.modelValue.args[i].type = 'number'; - if (inType === 'string') this.modelValue.args[i].type = 'text'; - } - } - }); - - this.$watch(() => this.modelValue.args, (args) => { - if (args == null) { - this.warn = null; - return; - } - const emptySlotIndex = args.findIndex(x => x.type === null); - if (emptySlotIndex !== -1 && emptySlotIndex < args.length) { - this.warn = { - slot: emptySlotIndex - }; - } else { - this.warn = null; - } - }, { - deep: true - }); - - this.$watch(() => this.hpml.variables, () => { - if (this.type != null && this.modelValue) { - this.error = this.hpml.typeCheck(this.modelValue); - } - }, { - deep: true - }); - }, - - methods: { - async changeType() { - const { canceled, result: type } = await os.dialog({ - type: null, - title: this.$ts._pages.selectType, - select: { - groupedItems: this.getScriptBlockList(this.getExpectedType ? this.getExpectedType() : null) - }, - showCancelButton: true - }); - if (canceled) return; - this.modelValue.type = type; - }, - - _getExpectedType(slot: number) { - return this.hpml.getExpectedType(this.modelValue, slot); - } - } -}); -</script> - -<style lang="scss" scoped> -.turmquns { - opacity: 0.7; -} - -.pbglfege { - opacity: 0.5; - padding: 16px; - text-align: center; - cursor: pointer; - color: var(--fg); -} - -.tbwccoaw { - > input, - > textarea { - display: block; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - width: 100%; - max-width: 100%; - min-width: 100%; - border: none; - box-shadow: none; - padding: 16px; - font-size: 16px; - background: transparent; - color: var(--fg); - box-sizing: border-box; - } - - > textarea { - min-height: 100px; - } -} - -.hpdwcrvs { - padding: 16px; - - > select { - display: block; - padding: 4px; - font-size: 16px; - width: 100%; - } -} -</style> diff --git a/src/client/pages/page-editor/page-editor.vue b/src/client/pages/page-editor/page-editor.vue deleted file mode 100644 index aefcc14564..0000000000 --- a/src/client/pages/page-editor/page-editor.vue +++ /dev/null @@ -1,561 +0,0 @@ -<template> -<div> - <div class="jqqmcavi" style="margin: 16px;"> - <MkButton v-if="pageId" class="button" inline link :to="`/@${ author.username }/pages/${ currentName }`"><i class="fas fa-external-link-square-alt"></i> {{ $ts._pages.viewPage }}</MkButton> - <MkButton inline @click="save" primary class="button" v-if="!readonly"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton> - <MkButton inline @click="duplicate" class="button" v-if="pageId"><i class="fas fa-copy"></i> {{ $ts.duplicate }}</MkButton> - <MkButton inline @click="del" class="button" v-if="pageId && !readonly" danger><i class="fas fa-trash-alt"></i> {{ $ts.delete }}</MkButton> - </div> - - <div v-if="tab === 'settings'"> - <div style="padding: 16px;" class="_formRoot"> - <MkInput v-model="title" class="_formBlock"> - <template #label>{{ $ts._pages.title }}</template> - </MkInput> - - <MkInput v-model="summary" class="_formBlock"> - <template #label>{{ $ts._pages.summary }}</template> - </MkInput> - - <MkInput v-model="name" class="_formBlock"> - <template #prefix>{{ url }}/@{{ author.username }}/pages/</template> - <template #label>{{ $ts._pages.url }}</template> - </MkInput> - - <MkSwitch v-model="alignCenter" class="_formBlock">{{ $ts._pages.alignCenter }}</MkSwitch> - - <MkSelect v-model="font" class="_formBlock"> - <template #label>{{ $ts._pages.font }}</template> - <option value="serif">{{ $ts._pages.fontSerif }}</option> - <option value="sans-serif">{{ $ts._pages.fontSansSerif }}</option> - </MkSelect> - - <MkSwitch v-model="hideTitleWhenPinned" class="_formBlock">{{ $ts._pages.hideTitleWhenPinned }}</MkSwitch> - - <div class="eyeCatch"> - <MkButton v-if="eyeCatchingImageId == null && !readonly" @click="setEyeCatchingImage"><i class="fas fa-plus"></i> {{ $ts._pages.eyeCatchingImageSet }}</MkButton> - <div v-else-if="eyeCatchingImage"> - <img :src="eyeCatchingImage.url" :alt="eyeCatchingImage.name" style="max-width: 100%;"/> - <MkButton @click="removeEyeCatchingImage()" v-if="!readonly"><i class="fas fa-trash-alt"></i> {{ $ts._pages.eyeCatchingImageRemove }}</MkButton> - </div> - </div> - </div> - </div> - - <div v-else-if="tab === 'contents'"> - <div style="padding: 16px;"> - <XBlocks class="content" v-model="content" :hpml="hpml"/> - - <MkButton @click="add()" v-if="!readonly"><i class="fas fa-plus"></i></MkButton> - </div> - </div> - - <div v-else-if="tab === 'variables'"> - <div class="qmuvgica"> - <XDraggable tag="div" class="variables" v-show="variables.length > 0" v-model="variables" item-key="name" handle=".drag-handle" :group="{ name: 'variables' }" animation="150" swap-threshold="0.5"> - <template #item="{element}"> - <XVariable - :modelValue="element" - :removable="true" - @remove="() => removeVariable(element)" - :hpml="hpml" - :name="element.name" - :title="element.name" - :draggable="true" - /> - </template> - </XDraggable> - - <MkButton @click="addVariable()" class="add" v-if="!readonly"><i class="fas fa-plus"></i></MkButton> - </div> - </div> - - <div v-else-if="tab === 'script'"> - <div> - <MkTextarea class="_code" v-model="script"/> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import { defineComponent, defineAsyncComponent, computed } from 'vue'; -import 'prismjs'; -import { highlight, languages } from 'prismjs/components/prism-core'; -import 'prismjs/components/prism-clike'; -import 'prismjs/components/prism-javascript'; -import 'prismjs/themes/prism-okaidia.css'; -import 'vue-prism-editor/dist/prismeditor.min.css'; -import { v4 as uuid } from 'uuid'; -import XVariable from './page-editor.script-block.vue'; -import XBlocks from './page-editor.blocks.vue'; -import MkTextarea from '@client/components/form/textarea.vue'; -import MkContainer from '@client/components/ui/container.vue'; -import MkButton from '@client/components/ui/button.vue'; -import MkSelect from '@client/components/form/select.vue'; -import MkSwitch from '@client/components/form/switch.vue'; -import MkInput from '@client/components/form/input.vue'; -import { blockDefs } from '@client/scripts/hpml/index'; -import { HpmlTypeChecker } from '@client/scripts/hpml/type-checker'; -import { url } from '@client/config'; -import { collectPageVars } from '@client/scripts/collect-page-vars'; -import * as os from '@client/os'; -import { selectFile } from '@client/scripts/select-file'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - XDraggable: defineAsyncComponent(() => import('vuedraggable').then(x => x.default)), - XVariable, XBlocks, MkTextarea, MkContainer, MkButton, MkSelect, MkSwitch, MkInput, - }, - - props: { - initPageId: { - type: String, - required: false - }, - initPageName: { - type: String, - required: false - }, - initUser: { - type: String, - required: false - }, - }, - - data() { - return { - [symbols.PAGE_INFO]: computed(() => { - let title = this.$ts._pages.newPage; - if (this.initPageId) { - title = this.$ts._pages.editPage; - } - else if (this.initPageName && this.initUser) { - title = this.$ts._pages.readPage; - } - return { - title: title, - icon: 'fas fa-pencil-alt', - bg: 'var(--bg)', - tabs: [{ - active: this.tab === 'settings', - title: this.$ts._pages.pageSetting, - icon: 'fas fa-cog', - onClick: () => { this.tab = 'settings'; }, - }, { - active: this.tab === 'contents', - title: this.$ts._pages.contents, - icon: 'fas fa-sticky-note', - onClick: () => { this.tab = 'contents'; }, - }, { - active: this.tab === 'variables', - title: this.$ts._pages.variables, - icon: 'fas fa-magic', - onClick: () => { this.tab = 'variables'; }, - }, { - active: this.tab === 'script', - title: this.$ts.script, - icon: 'fas fa-code', - onClick: () => { this.tab = 'script'; }, - }], - }; - }), - tab: 'settings', - author: this.$i, - readonly: false, - page: null, - pageId: null, - currentName: null, - title: '', - summary: null, - name: Date.now().toString(), - eyeCatchingImage: null, - eyeCatchingImageId: null, - font: 'sans-serif', - content: [], - alignCenter: false, - hideTitleWhenPinned: false, - variables: [], - hpml: null, - script: '', - url, - }; - }, - - watch: { - async eyeCatchingImageId() { - if (this.eyeCatchingImageId == null) { - this.eyeCatchingImage = null; - } else { - this.eyeCatchingImage = await os.api('drive/files/show', { - fileId: this.eyeCatchingImageId, - }); - } - }, - }, - - async created() { - this.hpml = new HpmlTypeChecker(); - - this.$watch('variables', () => { - this.hpml.variables = this.variables; - }, { deep: true }); - - this.$watch('content', () => { - this.hpml.pageVars = collectPageVars(this.content); - }, { deep: true }); - - if (this.initPageId) { - this.page = await os.api('pages/show', { - pageId: this.initPageId, - }); - } else if (this.initPageName && this.initUser) { - this.page = await os.api('pages/show', { - name: this.initPageName, - username: this.initUser, - }); - this.readonly = true; - } - - if (this.page) { - this.author = this.page.user; - this.pageId = this.page.id; - this.title = this.page.title; - this.name = this.page.name; - this.currentName = this.page.name; - this.summary = this.page.summary; - this.font = this.page.font; - this.script = this.page.script; - this.hideTitleWhenPinned = this.page.hideTitleWhenPinned; - this.alignCenter = this.page.alignCenter; - this.content = this.page.content; - this.variables = this.page.variables; - this.eyeCatchingImageId = this.page.eyeCatchingImageId; - } else { - const id = uuid(); - this.content = [{ - id, - type: 'text', - text: 'Hello World!' - }]; - } - }, - - provide() { - return { - readonly: this.readonly, - getScriptBlockList: this.getScriptBlockList, - getPageBlockList: this.getPageBlockList - } - }, - - methods: { - getSaveOptions() { - return { - title: this.title.trim(), - name: this.name.trim(), - summary: this.summary, - font: this.font, - script: this.script, - hideTitleWhenPinned: this.hideTitleWhenPinned, - alignCenter: this.alignCenter, - content: this.content, - variables: this.variables, - eyeCatchingImageId: this.eyeCatchingImageId, - }; - }, - - save() { - const options = this.getSaveOptions(); - - const onError = err => { - if (err.id == '3d81ceae-475f-4600-b2a8-2bc116157532') { - if (err.info.param == 'name') { - os.dialog({ - type: 'error', - title: this.$ts._pages.invalidNameTitle, - text: this.$ts._pages.invalidNameText - }); - } - } else if (err.code == 'NAME_ALREADY_EXISTS') { - os.dialog({ - type: 'error', - text: this.$ts._pages.nameAlreadyExists - }); - } - }; - - if (this.pageId) { - options.pageId = this.pageId; - os.api('pages/update', options) - .then(page => { - this.currentName = this.name.trim(); - os.dialog({ - type: 'success', - text: this.$ts._pages.updated - }); - }).catch(onError); - } else { - os.api('pages/create', options) - .then(page => { - this.pageId = page.id; - this.currentName = this.name.trim(); - os.dialog({ - type: 'success', - text: this.$ts._pages.created - }); - this.$router.push(`/pages/edit/${this.pageId}`); - }).catch(onError); - } - }, - - del() { - os.dialog({ - type: 'warning', - text: this.$t('removeAreYouSure', { x: this.title.trim() }), - showCancelButton: true - }).then(({ canceled }) => { - if (canceled) return; - os.api('pages/delete', { - pageId: this.pageId, - }).then(() => { - os.dialog({ - type: 'success', - text: this.$ts._pages.deleted - }); - this.$router.push(`/pages`); - }); - }); - }, - - duplicate() { - this.title = this.title + ' - copy'; - this.name = this.name + '-copy'; - os.api('pages/create', this.getSaveOptions()).then(page => { - this.pageId = page.id; - this.currentName = this.name.trim(); - os.dialog({ - type: 'success', - text: this.$ts._pages.created - }); - this.$router.push(`/pages/edit/${this.pageId}`); - }); - }, - - async add() { - const { canceled, result: type } = await os.dialog({ - type: null, - title: this.$ts._pages.chooseBlock, - select: { - groupedItems: this.getPageBlockList() - }, - showCancelButton: true - }); - if (canceled) return; - - const id = uuid(); - this.content.push({ id, type }); - }, - - async addVariable() { - let { canceled, result: name } = await os.dialog({ - title: this.$ts._pages.enterVariableName, - input: { - type: 'text', - }, - showCancelButton: true - }); - if (canceled) return; - - name = name.trim(); - - if (this.hpml.isUsedName(name)) { - os.dialog({ - type: 'error', - text: this.$ts._pages.variableNameIsAlreadyUsed - }); - return; - } - - const id = uuid(); - this.variables.push({ id, name, type: null }); - }, - - removeVariable(v) { - this.variables = this.variables.filter(x => x.name !== v.name); - }, - - getPageBlockList() { - return [{ - label: this.$ts._pages.contentBlocks, - items: [ - { value: 'section', text: this.$ts._pages.blocks.section }, - { value: 'text', text: this.$ts._pages.blocks.text }, - { value: 'image', text: this.$ts._pages.blocks.image }, - { value: 'textarea', text: this.$ts._pages.blocks.textarea }, - { value: 'note', text: this.$ts._pages.blocks.note }, - { value: 'canvas', text: this.$ts._pages.blocks.canvas }, - ] - }, { - label: this.$ts._pages.inputBlocks, - items: [ - { value: 'button', text: this.$ts._pages.blocks.button }, - { value: 'radioButton', text: this.$ts._pages.blocks.radioButton }, - { value: 'textInput', text: this.$ts._pages.blocks.textInput }, - { value: 'textareaInput', text: this.$ts._pages.blocks.textareaInput }, - { value: 'numberInput', text: this.$ts._pages.blocks.numberInput }, - { value: 'switch', text: this.$ts._pages.blocks.switch }, - { value: 'counter', text: this.$ts._pages.blocks.counter } - ] - }, { - label: this.$ts._pages.specialBlocks, - items: [ - { value: 'if', text: this.$ts._pages.blocks.if }, - { value: 'post', text: this.$ts._pages.blocks.post } - ] - }]; - }, - - getScriptBlockList(type: string = null) { - const list = []; - - const blocks = blockDefs.filter(block => type === null || block.out === null || block.out === type || typeof block.out === 'number'); - - for (const block of blocks) { - const category = list.find(x => x.category === block.category); - if (category) { - category.items.push({ - value: block.type, - text: this.$t(`_pages.script.blocks.${block.type}`) - }); - } else { - list.push({ - category: block.category, - label: this.$t(`_pages.script.categories.${block.category}`), - items: [{ - value: block.type, - text: this.$t(`_pages.script.blocks.${block.type}`) - }] - }); - } - } - - const userFns = this.variables.filter(x => x.type === 'fn'); - if (userFns.length > 0) { - list.unshift({ - label: this.$t(`_pages.script.categories.fn`), - items: userFns.map(v => ({ - value: 'fn:' + v.name, - text: v.name - })) - }); - } - - return list; - }, - - setEyeCatchingImage(e) { - selectFile(e.currentTarget || e.target, null, false).then(file => { - this.eyeCatchingImageId = file.id; - }); - }, - - removeEyeCatchingImage() { - this.eyeCatchingImageId = null; - }, - - highlighter(code) { - return highlight(code, languages.js, 'javascript'); - }, - } -}); -</script> - -<style lang="scss" scoped> -.jqqmcavi { - > .button { - & + .button { - margin-left: 8px; - } - } -} - -.gwbmwxkm { - position: relative; - - > header { - > .title { - z-index: 1; - margin: 0; - padding: 0 16px; - line-height: 42px; - font-size: 0.9em; - font-weight: bold; - box-shadow: 0 1px rgba(#000, 0.07); - - > i { - margin-right: 6px; - } - - &:empty { - display: none; - } - } - - > .buttons { - position: absolute; - z-index: 2; - top: 0; - right: 0; - - > button { - padding: 0; - width: 42px; - font-size: 0.9em; - line-height: 42px; - } - } - } - - > section { - padding: 0 32px 32px 32px; - - @media (max-width: 500px) { - padding: 0 16px 16px 16px; - } - - > .view { - display: inline-block; - margin: 16px 0 0 0; - font-size: 14px; - } - - > .content { - margin-bottom: 16px; - } - - > .eyeCatch { - margin-bottom: 16px; - - > div { - > img { - max-width: 100%; - } - } - } - } -} - -.qmuvgica { - padding: 16px; - - > .variables { - margin-bottom: 16px; - } - - > .add { - margin-bottom: 16px; - } -} -</style> diff --git a/src/client/pages/page.vue b/src/client/pages/page.vue deleted file mode 100644 index 3ea687a35d..0000000000 --- a/src/client/pages/page.vue +++ /dev/null @@ -1,311 +0,0 @@ -<template> -<div> - <transition name="fade" mode="out-in"> - <div v-if="page" class="xcukqgmh" :key="page.id" v-size="{ max: [450] }"> - <div class="_block main"> - <!-- - <div class="header"> - <h1>{{ page.title }}</h1> - </div> - --> - <div class="banner"> - <img :src="page.eyeCatchingImage.url" v-if="page.eyeCatchingImageId"/> - </div> - <div class="content"> - <XPage :page="page"/> - </div> - <div class="actions"> - <div class="like"> - <MkButton class="button" @click="unlike()" v-if="page.isLiked" v-tooltip="$ts._pages.unlike" primary><i class="fas fa-heart"></i><span class="count" v-if="page.likedCount > 0">{{ page.likedCount }}</span></MkButton> - <MkButton class="button" @click="like()" v-else v-tooltip="$ts._pages.like"><i class="far fa-heart"></i><span class="count" v-if="page.likedCount > 0">{{ page.likedCount }}</span></MkButton> - </div> - <div class="other"> - <button class="_button" @click="shareWithNote" v-tooltip="$ts.shareWithNote" v-click-anime><i class="fas fa-retweet fa-fw"></i></button> - <button class="_button" @click="share" v-tooltip="$ts.share" v-click-anime><i class="fas fa-share-alt fa-fw"></i></button> - </div> - </div> - <div class="user"> - <MkAvatar :user="page.user" class="avatar"/> - <div class="name"> - <MkUserName :user="page.user" style="display: block;"/> - <MkAcct :user="page.user"/> - </div> - <MkFollowButton v-if="!$i || $i.id != page.user.id" :user="page.user" :inline="true" :transparent="false" :full="true" large class="koudoku"/> - </div> - <div class="links"> - <MkA :to="`/@${username}/pages/${pageName}/view-source`" class="link">{{ $ts._pages.viewSource }}</MkA> - <template v-if="$i && $i.id === page.userId"> - <MkA :to="`/pages/edit/${page.id}`" class="link">{{ $ts._pages.editThisPage }}</MkA> - <button v-if="$i.pinnedPageId === page.id" @click="pin(false)" class="link _textButton">{{ $ts.unpin }}</button> - <button v-else @click="pin(true)" class="link _textButton">{{ $ts.pin }}</button> - </template> - </div> - </div> - <div class="footer"> - <div><i class="far fa-clock"></i> {{ $ts.createdAt }}: <MkTime :time="page.createdAt" mode="detail"/></div> - <div v-if="page.createdAt != page.updatedAt"><i class="far fa-clock"></i> {{ $ts.updatedAt }}: <MkTime :time="page.updatedAt" mode="detail"/></div> - </div> - <MkAd :prefer="['horizontal', 'horizontal-big']"/> - <MkContainer :max-height="300" :foldable="true" class="other"> - <template #header><i class="fas fa-clock"></i> {{ $ts.recentPosts }}</template> - <MkPagination :pagination="otherPostsPagination" #default="{items}"> - <MkPagePreview v-for="page in items" :page="page" :key="page.id" class="_gap"/> - </MkPagination> - </MkContainer> - </div> - <MkError v-else-if="error" @retry="fetch()"/> - <MkLoading v-else/> - </transition> -</div> -</template> - -<script lang="ts"> -import { computed, defineComponent } from 'vue'; -import XPage from '@client/components/page/page.vue'; -import MkButton from '@client/components/ui/button.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; -import { url } from '@client/config'; -import MkFollowButton from '@client/components/follow-button.vue'; -import MkContainer from '@client/components/ui/container.vue'; -import MkPagination from '@client/components/ui/pagination.vue'; -import MkPagePreview from '@client/components/page-preview.vue'; - -export default defineComponent({ - components: { - XPage, - MkButton, - MkFollowButton, - MkContainer, - MkPagination, - MkPagePreview, - }, - - props: { - pageName: { - type: String, - required: true - }, - username: { - type: String, - required: true - }, - }, - - data() { - return { - [symbols.PAGE_INFO]: computed(() => this.page ? { - title: computed(() => this.page.title || this.page.name), - avatar: this.page.user, - path: `/@${this.page.user.username}/pages/${this.page.name}`, - share: { - title: this.page.title || this.page.name, - text: this.page.summary, - }, - } : null), - page: null, - error: null, - otherPostsPagination: { - endpoint: 'users/pages', - limit: 6, - params: () => ({ - userId: this.page.user.id - }) - }, - }; - }, - - computed: { - path(): string { - return this.username + '/' + this.pageName; - } - }, - - watch: { - path() { - this.fetch(); - } - }, - - created() { - this.fetch(); - }, - - methods: { - fetch() { - this.page = null; - os.api('pages/show', { - name: this.pageName, - username: this.username, - }).then(page => { - this.page = page; - }).catch(e => { - this.error = e; - }); - }, - - share() { - navigator.share({ - title: this.page.title || this.page.name, - text: this.page.summary, - url: `${url}/@${this.page.user.username}/pages/${this.page.name}` - }); - }, - - shareWithNote() { - os.post({ - initialText: `${this.page.title || this.page.name} ${url}/@${this.page.user.username}/pages/${this.page.name}` - }); - }, - - like() { - os.apiWithDialog('pages/like', { - pageId: this.page.id, - }).then(() => { - this.page.isLiked = true; - this.page.likedCount++; - }); - }, - - async unlike() { - const confirm = await os.dialog({ - type: 'warning', - showCancelButton: true, - text: this.$ts.unlikeConfirm, - }); - if (confirm.canceled) return; - os.apiWithDialog('pages/unlike', { - pageId: this.page.id, - }).then(() => { - this.page.isLiked = false; - this.page.likedCount--; - }); - }, - - pin(pin) { - os.apiWithDialog('i/update', { - pinnedPageId: pin ? this.page.id : null, - }); - } - } -}); -</script> - -<style lang="scss" scoped> -.fade-enter-active, -.fade-leave-active { - transition: opacity 0.125s ease; -} -.fade-enter-from, -.fade-leave-to { - opacity: 0; -} - -.xcukqgmh { - --padding: 32px; - - &.max-width_450px { - --padding: 16px; - } - - > .main { - padding: var(--padding); - - > .header { - padding: 16px; - - > h1 { - margin: 0; - } - } - - > .banner { - > img { - // TODO: ่ฏใๆใใฎใขในใใฏใๆฏใง่กจ็คบ - display: block; - width: 100%; - height: 150px; - object-fit: cover; - } - } - - > .content { - margin-top: 16px; - padding: 16px 0 0 0; - } - - > .actions { - display: flex; - align-items: center; - margin-top: 16px; - padding: 16px 0 0 0; - border-top: solid 0.5px var(--divider); - - > .like { - > .button { - --accent: rgb(241 97 132); - --X8: rgb(241 92 128); - --buttonBg: rgb(216 71 106 / 5%); - --buttonHoverBg: rgb(216 71 106 / 10%); - color: #ff002f; - - ::v-deep(.count) { - margin-left: 0.5em; - } - } - } - - > .other { - margin-left: auto; - - > button { - padding: 8px; - margin: 0 8px; - - &:hover { - color: var(--fgHighlighted); - } - } - } - } - - > .user { - margin-top: 16px; - padding: 16px 0 0 0; - border-top: solid 0.5px var(--divider); - display: flex; - align-items: center; - - > .avatar { - width: 52px; - height: 52px; - } - - > .name { - margin: 0 0 0 12px; - font-size: 90%; - } - - > .koudoku { - margin-left: auto; - } - } - - > .links { - margin-top: 16px; - padding: 24px 0 0 0; - border-top: solid 0.5px var(--divider); - - > .link { - margin-right: 0.75em; - } - } - } - - > .footer { - margin: var(--padding); - font-size: 85%; - opacity: 0.75; - } -} -</style> diff --git a/src/client/pages/pages.vue b/src/client/pages/pages.vue deleted file mode 100644 index 6963682592..0000000000 --- a/src/client/pages/pages.vue +++ /dev/null @@ -1,96 +0,0 @@ -<template> -<MkSpacer> - <!-- TODO: MkHeaderใซ็ตฑๅ --> - <MkTab v-model="tab" v-if="$i"> - <option value="featured"><i class="fas fa-fire-alt"></i> {{ $ts._pages.featured }}</option> - <option value="my"><i class="fas fa-edit"></i> {{ $ts._pages.my }}</option> - <option value="liked"><i class="fas fa-heart"></i> {{ $ts._pages.liked }}</option> - </MkTab> - - <div class="_section"> - <div class="rknalgpo _content" v-if="tab === 'featured'"> - <MkPagination :pagination="featuredPagesPagination" #default="{items}"> - <MkPagePreview v-for="page in items" class="ckltabjg" :page="page" :key="page.id"/> - </MkPagination> - </div> - - <div class="rknalgpo _content my" v-if="tab === 'my'"> - <MkButton class="new" @click="create()"><i class="fas fa-plus"></i></MkButton> - <MkPagination :pagination="myPagesPagination" #default="{items}"> - <MkPagePreview v-for="page in items" class="ckltabjg" :page="page" :key="page.id"/> - </MkPagination> - </div> - - <div class="rknalgpo _content" v-if="tab === 'liked'"> - <MkPagination :pagination="likedPagesPagination" #default="{items}"> - <MkPagePreview v-for="like in items" class="ckltabjg" :page="like.page" :key="like.page.id"/> - </MkPagination> - </div> - </div> -</MkSpacer> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import MkPagePreview from '@client/components/page-preview.vue'; -import MkPagination from '@client/components/ui/pagination.vue'; -import MkButton from '@client/components/ui/button.vue'; -import MkTab from '@client/components/tab.vue'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - MkPagePreview, MkPagination, MkButton, MkTab - }, - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.pages, - icon: 'fas fa-sticky-note', - bg: 'var(--bg)', - actions: [{ - icon: 'fas fa-plus', - text: this.$ts.create, - handler: this.create, - }], - }, - tab: 'featured', - featuredPagesPagination: { - endpoint: 'pages/featured', - noPaging: true, - }, - myPagesPagination: { - endpoint: 'i/pages', - limit: 5, - }, - likedPagesPagination: { - endpoint: 'i/page-likes', - limit: 5, - }, - }; - }, - methods: { - create() { - this.$router.push(`/pages/new`); - } - } -}); -</script> - -<style lang="scss" scoped> -.rknalgpo { - &.my .ckltabjg:first-child { - margin-top: 16px; - } - - .ckltabjg:not(:last-child) { - margin-bottom: 8px; - } - - @media (min-width: 500px) { - .ckltabjg:not(:last-child) { - margin-bottom: 16px; - } - } -} -</style> diff --git a/src/client/pages/preview.vue b/src/client/pages/preview.vue deleted file mode 100644 index 3df446e676..0000000000 --- a/src/client/pages/preview.vue +++ /dev/null @@ -1,32 +0,0 @@ -<template> -<div class="graojtoi"> - <MkSample/> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import MkSample from '@client/components/sample.vue'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - MkSample, - }, - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.preview, - icon: 'fas fa-eye', - }, - } - }, -}); -</script> - -<style lang="scss" scoped> -.graojtoi { - padding: var(--margin); -} -</style> diff --git a/src/client/pages/reset-password.vue b/src/client/pages/reset-password.vue deleted file mode 100644 index 6dd9f24259..0000000000 --- a/src/client/pages/reset-password.vue +++ /dev/null @@ -1,69 +0,0 @@ -<template> -<FormBase v-if="token"> - <FormInput v-model="password" type="password"> - <template #prefix><i class="fas fa-lock"></i></template> - <span>{{ $ts.newPassword }}</span> - </FormInput> - - <FormButton primary @click="save">{{ $ts.save }}</FormButton> -</FormBase> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import FormLink from '@client/components/debobigego/link.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import FormInput from '@client/components/debobigego/input.vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - FormBase, - FormGroup, - FormLink, - FormInput, - FormButton, - }, - - props: { - token: { - type: String, - required: false - } - }, - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.resetPassword, - icon: 'fas fa-lock' - }, - password: '', - } - }, - - mounted() { - if (this.token == null) { - os.popup(import('@client/components/forgot-password.vue'), {}, {}, 'closed'); - this.$router.push('/'); - } - }, - - methods: { - async save() { - await os.apiWithDialog('reset-password', { - token: this.token, - password: this.password, - }); - this.$router.push('/'); - } - } -}); -</script> - -<style lang="scss" scoped> - -</style> diff --git a/src/client/pages/reversi/game.board.vue b/src/client/pages/reversi/game.board.vue deleted file mode 100644 index 0dd36faced..0000000000 --- a/src/client/pages/reversi/game.board.vue +++ /dev/null @@ -1,528 +0,0 @@ -<template> -<div class="xqnhankfuuilcwvhgsopeqncafzsquya"> - <header><b><MkA :to="userPage(blackUser)"><MkUserName :user="blackUser"/></MkA></b>({{ $ts._reversi.black }}) vs <b><MkA :to="userPage(whiteUser)"><MkUserName :user="whiteUser"/></MkA></b>({{ $ts._reversi.white }})</header> - - <div style="overflow: hidden; line-height: 28px;"> - <p class="turn" v-if="!iAmPlayer && !game.isEnded"> - <Mfm :key="'turn:' + turnUser().name" :text="$t('_reversi.turnOf', { name: turnUser().name })" :plain="true" :custom-emojis="turnUser().emojis"/> - <MkEllipsis/> - </p> - <p class="turn" v-if="logPos != logs.length"> - <Mfm :key="'past-turn-of:' + turnUser().name" :text="$t('_reversi.pastTurnOf', { name: turnUser().name })" :plain="true" :custom-emojis="turnUser().emojis"/> - </p> - <p class="turn1" v-if="iAmPlayer && !game.isEnded && !isMyTurn()">{{ $ts._reversi.opponentTurn }}<MkEllipsis/></p> - <p class="turn2" v-if="iAmPlayer && !game.isEnded && isMyTurn()" style="animation: tada 1s linear infinite both;">{{ $ts._reversi.myTurn }}</p> - <p class="result" v-if="game.isEnded && logPos == logs.length"> - <template v-if="game.winner"> - <Mfm :key="'won'" :text="$t('_reversi.won', { name: game.winner.name })" :plain="true" :custom-emojis="game.winner.emojis"/> - <span v-if="game.surrendered != null"> ({{ $ts._reversi.surrendered }})</span> - </template> - <template v-else>{{ $ts._reversi.drawn }}</template> - </p> - </div> - - <div class="board"> - <div class="labels-x" v-if="$store.state.gamesReversiShowBoardLabels"> - <span v-for="i in game.map[0].length">{{ String.fromCharCode(64 + i) }}</span> - </div> - <div class="flex"> - <div class="labels-y" v-if="$store.state.gamesReversiShowBoardLabels"> - <div v-for="i in game.map.length">{{ i }}</div> - </div> - <div class="cells" :style="cellsStyle"> - <div v-for="(stone, i) in o.board" - :class="{ empty: stone == null, none: o.map[i] == 'null', isEnded: game.isEnded, myTurn: !game.isEnded && isMyTurn(), can: turnUser() ? o.canPut(turnUser().id == blackUser.id, i) : null, prev: o.prevPos == i }" - @click="set(i)" - :title="`${String.fromCharCode(65 + o.transformPosToXy(i)[0])}${o.transformPosToXy(i)[1] + 1}`" - > - <template v-if="$store.state.gamesReversiUseAvatarStones || true"> - <img v-if="stone === true" :src="blackUser.avatarUrl" alt="black"> - <img v-if="stone === false" :src="whiteUser.avatarUrl" alt="white"> - </template> - <template v-else> - <i v-if="stone === true" class="fas fa-circle"></i> - <i v-if="stone === false" class="far fa-circle"></i> - </template> - </div> - </div> - <div class="labels-y" v-if="$store.state.gamesReversiShowBoardLabels"> - <div v-for="i in game.map.length">{{ i }}</div> - </div> - </div> - <div class="labels-x" v-if="$store.state.gamesReversiShowBoardLabels"> - <span v-for="i in game.map[0].length">{{ String.fromCharCode(64 + i) }}</span> - </div> - </div> - - <p class="status"><b>{{ $t('_reversi.turnCount', { count: logPos }) }}</b> {{ $ts._reversi.black }}:{{ o.blackCount }} {{ $ts._reversi.white }}:{{ o.whiteCount }} {{ $ts._reversi.total }}:{{ o.blackCount + o.whiteCount }}</p> - - <div class="actions" v-if="!game.isEnded && iAmPlayer"> - <MkButton @click="surrender" inline>{{ $ts._reversi.surrender }}</MkButton> - </div> - - <div class="player" v-if="game.isEnded"> - <span>{{ logPos }} / {{ logs.length }}</span> - <div class="buttons" v-if="!autoplaying"> - <MkButton inline @click="logPos = 0" :disabled="logPos == 0"><i class="fas fa-angle-double-left"></i></MkButton> - <MkButton inline @click="logPos--" :disabled="logPos == 0"><i class="fas fa-angle-left"></i></MkButton> - <MkButton inline @click="logPos++" :disabled="logPos == logs.length"><i class="fas fa-angle-right"></i></MkButton> - <MkButton inline @click="logPos = logs.length" :disabled="logPos == logs.length"><i class="fas fa-angle-double-right"></i></MkButton> - </div> - <MkButton @click="autoplay()" :disabled="autoplaying" style="margin: var(--margin) auto 0 auto;"><i class="fas fa-play"></i></MkButton> - </div> - - <div class="info"> - <p v-if="game.isLlotheo">{{ $ts._reversi.isLlotheo }}</p> - <p v-if="game.loopedBoard">{{ $ts._reversi.loopedMap }}</p> - <p v-if="game.canPutEverywhere">{{ $ts._reversi.canPutEverywhere }}</p> - </div> - - <div class="watchers"> - <MkAvatar v-for="user in watchers" :key="user.id" :user="user" class="avatar"/> - </div> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import * as CRC32 from 'crc-32'; -import Reversi, { Color } from '../../../games/reversi/core'; -import { url } from '@client/config'; -import MkButton from '@client/components/ui/button.vue'; -import { userPage } from '@client/filters/user'; -import * as os from '@client/os'; -import * as sound from '@client/scripts/sound'; - -export default defineComponent({ - components: { - MkButton - }, - - props: { - initGame: { - type: Object, - require: true - }, - connection: { - type: Object, - require: true - }, - }, - - data() { - return { - game: JSON.parse(JSON.stringify(this.initGame)), - o: null as Reversi, - logs: [], - logPos: 0, - watchers: [], - pollingClock: null, - }; - }, - - computed: { - iAmPlayer(): boolean { - if (!this.$i) return false; - return this.game.user1Id == this.$i.id || this.game.user2Id == this.$i.id; - }, - - myColor(): Color { - if (!this.iAmPlayer) return null; - if (this.game.user1Id == this.$i.id && this.game.black == 1) return true; - if (this.game.user2Id == this.$i.id && this.game.black == 2) return true; - return false; - }, - - opColor(): Color { - if (!this.iAmPlayer) return null; - return this.myColor === true ? false : true; - }, - - blackUser(): any { - return this.game.black == 1 ? this.game.user1 : this.game.user2; - }, - - whiteUser(): any { - return this.game.black == 1 ? this.game.user2 : this.game.user1; - }, - - cellsStyle(): any { - return { - 'grid-template-rows': `repeat(${this.game.map.length}, 1fr)`, - 'grid-template-columns': `repeat(${this.game.map[0].length}, 1fr)` - }; - } - }, - - watch: { - logPos(v) { - if (!this.game.isEnded) return; - const o = new Reversi(this.game.map, { - isLlotheo: this.game.isLlotheo, - canPutEverywhere: this.game.canPutEverywhere, - loopedBoard: this.game.loopedBoard - }); - for (const log of this.logs.slice(0, v)) { - o.put(log.color, log.pos); - } - this.o = o; - //this.$forceUpdate(); - } - }, - - created() { - this.o = new Reversi(this.game.map, { - isLlotheo: this.game.isLlotheo, - canPutEverywhere: this.game.canPutEverywhere, - loopedBoard: this.game.loopedBoard - }); - - for (const log of this.game.logs) { - this.o.put(log.color, log.pos); - } - - this.logs = this.game.logs; - this.logPos = this.logs.length; - - // ้ไฟกใๅใใใผใใฆใใใใใใซๅฎๆ็ใซใใผใชใณใฐใใใ - if (this.game.isStarted && !this.game.isEnded) { - this.pollingClock = setInterval(() => { - if (this.game.isEnded) return; - const crc32 = CRC32.str(this.logs.map(x => x.pos.toString()).join('')); - this.connection.send('check', { - crc32: crc32 - }); - }, 3000); - } - }, - - mounted() { - this.connection.on('set', this.onSet); - this.connection.on('rescue', this.onRescue); - this.connection.on('ended', this.onEnded); - this.connection.on('watchers', this.onWatchers); - }, - - beforeUnmount() { - this.connection.off('set', this.onSet); - this.connection.off('rescue', this.onRescue); - this.connection.off('ended', this.onEnded); - this.connection.off('watchers', this.onWatchers); - - clearInterval(this.pollingClock); - }, - - methods: { - userPage, - - // this.o ใใชใขใฏใใฃใใซใชใฃใๆใซใฏcomputedใซใงใใ - turnUser(): any { - if (this.o.turn === true) { - return this.game.black == 1 ? this.game.user1 : this.game.user2; - } else if (this.o.turn === false) { - return this.game.black == 1 ? this.game.user2 : this.game.user1; - } else { - return null; - } - }, - - // this.o ใใชใขใฏใใฃใใซใชใฃใๆใซใฏcomputedใซใงใใ - isMyTurn(): boolean { - if (!this.iAmPlayer) return false; - if (this.turnUser() == null) return false; - return this.turnUser().id == this.$i.id; - }, - - set(pos) { - if (this.game.isEnded) return; - if (!this.iAmPlayer) return; - if (!this.isMyTurn()) return; - if (!this.o.canPut(this.myColor, pos)) return; - - this.o.put(this.myColor, pos); - - // ใตใฆใณใใๅ็ใใ - sound.play(this.myColor ? 'reversiPutBlack' : 'reversiPutWhite'); - - this.connection.send('set', { - pos: pos - }); - - this.checkEnd(); - - this.$forceUpdate(); - }, - - onSet(x) { - this.logs.push(x); - this.logPos++; - this.o.put(x.color, x.pos); - this.checkEnd(); - this.$forceUpdate(); - - // ใตใฆใณใใๅ็ใใ - if (x.color !== this.myColor) { - sound.play(x.color ? 'reversiPutBlack' : 'reversiPutWhite'); - } - }, - - onEnded(x) { - this.game = JSON.parse(JSON.stringify(x.game)); - }, - - checkEnd() { - this.game.isEnded = this.o.isEnded; - if (this.game.isEnded) { - if (this.o.winner === true) { - this.game.winnerId = this.game.black == 1 ? this.game.user1Id : this.game.user2Id; - this.game.winner = this.game.black == 1 ? this.game.user1 : this.game.user2; - } else if (this.o.winner === false) { - this.game.winnerId = this.game.black == 1 ? this.game.user2Id : this.game.user1Id; - this.game.winner = this.game.black == 1 ? this.game.user2 : this.game.user1; - } else { - this.game.winnerId = null; - this.game.winner = null; - } - } - }, - - // ๆญฃใใใฒใผใ ๆ
ๅ ฑใ้ใใใฆใใใจใ - onRescue(game) { - this.game = JSON.parse(JSON.stringify(game)); - - this.o = new Reversi(this.game.map, { - isLlotheo: this.game.isLlotheo, - canPutEverywhere: this.game.canPutEverywhere, - loopedBoard: this.game.loopedBoard - }); - - for (const log of this.game.logs) { - this.o.put(log.color, log.pos, true); - } - - this.logs = this.game.logs; - this.logPos = this.logs.length; - - this.checkEnd(); - this.$forceUpdate(); - }, - - onWatchers(users) { - this.watchers = users; - }, - - surrender() { - os.api('games/reversi/games/surrender', { - gameId: this.game.id - }); - }, - - autoplay() { - this.autoplaying = true; - this.logPos = 0; - - setTimeout(() => { - this.logPos = 1; - - let i = 1; - let previousLog = this.game.logs[0]; - const tick = () => { - const log = this.game.logs[i]; - const time = new Date(log.at).getTime() - new Date(previousLog.at).getTime() - setTimeout(() => { - i++; - this.logPos++; - previousLog = log; - - if (i < this.game.logs.length) { - tick(); - } else { - this.autoplaying = false; - } - }, time); - }; - - tick(); - }, 1000); - } - } -}); -</script> - -<style lang="scss" scoped> - -@use "sass:math"; - -.xqnhankfuuilcwvhgsopeqncafzsquya { - text-align: center; - - > .go-index { - position: absolute; - top: 0; - left: 0; - z-index: 1; - width: 42px; - height :42px; - } - - > header { - padding: 8px; - border-bottom: dashed 1px var(--divider); - } - - > .board { - width: calc(100% - 16px); - max-width: 500px; - margin: 0 auto; - - $label-size: 16px; - $gap: 4px; - - > .labels-x { - height: $label-size; - padding: 0 $label-size; - display: flex; - - > * { - flex: 1; - display: flex; - align-items: center; - justify-content: center; - font-size: 0.8em; - - &:first-child { - margin-left: -(math.div($gap, 2)); - } - - &:last-child { - margin-right: -(math.div($gap, 2)); - } - } - } - - > .flex { - display: flex; - - > .labels-y { - width: $label-size; - display: flex; - flex-direction: column; - - > * { - flex: 1; - display: flex; - align-items: center; - justify-content: center; - font-size: 12px; - - &:first-child { - margin-top: -(math.div($gap, 2)); - } - - &:last-child { - margin-bottom: -(math.div($gap, 2)); - } - } - } - - > .cells { - flex: 1; - display: grid; - grid-gap: $gap; - - > div { - background: transparent; - border-radius: 6px; - overflow: hidden; - - * { - pointer-events: none; - user-select: none; - } - - &.empty { - border: solid 2px var(--divider); - } - - &.empty.can { - border-color: var(--accent); - } - - &.empty.myTurn { - border-color: var(--divider); - - &.can { - border-color: var(--accent); - cursor: pointer; - - &:hover { - background: var(--accent); - } - } - } - - &.prev { - box-shadow: 0 0 0 4px var(--accent); - } - - &.isEnded { - border-color: var(--divider); - } - - &.none { - border-color: transparent !important; - } - - > svg, > img { - display: block; - width: 100%; - height: 100%; - } - } - } - } - } - - > .status { - margin: 0; - padding: 16px 0; - } - - > .actions { - padding-bottom: 16px; - } - - > .player { - padding: 0 16px 32px 16px; - margin: 0 auto; - max-width: 500px; - - > span { - display: inline-block; - margin: 0 8px; - min-width: 70px; - } - - > .buttons { - display: flex; - - > * { - flex: 1; - } - } - } - - > .watchers { - padding: 0 0 16px 0; - - &:empty { - display: none; - } - - > .avatar { - width: 32px; - height: 32px; - } - } -} -</style> diff --git a/src/client/pages/reversi/game.setting.vue b/src/client/pages/reversi/game.setting.vue deleted file mode 100644 index eb6f24e4ab..0000000000 --- a/src/client/pages/reversi/game.setting.vue +++ /dev/null @@ -1,390 +0,0 @@ -<template> -<div class="urbixznjwwuukfsckrwzwsqzsxornqij"> - <header><b><MkUserName :user="game.user1"/></b> vs <b><MkUserName :user="game.user2"/></b></header> - - <div> - <p>{{ $ts._reversi.gameSettings }}</p> - - <div class="card map _panel"> - <header> - <select v-model="mapName" :placeholder="$ts._reversi.chooseBoard" @change="onMapChange"> - <option label="-Custom-" :value="mapName" v-if="mapName == '-Custom-'"/> - <option :label="$ts.random" :value="null"/> - <optgroup v-for="c in mapCategories" :key="c" :label="c"> - <option v-for="m in Object.values(maps).filter(m => m.category == c)" :key="m.name" :label="m.name" :value="m.name">{{ m.name }}</option> - </optgroup> - </select> - </header> - - <div> - <div class="random" v-if="game.map == null"><i class="fas fa-dice"></i></div> - <div class="board" v-else :style="{ 'grid-template-rows': `repeat(${ game.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.map[0].length }, 1fr)` }"> - <div v-for="(x, i) in game.map.join('')" :class="{ none: x == ' ' }" @click="onPixelClick(i, x)"> - <i v-if="x === 'b'" class="fas fa-circle"></i> - <i v-if="x === 'w'" class="far fa-circle"></i> - </div> - </div> - </div> - </div> - - <div class="card _panel"> - <header> - <span>{{ $ts._reversi.blackOrWhite }}</span> - </header> - - <div> - <MkRadio v-model="game.bw" value="random" @update:modelValue="updateSettings('bw')">{{ $ts.random }}</MkRadio> - <MkRadio v-model="game.bw" :value="'1'" @update:modelValue="updateSettings('bw')"> - <I18n :src="$ts._reversi.blackIs" tag="span"> - <template #name> - <b><MkUserName :user="game.user1"/></b> - </template> - </I18n> - </MkRadio> - <MkRadio v-model="game.bw" :value="'2'" @update:modelValue="updateSettings('bw')"> - <I18n :src="$ts._reversi.blackIs" tag="span"> - <template #name> - <b><MkUserName :user="game.user2"/></b> - </template> - </I18n> - </MkRadio> - </div> - </div> - - <div class="card _panel"> - <header> - <span>{{ $ts._reversi.rules }}</span> - </header> - - <div> - <MkSwitch v-model="game.isLlotheo" @update:modelValue="updateSettings('isLlotheo')">{{ $ts._reversi.isLlotheo }}</MkSwitch> - <MkSwitch v-model="game.loopedBoard" @update:modelValue="updateSettings('loopedBoard')">{{ $ts._reversi.loopedMap }}</MkSwitch> - <MkSwitch v-model="game.canPutEverywhere" @update:modelValue="updateSettings('canPutEverywhere')">{{ $ts._reversi.canPutEverywhere }}</MkSwitch> - </div> - </div> - - <div class="card form _panel" v-if="form"> - <header> - <span>{{ $ts._reversi.botSettings }}</span> - </header> - - <div> - <template v-for="item in form"> - <MkSwitch v-if="item.type == 'switch'" v-model="item.value" :key="item.id" @change="onChangeForm(item)">{{ item.label || item.desc || '' }}</MkSwitch> - - <div class="card" v-if="item.type == 'radio'" :key="item.id"> - <header> - <span>{{ item.label }}</span> - </header> - - <div> - <MkRadio v-for="(r, i) in item.items" :key="item.id + ':' + i" v-model="item.value" :value="r.value" @update:modelValue="onChangeForm(item)">{{ r.label }}</MkRadio> - </div> - </div> - - <div class="card" v-if="item.type == 'slider'" :key="item.id"> - <header> - <span>{{ item.label }}</span> - </header> - - <div> - <input type="range" :min="item.min" :max="item.max" :step="item.step || 1" v-model="item.value" @change="onChangeForm(item)"/> - </div> - </div> - - <div class="card" v-if="item.type == 'textbox'" :key="item.id"> - <header> - <span>{{ item.label }}</span> - </header> - - <div> - <input v-model="item.value" @change="onChangeForm(item)"/> - </div> - </div> - </template> - </div> - </div> - </div> - - <footer class="_acrylic"> - <p class="status"> - <template v-if="isAccepted && isOpAccepted">{{ $ts._reversi.thisGameIsStartedSoon }}<MkEllipsis/></template> - <template v-if="isAccepted && !isOpAccepted">{{ $ts._reversi.waitingForOther }}<MkEllipsis/></template> - <template v-if="!isAccepted && isOpAccepted">{{ $ts._reversi.waitingForMe }}</template> - <template v-if="!isAccepted && !isOpAccepted">{{ $ts._reversi.waitingBoth }}<MkEllipsis/></template> - </p> - - <div class="actions"> - <MkButton inline @click="exit">{{ $ts.cancel }}</MkButton> - <MkButton inline primary @click="accept" v-if="!isAccepted">{{ $ts._reversi.ready }}</MkButton> - <MkButton inline primary @click="cancel" v-if="isAccepted">{{ $ts._reversi.cancelReady }}</MkButton> - </div> - </footer> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import * as maps from '../../../games/reversi/maps'; -import MkButton from '@client/components/ui/button.vue'; -import MkSwitch from '@client/components/form/switch.vue'; -import MkRadio from '@client/components/form/radio.vue'; - -export default defineComponent({ - components: { - MkButton, - MkSwitch, - MkRadio, - }, - - props: { - initGame: { - type: Object, - require: true - }, - connection: { - type: Object, - require: true - }, - }, - - data() { - return { - game: this.initGame, - o: null, - isLlotheo: false, - mapName: maps.eighteight.name, - maps: maps, - form: null, - messages: [], - }; - }, - - computed: { - mapCategories(): string[] { - const categories = Object.values(maps).map(x => x.category); - return categories.filter((item, pos) => categories.indexOf(item) == pos); - }, - isAccepted(): boolean { - if (this.game.user1Id == this.$i.id && this.game.user1Accepted) return true; - if (this.game.user2Id == this.$i.id && this.game.user2Accepted) return true; - return false; - }, - isOpAccepted(): boolean { - if (this.game.user1Id != this.$i.id && this.game.user1Accepted) return true; - if (this.game.user2Id != this.$i.id && this.game.user2Accepted) return true; - return false; - } - }, - - created() { - this.connection.on('changeAccepts', this.onChangeAccepts); - this.connection.on('updateSettings', this.onUpdateSettings); - this.connection.on('initForm', this.onInitForm); - this.connection.on('message', this.onMessage); - - if (this.game.user1Id != this.$i.id && this.game.form1) this.form = this.game.form1; - if (this.game.user2Id != this.$i.id && this.game.form2) this.form = this.game.form2; - }, - - beforeUnmount() { - this.connection.off('changeAccepts', this.onChangeAccepts); - this.connection.off('updateSettings', this.onUpdateSettings); - this.connection.off('initForm', this.onInitForm); - this.connection.off('message', this.onMessage); - }, - - methods: { - exit() { - - }, - - accept() { - this.connection.send('accept', {}); - }, - - cancel() { - this.connection.send('cancelAccept', {}); - }, - - onChangeAccepts(accepts) { - this.game.user1Accepted = accepts.user1; - this.game.user2Accepted = accepts.user2; - }, - - updateSettings(key: string) { - this.connection.send('updateSettings', { - key: key, - value: this.game[key] - }); - }, - - onUpdateSettings({ key, value }) { - this.game[key] = value; - if (this.game.map == null) { - this.mapName = null; - } else { - const found = Object.values(maps).find(x => x.data.join('') == this.game.map.join('')); - this.mapName = found ? found.name : '-Custom-'; - } - }, - - onInitForm(x) { - if (x.userId == this.$i.id) return; - this.form = x.form; - }, - - onMessage(x) { - if (x.userId == this.$i.id) return; - this.messages.unshift(x.message); - }, - - onChangeForm(item) { - this.connection.send('updateForm', { - id: item.id, - value: item.value - }); - }, - - onMapChange() { - if (this.mapName == null) { - this.game.map = null; - } else { - this.game.map = Object.values(maps).find(x => x.name == this.mapName).data; - } - this.updateSettings('map'); - }, - - onPixelClick(pos, pixel) { - const x = pos % this.game.map[0].length; - const y = Math.floor(pos / this.game.map[0].length); - const newPixel = - pixel == ' ' ? '-' : - pixel == '-' ? 'b' : - pixel == 'b' ? 'w' : - ' '; - const line = this.game.map[y].split(''); - line[x] = newPixel; - this.game.map[y] = line.join(''); - this.updateSettings('map'); - } - } -}); -</script> - -<style lang="scss" scoped> -.urbixznjwwuukfsckrwzwsqzsxornqij { - text-align: center; - background: var(--bg); - - > header { - padding: 8px; - border-bottom: dashed 1px #c4cdd4; - } - - > div { - padding: 0 16px; - - > .card { - margin: 0 auto 16px auto; - - &.map { - > header { - > select { - width: 100%; - padding: 12px 14px; - background: var(--face); - border: 1px solid var(--inputBorder); - border-radius: 4px; - color: var(--fg); - cursor: pointer; - transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1); - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - - &:focus-visible, - &:active { - border-color: var(--accent); - } - } - } - - > div { - > .random { - padding: 32px 0; - font-size: 64px; - color: var(--fg); - opacity: 0.7; - } - - > .board { - display: grid; - grid-gap: 4px; - width: 300px; - height: 300px; - margin: 0 auto; - color: var(--fg); - - > div { - background: transparent; - border: solid 2px var(--divider); - border-radius: 6px; - overflow: hidden; - cursor: pointer; - - * { - pointer-events: none; - user-select: none; - width: 100%; - height: 100%; - } - - &.none { - border-color: transparent; - } - } - } - } - } - - &.form { - > div { - > .card + .card { - margin-top: 16px; - } - - input[type='range'] { - width: 100%; - } - } - } - } - - .card { - max-width: 400px; - - > header { - padding: 18px 20px; - border-bottom: 1px solid var(--divider); - } - - > div { - padding: 20px; - color: var(--fg); - } - } - } - - > footer { - position: sticky; - bottom: 0; - padding: 16px; - border-top: solid 1px var(--divider); - - > .status { - margin: 0 0 16px 0; - } - } -} -</style> diff --git a/src/client/pages/reversi/game.vue b/src/client/pages/reversi/game.vue deleted file mode 100644 index ae10b45b5b..0000000000 --- a/src/client/pages/reversi/game.vue +++ /dev/null @@ -1,76 +0,0 @@ -<template> -<div v-if="game == null"><MkLoading/></div> -<GameSetting v-else-if="!game.isStarted" :init-game="game" :connection="connection"/> -<GameBoard v-else :init-game="game" :connection="connection"/> -</template> - -<script lang="ts"> -import { defineComponent, markRaw } from 'vue'; -import GameSetting from './game.setting.vue'; -import GameBoard from './game.board.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - GameSetting, - GameBoard, - }, - - props: { - gameId: { - type: String, - required: true - }, - }, - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts._reversi.reversi, - icon: 'fas fa-gamepad' - }, - game: null, - connection: null, - }; - }, - - watch: { - gameId() { - this.fetch(); - } - }, - - mounted() { - this.fetch(); - }, - - beforeUnmount() { - if (this.connection) { - this.connection.dispose(); - } - }, - - methods: { - fetch() { - os.api('games/reversi/games/show', { - gameId: this.gameId - }).then(game => { - this.game = game; - - if (this.connection) { - this.connection.dispose(); - } - this.connection = markRaw(os.stream.useChannel('gamesReversiGame', { - gameId: this.game.id - })); - this.connection.on('started', this.onStarted); - }); - }, - - onStarted(game) { - Object.assign(this.game, game); - }, - } -}); -</script> diff --git a/src/client/pages/reversi/index.vue b/src/client/pages/reversi/index.vue deleted file mode 100644 index cedfd12089..0000000000 --- a/src/client/pages/reversi/index.vue +++ /dev/null @@ -1,279 +0,0 @@ -<template> -<div class="bgvwxkhb" v-if="!matching"> - <h1>Misskey {{ $ts._reversi.reversi }}</h1> - - <div class="play"> - <MkButton primary round @click="match" style="margin: var(--margin) auto 0 auto;">{{ $ts.invite }}</MkButton> - </div> - - <div class="_section"> - <MkFolder v-if="invitations.length > 0"> - <template #header>{{ $ts.invitations }}</template> - <div class="nfcacttm"> - <button class="invitation _panel _button" v-for="invitation in invitations" tabindex="-1" @click="accept(invitation)"> - <MkAvatar class="avatar" :user="invitation.parent" :show-indicator="true"/> - <span class="name"><b><MkUserName :user="invitation.parent"/></b></span> - <span class="username">@{{ invitation.parent.username }}</span> - <MkTime :time="invitation.createdAt" class="time"/> - </button> - </div> - </MkFolder> - - <MkFolder v-if="myGames.length > 0"> - <template #header>{{ $ts._reversi.myGames }}</template> - <div class="knextgwz"> - <MkA class="game _panel" v-for="g in myGames" tabindex="-1" :to="`/games/reversi/${g.id}`" :key="g.id"> - <div class="players"> - <MkAvatar class="avatar" :user="g.user1"/><b><MkUserName :user="g.user1"/></b> vs <b><MkUserName :user="g.user2"/></b><MkAvatar class="avatar" :user="g.user2"/> - </div> - <footer><span class="state" :class="{ playing: !g.isEnded }">{{ g.isEnded ? $ts._reversi.ended : $ts._reversi.playing }}</span><MkTime class="time" :time="g.createdAt"/></footer> - </MkA> - </div> - </MkFolder> - - <MkFolder v-if="games.length > 0"> - <template #header>{{ $ts._reversi.allGames }}</template> - <div class="knextgwz"> - <MkA class="game _panel" v-for="g in games" tabindex="-1" :to="`/games/reversi/${g.id}`" :key="g.id"> - <div class="players"> - <MkAvatar class="avatar" :user="g.user1"/><b><MkUserName :user="g.user1"/></b> vs <b><MkUserName :user="g.user2"/></b><MkAvatar class="avatar" :user="g.user2"/> - </div> - <footer><span class="state" :class="{ playing: !g.isEnded }">{{ g.isEnded ? $ts._reversi.ended : $ts._reversi.playing }}</span><MkTime class="time" :time="g.createdAt"/></footer> - </MkA> - </div> - </MkFolder> - </div> -</div> -<div class="sazhgisb" v-else> - <h1> - <I18n :src="$ts.waitingFor" tag="span"> - <template #x> - <b><MkUserName :user="matching"/></b> - </template> - </I18n> - <MkEllipsis/> - </h1> - <div class="cancel"> - <MkButton inline round @click="cancel">{{ $ts.cancel }}</MkButton> - </div> -</div> -</template> - -<script lang="ts"> -import { defineComponent, markRaw } from 'vue'; -import * as os from '@client/os'; -import MkButton from '@client/components/ui/button.vue'; -import MkFolder from '@client/components/ui/folder.vue'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - MkButton, MkFolder, - }, - - inject: ['navHook'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts._reversi.reversi, - icon: 'fas fa-gamepad' - }, - games: [], - gamesFetching: true, - gamesMoreFetching: false, - myGames: [], - matching: null, - invitations: [], - connection: null, - pingClock: null, - }; - }, - - mounted() { - if (this.$i) { - this.connection = markRaw(os.stream.useChannel('gamesReversi')); - - this.connection.on('invited', this.onInvited); - - this.connection.on('matched', this.onMatched); - - this.pingClock = setInterval(() => { - if (this.matching) { - this.connection.send('ping', { - id: this.matching.id - }); - } - }, 3000); - - os.api('games/reversi/games', { - my: true - }).then(games => { - this.myGames = games; - }); - - os.api('games/reversi/invitations').then(invitations => { - this.invitations = this.invitations.concat(invitations); - }); - } - - os.api('games/reversi/games').then(games => { - this.games = games; - this.gamesFetching = false; - }); - }, - - beforeUnmount() { - if (this.connection) { - this.connection.dispose(); - clearInterval(this.pingClock); - } - }, - - methods: { - go(game) { - const url = '/games/reversi/' + game.id; - if (this.navHook) { - this.navHook(url); - } else { - this.$router.push(url); - } - }, - - async match() { - const user = await os.selectUser({ local: true }); - if (user == null) return; - os.api('games/reversi/match', { - userId: user.id - }).then(res => { - if (res == null) { - this.matching = user; - } else { - this.go(res); - } - }); - }, - - cancel() { - this.matching = null; - os.api('games/reversi/match/cancel'); - }, - - accept(invitation) { - os.api('games/reversi/match', { - userId: invitation.parent.id - }).then(game => { - if (game) { - this.go(game); - } - }); - }, - - onMatched(game) { - this.go(game); - }, - - onInvited(invite) { - this.invitations.unshift(invite); - } - } -}); -</script> - -<style lang="scss" scoped> -.bgvwxkhb { - > h1 { - margin: 0; - padding: 24px; - text-align: center; - font-size: 1.5em; - background: linear-gradient(0deg, #43c583, #438881); - color: #fff; - } - - > .play { - text-align: center; - } -} - -.sazhgisb { - text-align: center; -} - -.nfcacttm { - > .invitation { - display: flex; - box-sizing: border-box; - width: 100%; - padding: 16px; - line-height: 32px; - text-align: left; - - > .avatar { - width: 32px; - height: 32px; - margin-right: 8px; - } - - > .name { - margin-right: 8px; - } - - > .username { - margin-right: 8px; - opacity: 0.7; - } - - > .time { - margin-left: auto; - opacity: 0.7; - } - } -} - -.knextgwz { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); - grid-gap: var(--margin); - - > .game { - > .players { - text-align: center; - padding: 16px; - line-height: 32px; - - > .avatar { - width: 32px; - height: 32px; - - &:first-child { - margin-right: 8px; - } - - &:last-child { - margin-left: 8px; - } - } - } - - > footer { - display: flex; - align-items: baseline; - border-top: solid 0.5px var(--divider); - padding: 6px 8px; - font-size: 0.9em; - - > .state { - &.playing { - color: var(--accent); - } - } - - > .time { - margin-left: auto; - opacity: 0.7; - } - } - } -} -</style> diff --git a/src/client/pages/room/preview.vue b/src/client/pages/room/preview.vue deleted file mode 100644 index 0cb6bcf04c..0000000000 --- a/src/client/pages/room/preview.vue +++ /dev/null @@ -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 '@client/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> diff --git a/src/client/pages/room/room.vue b/src/client/pages/room/room.vue deleted file mode 100644 index 671dca3577..0000000000 --- a/src/client/pages/room/room.vue +++ /dev/null @@ -1,285 +0,0 @@ -<template> -<div class="hveuntkp"> - <div class="controller _section" v-if="objectSelected"> - <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 @click="translate()" :primary="isTranslateMode"><i class="fas fa-arrows-alt"></i> {{ $ts._rooms.translate }}</MkButton> - <MkButton inline @click="rotate()" :primary="isRotateMode"><i class="fas fa-undo"></i> {{ $ts._rooms.rotate }}</MkButton> - <MkButton inline v-if="isTranslateMode || isRotateMode" @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 class="menu _section" v-if="isMyRoom"> - <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 '@client/scripts/room/room'; -import { parseAcct } from '@/misc/acct'; -import XPreview from './preview.vue'; -const storeItems = require('@client/scripts/room/furnitures.json5'); -import { query as urlQuery } from '../../../prelude/url'; -import MkButton from '@client/components/ui/button.vue'; -import MkSelect from '@client/components/form/select.vue'; -import { selectFile } from '@client/scripts/select-file'; -import * as os from '@client/os'; -import { ColdDeviceStorage } from '@client/store'; -import * as symbols from '@client/symbols'; - -let room: Room; - -export default defineComponent({ - components: { - XPreview, - MkButton, - MkSelect, - }, - - 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', { - ...parseAcct(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'), - }); - }, - - beforeRouteLeave(to, from, next) { - if (this.changed) { - os.dialog({ - type: 'warning', - text: this.$ts.leaveConfirm, - showCancelButton: true - }).then(({ canceled }) => { - if (canceled) { - next(false); - } else { - next(); - } - }); - } else { - next(); - } - }, - - 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.dialog({ - type: null, - title: this.$ts._rooms.addFurniture, - select: { - items: storeItems.map(item => ({ - value: item.id, text: this.$t('_rooms._furnitures.' + item.id) - })) - }, - showCancelButton: true - }); - 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.dialog({ - type: 'error', - text: e.message - }); - }); - }, - - clear() { - os.dialog({ - type: 'warning', - text: this.$ts._rooms.clearConfirm, - showCancelButton: true - }).then(({ canceled }) => { - if (canceled) return; - room.removeAllFurnitures(); - this.changed = true; - }); - }, - - chooseImage(key, e) { - selectFile(e.currentTarget || e.target, null, false).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> diff --git a/src/client/pages/scratchpad.vue b/src/client/pages/scratchpad.vue deleted file mode 100644 index 99164ec51f..0000000000 --- a/src/client/pages/scratchpad.vue +++ /dev/null @@ -1,149 +0,0 @@ -<template> -<div class="iltifgqe"> - <div class="editor _panel _gap"> - <PrismEditor class="_code code" v-model="code" :highlight="highlighter" :line-numbers="false"/> - <MkButton style="position: absolute; top: 8px; right: 8px;" @click="run()" primary><i class="fas fa-play"></i></MkButton> - </div> - - <MkContainer :foldable="true" class="_gap"> - <template #header>{{ $ts.output }}</template> - <div class="bepmlvbi"> - <div v-for="log in logs" class="log" :key="log.id" :class="{ print: log.print }">{{ log.text }}</div> - </div> - </MkContainer> - - <div class="_gap"> - {{ $ts.scratchpadDescription }} - </div> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import 'prismjs'; -import { highlight, languages } from 'prismjs/components/prism-core'; -import 'prismjs/components/prism-clike'; -import 'prismjs/components/prism-javascript'; -import 'prismjs/themes/prism-okaidia.css'; -import { PrismEditor } from 'vue-prism-editor'; -import 'vue-prism-editor/dist/prismeditor.min.css'; -import { AiScript, parse, utils, values } from '@syuilo/aiscript'; -import MkContainer from '@client/components/ui/container.vue'; -import MkButton from '@client/components/ui/button.vue'; -import { createAiScriptEnv } from '@client/scripts/aiscript/api'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - MkContainer, - MkButton, - PrismEditor, - }, - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.scratchpad, - icon: 'fas fa-terminal', - }, - code: '', - logs: [], - } - }, - - watch: { - code() { - localStorage.setItem('scratchpad', this.code); - } - }, - - created() { - const saved = localStorage.getItem('scratchpad'); - if (saved) { - this.code = saved; - } - }, - - methods: { - async run() { - this.logs = []; - const aiscript = new AiScript(createAiScriptEnv({ - storageKey: 'scratchpad', - token: this.$i?.token, - }), { - in: (q) => { - return new Promise(ok => { - os.dialog({ - title: q, - input: {} - }).then(({ canceled, result: a }) => { - ok(a); - }); - }); - }, - out: (value) => { - this.logs.push({ - id: Math.random(), - text: value.type === 'str' ? value.value : utils.valToString(value), - print: true - }); - }, - log: (type, params) => { - switch (type) { - case 'end': this.logs.push({ - id: Math.random(), - text: utils.valToString(params.val, true), - print: false - }); break; - default: break; - } - } - }); - - let ast; - try { - ast = parse(this.code); - } catch (e) { - os.dialog({ - type: 'error', - text: 'Syntax error :(' - }); - return; - } - try { - await aiscript.exec(ast); - } catch (e) { - os.dialog({ - type: 'error', - text: e - }); - } - }, - - highlighter(code) { - return highlight(code, languages.js, 'javascript'); - }, - } -}); -</script> - -<style lang="scss" scoped> -.iltifgqe { - padding: 16px; - - > .editor { - position: relative; - } -} - -.bepmlvbi { - padding: 16px; - - > .log { - &:not(.print) { - opacity: 0.7; - } - } -} -</style> diff --git a/src/client/pages/search.vue b/src/client/pages/search.vue deleted file mode 100644 index 8cf4d32a8f..0000000000 --- a/src/client/pages/search.vue +++ /dev/null @@ -1,53 +0,0 @@ -<template> -<div class="_section"> - <div class="_content"> - <XNotes ref="notes" :pagination="pagination" @before="before" @after="after"/> - </div> -</div> -</template> - -<script lang="ts"> -import { computed, defineComponent } from 'vue'; -import Progress from '@client/scripts/loading'; -import XNotes from '@client/components/notes.vue'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - XNotes - }, - - data() { - return { - [symbols.PAGE_INFO]: { - title: computed(() => this.$t('searchWith', { q: this.$route.query.q })), - icon: 'fas fa-search', - }, - pagination: { - endpoint: 'notes/search', - limit: 10, - params: () => ({ - query: this.$route.query.q, - channelId: this.$route.query.channel, - }) - }, - }; - }, - - watch: { - $route() { - (this.$refs.notes as any).reload(); - } - }, - - methods: { - before() { - Progress.start(); - }, - - after() { - Progress.done(); - } - } -}); -</script> diff --git a/src/client/pages/settings/2fa.vue b/src/client/pages/settings/2fa.vue deleted file mode 100644 index 386e7c635a..0000000000 --- a/src/client/pages/settings/2fa.vue +++ /dev/null @@ -1,247 +0,0 @@ -<template> -<section class="_card"> - <div class="_title"><i class="fas fa-lock"></i> {{ $ts.twoStepAuthentication }}</div> - <div class="_content"> - <MkButton v-if="!data && !$i.twoFactorEnabled" @click="register">{{ $ts._2fa.registerDevice }}</MkButton> - <template v-if="$i.twoFactorEnabled"> - <p>{{ $ts._2fa.alreadyRegistered }}</p> - <MkButton @click="unregister">{{ $ts.unregister }}</MkButton> - - <template v-if="supportsCredentials"> - <hr class="totp-method-sep"> - - <h2 class="heading">{{ $ts.securityKey }}</h2> - <p>{{ $ts._2fa.securityKeyInfo }}</p> - <div class="key-list"> - <div class="key" v-for="key in $i.securityKeysList"> - <h3>{{ key.name }}</h3> - <div class="last-used">{{ $ts.lastUsed }}<MkTime :time="key.lastUsed"/></div> - <MkButton @click="unregisterKey(key)">{{ $ts.unregister }}</MkButton> - </div> - </div> - - <MkSwitch v-model="usePasswordLessLogin" @update:modelValue="updatePasswordLessLogin" v-if="$i.securityKeysList.length > 0">{{ $ts.passwordLessLogin }}</MkSwitch> - - <MkInfo warn v-if="registration && registration.error">{{ $ts.error }} {{ registration.error }}</MkInfo> - <MkButton v-if="!registration || registration.error" @click="addSecurityKey">{{ $ts._2fa.registerKey }}</MkButton> - - <ol v-if="registration && !registration.error"> - <li v-if="registration.stage >= 0"> - {{ $ts.tapSecurityKey }} - <i v-if="registration.saving && registration.stage == 0" class="fas fa-spinner fa-pulse fa-fw"></i> - </li> - <li v-if="registration.stage >= 1"> - <MkForm :disabled="registration.stage != 1 || registration.saving"> - <MkInput v-model="keyName" :max="30"> - <template #label>{{ $ts.securityKeyName }}</template> - </MkInput> - <MkButton @click="registerKey" :disabled="keyName.length == 0">{{ $ts.registerSecurityKey }}</MkButton> - <i v-if="registration.saving && registration.stage == 1" class="fas fa-spinner fa-pulse fa-fw"></i> - </MkForm> - </li> - </ol> - </template> - </template> - <div v-if="data && !$i.twoFactorEnabled"> - <ol style="margin: 0; padding: 0 0 0 1em;"> - <li> - <I18n :src="$ts._2fa.step1" tag="span"> - <template #a> - <a href="https://authy.com/" rel="noopener" target="_blank" class="_link">Authy</a> - </template> - <template #b> - <a href="https://support.google.com/accounts/answer/1066447" rel="noopener" target="_blank" class="_link">Google Authenticator</a> - </template> - </I18n> - </li> - <li>{{ $ts._2fa.step2 }}<br><img :src="data.qr"></li> - <li>{{ $ts._2fa.step3 }}<br> - <MkInput v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" spellcheck="false"><template #label>{{ $ts.token }}</template></MkInput> - <MkButton primary @click="submit">{{ $ts.done }}</MkButton> - </li> - </ol> - <MkInfo>{{ $ts._2fa.step4 }}</MkInfo> - </div> - </div> -</section> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import { hostname } from '@client/config'; -import { byteify, hexify, stringify } from '@client/scripts/2fa'; -import MkButton from '@client/components/ui/button.vue'; -import MkInfo from '@client/components/ui/info.vue'; -import MkInput from '@client/components/form/input.vue'; -import MkSwitch from '@client/components/form/switch.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - FormBase, - MkButton, MkInfo, MkInput, MkSwitch - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.twoStepAuthentication, - icon: 'fas fa-lock' - }, - data: null, - supportsCredentials: !!navigator.credentials, - usePasswordLessLogin: this.$i.usePasswordLessLogin, - registration: null, - keyName: '', - token: null, - }; - }, - - methods: { - register() { - os.dialog({ - title: this.$ts.password, - input: { - type: 'password' - } - }).then(({ canceled, result: password }) => { - if (canceled) return; - os.api('i/2fa/register', { - password: password - }).then(data => { - this.data = data; - }); - }); - }, - - unregister() { - os.dialog({ - title: this.$ts.password, - input: { - type: 'password' - } - }).then(({ canceled, result: password }) => { - if (canceled) return; - os.api('i/2fa/unregister', { - password: password - }).then(() => { - this.usePasswordLessLogin = false; - this.updatePasswordLessLogin(); - }).then(() => { - os.success(); - this.$i.twoFactorEnabled = false; - }); - }); - }, - - submit() { - os.api('i/2fa/done', { - token: this.token - }).then(() => { - os.success(); - this.$i.twoFactorEnabled = true; - }).catch(e => { - os.dialog({ - type: 'error', - text: e - }); - }); - }, - - registerKey() { - this.registration.saving = true; - os.api('i/2fa/key-done', { - password: this.registration.password, - name: this.keyName, - challengeId: this.registration.challengeId, - // we convert each 16 bits to a string to serialise - clientDataJSON: stringify(this.registration.credential.response.clientDataJSON), - attestationObject: hexify(this.registration.credential.response.attestationObject) - }).then(key => { - this.registration = null; - key.lastUsed = new Date(); - os.success(); - }) - }, - - unregisterKey(key) { - os.dialog({ - title: this.$ts.password, - input: { - type: 'password' - } - }).then(({ canceled, result: password }) => { - if (canceled) return; - return os.api('i/2fa/remove-key', { - password, - credentialId: key.id - }).then(() => { - this.usePasswordLessLogin = false; - this.updatePasswordLessLogin(); - }).then(() => { - os.success(); - }); - }); - }, - - addSecurityKey() { - os.dialog({ - title: this.$ts.password, - input: { - type: 'password' - } - }).then(({ canceled, result: password }) => { - if (canceled) return; - os.api('i/2fa/register-key', { - password - }).then(registration => { - this.registration = { - password, - challengeId: registration.challengeId, - stage: 0, - publicKeyOptions: { - challenge: byteify(registration.challenge, 'base64'), - rp: { - id: hostname, - name: 'Misskey' - }, - user: { - id: byteify(this.$i.id, 'ascii'), - name: this.$i.username, - displayName: this.$i.name, - }, - pubKeyCredParams: [{ alg: -7, type: 'public-key' }], - timeout: 60000, - attestation: 'direct' - }, - saving: true - }; - return navigator.credentials.create({ - publicKey: this.registration.publicKeyOptions - }); - }).then(credential => { - this.registration.credential = credential; - this.registration.saving = false; - this.registration.stage = 1; - }).catch(err => { - console.warn('Error while registering?', err); - this.registration.error = err.message; - this.registration.stage = -1; - }); - }); - }, - - updatePasswordLessLogin() { - os.api('i/2fa/password-less', { - value: !!this.usePasswordLessLogin - }); - } - } -}); -</script> diff --git a/src/client/pages/settings/account-info.vue b/src/client/pages/settings/account-info.vue deleted file mode 100644 index 16ce91b12f..0000000000 --- a/src/client/pages/settings/account-info.vue +++ /dev/null @@ -1,185 +0,0 @@ -<template> -<FormBase> - <FormKeyValueView> - <template #key>ID</template> - <template #value><span class="_monospace">{{ $i.id }}</span></template> - </FormKeyValueView> - - <FormGroup> - <FormKeyValueView> - <template #key>{{ $ts.registeredDate }}</template> - <template #value><MkTime :time="$i.createdAt" mode="detail"/></template> - </FormKeyValueView> - </FormGroup> - - <FormGroup v-if="stats"> - <template #label>{{ $ts.statistics }}</template> - <FormKeyValueView> - <template #key>{{ $ts.notesCount }}</template> - <template #value>{{ number(stats.notesCount) }}</template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>{{ $ts.repliesCount }}</template> - <template #value>{{ number(stats.repliesCount) }}</template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>{{ $ts.renotesCount }}</template> - <template #value>{{ number(stats.renotesCount) }}</template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>{{ $ts.repliedCount }}</template> - <template #value>{{ number(stats.repliedCount) }}</template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>{{ $ts.renotedCount }}</template> - <template #value>{{ number(stats.renotedCount) }}</template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>{{ $ts.pollVotesCount }}</template> - <template #value>{{ number(stats.pollVotesCount) }}</template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>{{ $ts.pollVotedCount }}</template> - <template #value>{{ number(stats.pollVotedCount) }}</template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>{{ $ts.sentReactionsCount }}</template> - <template #value>{{ number(stats.sentReactionsCount) }}</template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>{{ $ts.receivedReactionsCount }}</template> - <template #value>{{ number(stats.receivedReactionsCount) }}</template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>{{ $ts.noteFavoritesCount }}</template> - <template #value>{{ number(stats.noteFavoritesCount) }}</template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>{{ $ts.followingCount }}</template> - <template #value>{{ number(stats.followingCount) }}</template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>{{ $ts.followingCount }} ({{ $ts.local }})</template> - <template #value>{{ number(stats.localFollowingCount) }}</template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>{{ $ts.followingCount }} ({{ $ts.remote }})</template> - <template #value>{{ number(stats.remoteFollowingCount) }}</template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>{{ $ts.followersCount }}</template> - <template #value>{{ number(stats.followersCount) }}</template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>{{ $ts.followersCount }} ({{ $ts.local }})</template> - <template #value>{{ number(stats.localFollowersCount) }}</template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>{{ $ts.followersCount }} ({{ $ts.remote }})</template> - <template #value>{{ number(stats.remoteFollowersCount) }}</template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>{{ $ts.pageLikesCount }}</template> - <template #value>{{ number(stats.pageLikesCount) }}</template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>{{ $ts.pageLikedCount }}</template> - <template #value>{{ number(stats.pageLikedCount) }}</template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>{{ $ts.driveFilesCount }}</template> - <template #value>{{ number(stats.driveFilesCount) }}</template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>{{ $ts.driveUsage }}</template> - <template #value>{{ bytes(stats.driveUsage) }}</template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>{{ $ts.reversiCount }}</template> - <template #value>{{ number(stats.reversiCount) }}</template> - </FormKeyValueView> - </FormGroup> - - <FormGroup> - <template #label>{{ $ts.other }}</template> - <FormKeyValueView> - <template #key>emailVerified</template> - <template #value>{{ $i.emailVerified ? $ts.yes : $ts.no }}</template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>twoFactorEnabled</template> - <template #value>{{ $i.twoFactorEnabled ? $ts.yes : $ts.no }}</template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>securityKeys</template> - <template #value>{{ $i.securityKeys ? $ts.yes : $ts.no }}</template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>usePasswordLessLogin</template> - <template #value>{{ $i.usePasswordLessLogin ? $ts.yes : $ts.no }}</template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>isModerator</template> - <template #value>{{ $i.isModerator ? $ts.yes : $ts.no }}</template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>isAdmin</template> - <template #value>{{ $i.isAdmin ? $ts.yes : $ts.no }}</template> - </FormKeyValueView> - </FormGroup> -</FormBase> -</template> - -<script lang="ts"> -import { defineAsyncComponent, defineComponent } from 'vue'; -import FormSwitch from '@client/components/form/switch.vue'; -import FormSelect from '@client/components/form/select.vue'; -import FormLink from '@client/components/debobigego/link.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import FormKeyValueView from '@client/components/debobigego/key-value-view.vue'; -import * as os from '@client/os'; -import number from '@client/filters/number'; -import bytes from '@client/filters/bytes'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - FormBase, - FormSelect, - FormSwitch, - FormButton, - FormLink, - FormGroup, - FormKeyValueView, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.accountInfo, - icon: 'fas fa-info-circle' - }, - stats: null - } - }, - - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - - os.api('users/stats', { - userId: this.$i.id - }).then(stats => { - this.stats = stats; - }); - }, - - methods: { - number, - bytes, - } -}); -</script> diff --git a/src/client/pages/settings/accounts.vue b/src/client/pages/settings/accounts.vue deleted file mode 100644 index d2966cc216..0000000000 --- a/src/client/pages/settings/accounts.vue +++ /dev/null @@ -1,149 +0,0 @@ -<template> -<FormBase> - <FormSuspense :p="init"> - <FormButton @click="addAccount" primary><i class="fas fa-plus"></i> {{ $ts.addAccount }}</FormButton> - - <div class="_debobigegoItem _button" v-for="account in accounts" :key="account.id" @click="menu(account, $event)"> - <div class="_debobigegoPanel lcjjdxlm"> - <div class="avatar"> - <MkAvatar :user="account" class="avatar"/> - </div> - <div class="body"> - <div class="name"> - <MkUserName :user="account"/> - </div> - <div class="acct"> - <MkAcct :user="account"/> - </div> - </div> - </div> - </div> - </FormSuspense> -</FormBase> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import FormSuspense from '@client/components/debobigego/suspense.vue'; -import FormLink from '@client/components/debobigego/link.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; -import { getAccounts, addAccount, login } from '@client/account'; - -export default defineComponent({ - components: { - FormBase, - FormSuspense, - FormButton, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.accounts, - icon: 'fas fa-users', - bg: 'var(--bg)', - }, - storedAccounts: getAccounts().then(accounts => accounts.filter(x => x.id !== this.$i.id)), - accounts: null, - init: async () => os.api('users/show', { - userIds: (await this.storedAccounts).map(x => x.id) - }).then(accounts => { - this.accounts = accounts; - }), - }; - }, - - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - methods: { - menu(account, ev) { - os.popupMenu([{ - text: this.$ts.switch, - icon: 'fas fa-exchange-alt', - action: () => this.switchAccount(account), - }, { - text: this.$ts.remove, - icon: 'fas fa-trash-alt', - danger: true, - action: () => this.removeAccount(account), - }], ev.currentTarget || ev.target); - }, - - addAccount(ev) { - os.popupMenu([{ - text: this.$ts.existingAccount, - action: () => { this.addExistingAccount(); }, - }, { - text: this.$ts.createAccount, - action: () => { this.createAccount(); }, - }], ev.currentTarget || ev.target); - }, - - addExistingAccount() { - os.popup(import('@client/components/signin-dialog.vue'), {}, { - done: res => { - addAccount(res.id, res.i); - os.success(); - }, - }, 'closed'); - }, - - createAccount() { - os.popup(import('@client/components/signup-dialog.vue'), {}, { - done: res => { - addAccount(res.id, res.i); - this.switchAccountWithToken(res.i); - }, - }, 'closed'); - }, - - async switchAccount(account: any) { - const storedAccounts = await getAccounts(); - const token = storedAccounts.find(x => x.id === account.id).token; - this.switchAccountWithToken(token); - }, - - switchAccountWithToken(token: string) { - login(token); - }, - } -}); -</script> - -<style lang="scss" scoped> -.lcjjdxlm { - display: flex; - padding: 16px; - - > .avatar { - display: block; - flex-shrink: 0; - margin: 0 12px 0 0; - - > .avatar { - width: 50px; - height: 50px; - } - } - - > .body { - display: flex; - flex-direction: column; - justify-content: center; - width: calc(100% - 62px); - position: relative; - - > .name { - font-weight: bold; - } - } -} -</style> diff --git a/src/client/pages/settings/api.vue b/src/client/pages/settings/api.vue deleted file mode 100644 index 5c7496e2f9..0000000000 --- a/src/client/pages/settings/api.vue +++ /dev/null @@ -1,65 +0,0 @@ -<template> -<FormBase> - <FormButton @click="generateToken" primary>{{ $ts.generateAccessToken }}</FormButton> - <FormLink to="/settings/apps">{{ $ts.manageAccessTokens }}</FormLink> - <FormLink to="/api-console" :behavior="isDesktop ? 'window' : null">API console</FormLink> -</FormBase> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import FormSwitch from '@client/components/form/switch.vue'; -import FormSelect from '@client/components/form/select.vue'; -import FormLink from '@client/components/debobigego/link.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - FormBase, - FormButton, - FormLink, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: 'API', - icon: 'fas fa-key', - bg: 'var(--bg)', - }, - isDesktop: window.innerWidth >= 1100, - }; - }, - - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - methods: { - generateToken() { - os.popup(import('@client/components/token-generate-window.vue'), {}, { - done: async result => { - const { name, permissions } = result; - const { token } = await os.api('miauth/gen-token', { - session: null, - name: name, - permission: permissions, - }); - - os.dialog({ - type: 'success', - title: this.$ts.token, - text: token - }); - }, - }, 'closed'); - }, - } -}); -</script> diff --git a/src/client/pages/settings/apps.vue b/src/client/pages/settings/apps.vue deleted file mode 100644 index da4f672adf..0000000000 --- a/src/client/pages/settings/apps.vue +++ /dev/null @@ -1,113 +0,0 @@ -<template> -<FormBase> - <FormPagination :pagination="pagination" ref="list"> - <template #empty> - <div class="_fullinfo"> - <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> - <div>{{ $ts.nothing }}</div> - </div> - </template> - <template #default="{items}"> - <div class="_debobigegoPanel bfomjevm" v-for="token in items" :key="token.id"> - <img class="icon" :src="token.iconUrl" alt="" v-if="token.iconUrl"/> - <div class="body"> - <div class="name">{{ token.name }}</div> - <div class="description">{{ token.description }}</div> - <div class="_keyValue"> - <div>{{ $ts.installedDate }}:</div> - <div><MkTime :time="token.createdAt"/></div> - </div> - <div class="_keyValue"> - <div>{{ $ts.lastUsedDate }}:</div> - <div><MkTime :time="token.lastUsedAt"/></div> - </div> - <div class="actions"> - <button class="_button" @click="revoke(token)"><i class="fas fa-trash-alt"></i></button> - </div> - <details> - <summary>{{ $ts.details }}</summary> - <ul> - <li v-for="p in token.permission" :key="p">{{ $t(`_permissions.${p}`) }}</li> - </ul> - </details> - </div> - </div> - </template> - </FormPagination> -</FormBase> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import FormPagination from '@client/components/debobigego/pagination.vue'; -import FormSelect from '@client/components/form/select.vue'; -import FormLink from '@client/components/debobigego/link.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - FormBase, - FormPagination, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.installedApps, - icon: 'fas fa-plug', - bg: 'var(--bg)', - }, - pagination: { - endpoint: 'i/apps', - limit: 100, - params: { - sort: '+lastUsedAt' - } - }, - }; - }, - - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - methods: { - revoke(token) { - os.api('i/revoke-token', { tokenId: token.id }).then(() => { - this.$refs.list.reload(); - }); - } - } -}); -</script> - -<style lang="scss" scoped> -.bfomjevm { - display: flex; - padding: 16px; - - > .icon { - display: block; - flex-shrink: 0; - margin: 0 12px 0 0; - width: 50px; - height: 50px; - border-radius: 8px; - } - - > .body { - width: calc(100% - 62px); - position: relative; - - > .name { - font-weight: bold; - } - } -} -</style> diff --git a/src/client/pages/settings/custom-css.vue b/src/client/pages/settings/custom-css.vue deleted file mode 100644 index fd473a11fa..0000000000 --- a/src/client/pages/settings/custom-css.vue +++ /dev/null @@ -1,73 +0,0 @@ -<template> -<FormBase> - <FormInfo warn>{{ $ts.customCssWarn }}</FormInfo> - - <FormTextarea v-model="localCustomCss" manual-save tall class="_monospace" style="tab-size: 2;"> - <span>{{ $ts.local }}</span> - </FormTextarea> -</FormBase> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import FormTextarea from '@client/components/form/textarea.vue'; -import FormSelect from '@client/components/form/select.vue'; -import FormRadios from '@client/components/form/radios.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import FormLink from '@client/components/debobigego/link.vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import FormInfo from '@client/components/debobigego/info.vue'; -import * as os from '@client/os'; -import { ColdDeviceStorage } from '@client/store'; -import { unisonReload } from '@client/scripts/unison-reload'; -import * as symbols from '@client/symbols'; -import { defaultStore } from '@client/store'; - -export default defineComponent({ - components: { - FormTextarea, - FormSelect, - FormRadios, - FormBase, - FormGroup, - FormLink, - FormButton, - FormInfo, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.customCss, - icon: 'fas fa-code', - bg: 'var(--bg)', - }, - localCustomCss: localStorage.getItem('customCss') - } - }, - - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - - this.$watch('localCustomCss', this.apply); - }, - - methods: { - async apply() { - localStorage.setItem('customCss', this.localCustomCss); - - const { canceled } = await os.dialog({ - type: 'info', - text: this.$ts.reloadToApplySetting, - showCancelButton: true - }); - if (canceled) return; - - unisonReload(); - } - } -}); -</script> diff --git a/src/client/pages/settings/deck.vue b/src/client/pages/settings/deck.vue deleted file mode 100644 index e4b5c697c4..0000000000 --- a/src/client/pages/settings/deck.vue +++ /dev/null @@ -1,107 +0,0 @@ -<template> -<FormBase> - <FormGroup> - <template #label>{{ $ts.defaultNavigationBehaviour }}</template> - <FormSwitch v-model="navWindow">{{ $ts.openInWindow }}</FormSwitch> - </FormGroup> - - <FormSwitch v-model="alwaysShowMainColumn">{{ $ts._deck.alwaysShowMainColumn }}</FormSwitch> - - <FormRadios v-model="columnAlign"> - <template #desc>{{ $ts._deck.columnAlign }}</template> - <option value="left">{{ $ts.left }}</option> - <option value="center">{{ $ts.center }}</option> - </FormRadios> - - <FormRadios v-model="columnHeaderHeight"> - <template #desc>{{ $ts._deck.columnHeaderHeight }}</template> - <option :value="42">{{ $ts.narrow }}</option> - <option :value="45">{{ $ts.medium }}</option> - <option :value="48">{{ $ts.wide }}</option> - </FormRadios> - - <FormInput v-model="columnMargin" type="number"> - <span>{{ $ts._deck.columnMargin }}</span> - <template #suffix>px</template> - </FormInput> - - <FormLink @click="setProfile">{{ $ts._deck.profile }}<template #suffix>{{ profile }}</template></FormLink> -</FormBase> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import FormSwitch from '@client/components/debobigego/switch.vue'; -import FormLink from '@client/components/debobigego/link.vue'; -import FormRadios from '@client/components/debobigego/radios.vue'; -import FormInput from '@client/components/debobigego/input.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import { deckStore } from '@client/ui/deck/deck-store'; -import * as os from '@client/os'; -import { unisonReload } from '@client/scripts/unison-reload'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - FormSwitch, - FormLink, - FormInput, - FormRadios, - FormBase, - FormGroup, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.deck, - icon: 'fas fa-columns', - bg: 'var(--bg)', - }, - } - }, - - computed: { - navWindow: deckStore.makeGetterSetter('navWindow'), - alwaysShowMainColumn: deckStore.makeGetterSetter('alwaysShowMainColumn'), - columnAlign: deckStore.makeGetterSetter('columnAlign'), - columnMargin: deckStore.makeGetterSetter('columnMargin'), - columnHeaderHeight: deckStore.makeGetterSetter('columnHeaderHeight'), - profile: deckStore.makeGetterSetter('profile'), - }, - - watch: { - async navWindow() { - const { canceled } = await os.dialog({ - type: 'info', - text: this.$ts.reloadToApplySetting, - showCancelButton: true - }); - if (canceled) return; - - unisonReload(); - } - }, - - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - methods: { - async setProfile() { - const { canceled, result: name } = await os.dialog({ - title: this.$ts._deck.profile, - input: { - allowEmpty: false - } - }); - if (canceled) return; - this.profile = name; - unisonReload(); - } - } -}); -</script> diff --git a/src/client/pages/settings/delete-account.vue b/src/client/pages/settings/delete-account.vue deleted file mode 100644 index 6bac214e04..0000000000 --- a/src/client/pages/settings/delete-account.vue +++ /dev/null @@ -1,68 +0,0 @@ -<template> -<FormBase> - <FormInfo warn>{{ $ts._accountDelete.mayTakeTime }}</FormInfo> - <FormInfo>{{ $ts._accountDelete.sendEmail }}</FormInfo> - <FormButton @click="deleteAccount" danger v-if="!$i.isDeleted">{{ $ts._accountDelete.requestAccountDelete }}</FormButton> - <FormButton disabled v-else>{{ $ts._accountDelete.inProgress }}</FormButton> -</FormBase> -</template> - -<script lang="ts"> -import { defineAsyncComponent, defineComponent } from 'vue'; -import FormInfo from '@client/components/debobigego/info.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import * as os from '@client/os'; -import { debug } from '@client/config'; -import { signout } from '@client/account'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - FormBase, - FormButton, - FormGroup, - FormInfo, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts._accountDelete.accountDelete, - icon: 'fas fa-exclamation-triangle', - bg: 'var(--bg)', - }, - debug, - } - }, - - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - methods: { - async deleteAccount() { - const { canceled, result: password } = await os.dialog({ - title: this.$ts.password, - input: { - type: 'password' - } - }); - if (canceled) return; - - await os.apiWithDialog('i/delete-account', { - password: password - }); - - await os.dialog({ - title: this.$ts._accountDelete.started, - }); - - signout(); - } - } -}); -</script> diff --git a/src/client/pages/settings/drive.vue b/src/client/pages/settings/drive.vue deleted file mode 100644 index 2d73eb4df7..0000000000 --- a/src/client/pages/settings/drive.vue +++ /dev/null @@ -1,147 +0,0 @@ -<template> -<FormBase class=""> - <FormGroup v-if="!fetching"> - <template #label>{{ $ts.usageAmount }}</template> - <div class="_debobigegoItem uawsfosz"> - <div class="_debobigegoPanel"> - <div class="meter"><div :style="meterStyle"></div></div> - </div> - </div> - <FormKeyValueView> - <template #key>{{ $ts.capacity }}</template> - <template #value>{{ bytes(capacity, 1) }}</template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>{{ $ts.inUse }}</template> - <template #value>{{ bytes(usage, 1) }}</template> - </FormKeyValueView> - </FormGroup> - - <div class="_debobigegoItem"> - <div class="_debobigegoLabel">{{ $ts.statistics }}</div> - <div class="_debobigegoPanel"> - <div ref="chart"></div> - </div> - </div> - - <FormButton :center="false" @click="chooseUploadFolder()" primary> - {{ $ts.uploadFolder }} - <template #suffix>{{ uploadFolder ? uploadFolder.name : '-' }}</template> - <template #suffixIcon><i class="fas fa-folder-open"></i></template> - </FormButton> -</FormBase> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import * as tinycolor from 'tinycolor2'; -import FormButton from '@client/components/debobigego/button.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import FormKeyValueView from '@client/components/debobigego/key-value-view.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import * as os from '@client/os'; -import bytes from '@client/filters/bytes'; -import * as symbols from '@client/symbols'; - -// TODO: render chart - -export default defineComponent({ - components: { - FormBase, - FormButton, - FormGroup, - FormKeyValueView, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.drive, - icon: 'fas fa-cloud', - bg: 'var(--bg)', - }, - fetching: true, - usage: null, - capacity: null, - uploadFolder: null, - } - }, - - computed: { - meterStyle(): any { - return { - width: `${this.usage / this.capacity * 100}%`, - background: tinycolor({ - h: 180 - (this.usage / this.capacity * 180), - s: 0.7, - l: 0.5 - }) - }; - } - }, - - async created() { - os.api('drive').then(info => { - this.capacity = info.capacity; - this.usage = info.usage; - this.fetching = false; - this.$nextTick(() => { - this.renderChart(); - }); - }); - - if (this.$store.state.uploadFolder) { - this.uploadFolder = await os.api('drive/folders/show', { - folderId: this.$store.state.uploadFolder - }); - } - }, - - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - methods: { - chooseUploadFolder() { - os.selectDriveFolder(false).then(async folder => { - this.$store.set('uploadFolder', folder ? folder.id : null); - os.success(); - if (this.$store.state.uploadFolder) { - this.uploadFolder = await os.api('drive/folders/show', { - folderId: this.$store.state.uploadFolder - }); - } else { - this.uploadFolder = null; - } - }); - }, - - bytes - } -}); -</script> - -<style lang="scss" scoped> - -@use "sass:math"; - -.uawsfosz { - > div { - padding: 24px; - - > .meter { - $size: 12px; - background: rgba(0, 0, 0, 0.1); - border-radius: math.div($size, 2); - overflow: hidden; - - > div { - height: $size; - border-radius: math.div($size, 2); - } - } - } -} -</style> diff --git a/src/client/pages/settings/email-address.vue b/src/client/pages/settings/email-address.vue deleted file mode 100644 index f98b22ada7..0000000000 --- a/src/client/pages/settings/email-address.vue +++ /dev/null @@ -1,70 +0,0 @@ -<template> -<FormBase> - <FormGroup> - <FormInput v-model="emailAddress" type="email"> - {{ $ts.emailAddress }} - <template #desc v-if="$i.email && !$i.emailVerified">{{ $ts.verificationEmailSent }}</template> - <template #desc v-else-if="emailAddress === $i.email && $i.emailVerified">{{ $ts.emailVerified }}</template> - </FormInput> - </FormGroup> - <FormButton @click="save" primary>{{ $ts.save }}</FormButton> -</FormBase> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import FormInput from '@client/components/form/input.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - FormBase, - FormInput, - FormButton, - FormGroup, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.emailAddress, - icon: 'fas fa-envelope', - bg: 'var(--bg)', - }, - emailAddress: null, - code: null, - } - }, - - created() { - this.emailAddress = this.$i.email; - }, - - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - methods: { - save() { - os.dialog({ - title: this.$ts.password, - input: { - type: 'password' - } - }).then(({ canceled, result: password }) => { - if (canceled) return; - os.apiWithDialog('i/update-email', { - password: password, - email: this.emailAddress, - }); - }); - } - } -}); -</script> diff --git a/src/client/pages/settings/email-notification.vue b/src/client/pages/settings/email-notification.vue deleted file mode 100644 index 1b78621c3f..0000000000 --- a/src/client/pages/settings/email-notification.vue +++ /dev/null @@ -1,91 +0,0 @@ -<template> -<FormBase> - <FormGroup> - <FormSwitch v-model="mention"> - {{ $ts._notification._types.mention }} - </FormSwitch> - <FormSwitch v-model="reply"> - {{ $ts._notification._types.reply }} - </FormSwitch> - <FormSwitch v-model="quote"> - {{ $ts._notification._types.quote }} - </FormSwitch> - <FormSwitch v-model="follow"> - {{ $ts._notification._types.follow }} - </FormSwitch> - <FormSwitch v-model="receiveFollowRequest"> - {{ $ts._notification._types.receiveFollowRequest }} - </FormSwitch> - <FormSwitch v-model="groupInvited"> - {{ $ts._notification._types.groupInvited }} - </FormSwitch> - </FormGroup> -</FormBase> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import FormSwitch from '@client/components/form/switch.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - FormBase, - FormSwitch, - FormButton, - FormGroup, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.emailNotification, - icon: 'fas fa-envelope', - bg: 'var(--bg)', - }, - - mention: this.$i.emailNotificationTypes.includes('mention'), - reply: this.$i.emailNotificationTypes.includes('reply'), - quote: this.$i.emailNotificationTypes.includes('quote'), - follow: this.$i.emailNotificationTypes.includes('follow'), - receiveFollowRequest: this.$i.emailNotificationTypes.includes('receiveFollowRequest'), - groupInvited: this.$i.emailNotificationTypes.includes('groupInvited'), - } - }, - - created() { - this.$watch('mention', this.save); - this.$watch('reply', this.save); - this.$watch('quote', this.save); - this.$watch('follow', this.save); - this.$watch('receiveFollowRequest', this.save); - this.$watch('groupInvited', this.save); - }, - - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - methods: { - save() { - os.api('i/update', { - emailNotificationTypes: [ - ...[this.mention ? 'mention' : null], - ...[this.reply ? 'reply' : null], - ...[this.quote ? 'quote' : null], - ...[this.follow ? 'follow' : null], - ...[this.receiveFollowRequest ? 'receiveFollowRequest' : null], - ...[this.groupInvited ? 'groupInvited' : null], - ].filter(x => x != null) - }); - } - } -}); -</script> diff --git a/src/client/pages/settings/email.vue b/src/client/pages/settings/email.vue deleted file mode 100644 index adc62133ac..0000000000 --- a/src/client/pages/settings/email.vue +++ /dev/null @@ -1,66 +0,0 @@ -<template> -<FormBase> - <FormGroup> - <template #label>{{ $ts.emailAddress }}</template> - <FormLink to="/settings/email/address"> - <template v-if="$i.email && !$i.emailVerified" #icon><i class="fas fa-exclamation-triangle" style="color: var(--warn);"></i></template> - <template v-else-if="$i.email && $i.emailVerified" #icon><i class="fas fa-check" style="color: var(--success);"></i></template> - {{ $i.email || $ts.notSet }} - </FormLink> - </FormGroup> - - <FormLink to="/settings/email/notification"> - <template #icon><i class="fas fa-bell"></i></template> - {{ $ts.emailNotification }} - </FormLink> - - <FormSwitch :value="$i.receiveAnnouncementEmail" @update:modelValue="onChangeReceiveAnnouncementEmail"> - {{ $ts.receiveAnnouncementFromInstance }} - </FormSwitch> -</FormBase> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import FormLink from '@client/components/debobigego/link.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import FormSwitch from '@client/components/debobigego/switch.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - FormBase, - FormLink, - FormButton, - FormSwitch, - FormGroup, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.email, - icon: 'fas fa-envelope', - bg: 'var(--bg)', - }, - } - }, - - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - methods: { - onChangeReceiveAnnouncementEmail(v) { - os.api('i/update', { - receiveAnnouncementEmail: v - }); - }, - } -}); -</script> diff --git a/src/client/pages/settings/experimental-features.vue b/src/client/pages/settings/experimental-features.vue deleted file mode 100644 index 971c45a628..0000000000 --- a/src/client/pages/settings/experimental-features.vue +++ /dev/null @@ -1,52 +0,0 @@ -<template> -<FormBase> - <FormButton @click="error()">error test</FormButton> -</FormBase> -</template> - -<script lang="ts"> -import { defineAsyncComponent, defineComponent } from 'vue'; -import FormSwitch from '@client/components/form/switch.vue'; -import FormSelect from '@client/components/form/select.vue'; -import FormLink from '@client/components/debobigego/link.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import FormKeyValueView from '@client/components/debobigego/key-value-view.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - FormBase, - FormSelect, - FormSwitch, - FormButton, - FormLink, - FormGroup, - FormKeyValueView, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.experimentalFeatures, - icon: 'fas fa-flask' - }, - stats: null - } - }, - - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - methods: { - error() { - throw new Error('Test error'); - } - } -}); -</script> diff --git a/src/client/pages/settings/general.vue b/src/client/pages/settings/general.vue deleted file mode 100644 index 59dd251948..0000000000 --- a/src/client/pages/settings/general.vue +++ /dev/null @@ -1,223 +0,0 @@ -<template> -<FormBase> - <FormSwitch v-model="showFixedPostForm">{{ $ts.showFixedPostForm }}</FormSwitch> - - <FormSelect v-model="lang"> - <template #label>{{ $ts.uiLanguage }}</template> - <option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option> - <template #caption> - <I18n :src="$ts.i18nInfo" tag="span"> - <template #link> - <MkLink url="https://crowdin.com/project/misskey">Crowdin</MkLink> - </template> - </I18n> - </template> - </FormSelect> - - <FormGroup> - <template #label>{{ $ts.behavior }}</template> - <FormSwitch v-model="imageNewTab">{{ $ts.openImageInNewTab }}</FormSwitch> - <FormSwitch v-model="enableInfiniteScroll">{{ $ts.enableInfiniteScroll }}</FormSwitch> - <FormSwitch v-model="useReactionPickerForContextMenu">{{ $ts.useReactionPickerForContextMenu }}</FormSwitch> - <FormSwitch v-model="disablePagesScript">{{ $ts.disablePagesScript }}</FormSwitch> - </FormGroup> - - <FormSelect v-model="serverDisconnectedBehavior"> - <template #label>{{ $ts.whenServerDisconnected }}</template> - <option value="reload">{{ $ts._serverDisconnectedBehavior.reload }}</option> - <option value="dialog">{{ $ts._serverDisconnectedBehavior.dialog }}</option> - <option value="quiet">{{ $ts._serverDisconnectedBehavior.quiet }}</option> - </FormSelect> - - <FormGroup> - <template #label>{{ $ts.appearance }}</template> - <FormSwitch v-model="disableAnimatedMfm">{{ $ts.disableAnimatedMfm }}</FormSwitch> - <FormSwitch v-model="reduceAnimation">{{ $ts.reduceUiAnimation }}</FormSwitch> - <FormSwitch v-model="useBlurEffect">{{ $ts.useBlurEffect }}</FormSwitch> - <FormSwitch v-model="useBlurEffectForModal">{{ $ts.useBlurEffectForModal }}</FormSwitch> - <FormSwitch v-model="showGapBetweenNotesInTimeline">{{ $ts.showGapBetweenNotesInTimeline }}</FormSwitch> - <FormSwitch v-model="loadRawImages">{{ $ts.loadRawImages }}</FormSwitch> - <FormSwitch v-model="disableShowingAnimatedImages">{{ $ts.disableShowingAnimatedImages }}</FormSwitch> - <FormSwitch v-model="squareAvatars">{{ $ts.squareAvatars }}</FormSwitch> - <FormSwitch v-model="useSystemFont">{{ $ts.useSystemFont }}</FormSwitch> - <FormSwitch v-model="useOsNativeEmojis">{{ $ts.useOsNativeEmojis }} - <div><Mfm text="๐ฎ๐ฆ๐ญ๐ฉ๐ฐ๐ซ๐ฌ๐ฅ๐ช" :key="useOsNativeEmojis"/></div> - </FormSwitch> - </FormGroup> - - <FormGroup> - <FormSwitch v-model="aiChanMode">{{ $ts.aiChanMode }}</FormSwitch> - </FormGroup> - - <FormRadios v-model="fontSize"> - <template #desc>{{ $ts.fontSize }}</template> - <option value="small"><span style="font-size: 14px;">Aa</span></option> - <option :value="null"><span style="font-size: 16px;">Aa</span></option> - <option value="large"><span style="font-size: 18px;">Aa</span></option> - <option value="veryLarge"><span style="font-size: 20px;">Aa</span></option> - </FormRadios> - - <FormSelect v-model="instanceTicker"> - <template #label>{{ $ts.instanceTicker }}</template> - <option value="none">{{ $ts._instanceTicker.none }}</option> - <option value="remote">{{ $ts._instanceTicker.remote }}</option> - <option value="always">{{ $ts._instanceTicker.always }}</option> - </FormSelect> - - <FormSelect v-model="nsfw"> - <template #label>{{ $ts.nsfw }}</template> - <option value="respect">{{ $ts._nsfw.respect }}</option> - <option value="ignore">{{ $ts._nsfw.ignore }}</option> - <option value="force">{{ $ts._nsfw.force }}</option> - </FormSelect> - - <FormGroup> - <template #label>{{ $ts.defaultNavigationBehaviour }}</template> - <FormSwitch v-model="defaultSideView">{{ $ts.openInSideView }}</FormSwitch> - </FormGroup> - - <FormSelect v-model="chatOpenBehavior"> - <template #label>{{ $ts.chatOpenBehavior }}</template> - <option value="page">{{ $ts.showInPage }}</option> - <option value="window">{{ $ts.openInWindow }}</option> - <option value="popout">{{ $ts.popout }}</option> - </FormSelect> - - <FormLink to="/settings/deck">{{ $ts.deck }}</FormLink> - - <FormLink to="/settings/custom-css"><template #icon><i class="fas fa-code"></i></template>{{ $ts.customCss }}</FormLink> -</FormBase> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import FormSwitch from '@client/components/debobigego/switch.vue'; -import FormSelect from '@client/components/debobigego/select.vue'; -import FormRadios from '@client/components/debobigego/radios.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import FormLink from '@client/components/debobigego/link.vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import MkLink from '@client/components/link.vue'; -import { langs } from '@client/config'; -import { defaultStore } from '@client/store'; -import { ColdDeviceStorage } from '@client/store'; -import * as os from '@client/os'; -import { unisonReload } from '@client/scripts/unison-reload'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - MkLink, - FormSwitch, - FormSelect, - FormRadios, - FormBase, - FormGroup, - FormLink, - FormButton, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.general, - icon: 'fas fa-cogs', - bg: 'var(--bg)' - }, - langs, - lang: localStorage.getItem('lang'), - fontSize: localStorage.getItem('fontSize'), - useSystemFont: localStorage.getItem('useSystemFont') != null, - } - }, - - computed: { - serverDisconnectedBehavior: defaultStore.makeGetterSetter('serverDisconnectedBehavior'), - reduceAnimation: defaultStore.makeGetterSetter('animation', v => !v, v => !v), - useBlurEffectForModal: defaultStore.makeGetterSetter('useBlurEffectForModal'), - useBlurEffect: defaultStore.makeGetterSetter('useBlurEffect'), - showGapBetweenNotesInTimeline: defaultStore.makeGetterSetter('showGapBetweenNotesInTimeline'), - disableAnimatedMfm: defaultStore.makeGetterSetter('animatedMfm', v => !v, v => !v), - useOsNativeEmojis: defaultStore.makeGetterSetter('useOsNativeEmojis'), - disableShowingAnimatedImages: defaultStore.makeGetterSetter('disableShowingAnimatedImages'), - loadRawImages: defaultStore.makeGetterSetter('loadRawImages'), - imageNewTab: defaultStore.makeGetterSetter('imageNewTab'), - nsfw: defaultStore.makeGetterSetter('nsfw'), - disablePagesScript: defaultStore.makeGetterSetter('disablePagesScript'), - showFixedPostForm: defaultStore.makeGetterSetter('showFixedPostForm'), - defaultSideView: defaultStore.makeGetterSetter('defaultSideView'), - chatOpenBehavior: ColdDeviceStorage.makeGetterSetter('chatOpenBehavior'), - instanceTicker: defaultStore.makeGetterSetter('instanceTicker'), - enableInfiniteScroll: defaultStore.makeGetterSetter('enableInfiniteScroll'), - useReactionPickerForContextMenu: defaultStore.makeGetterSetter('useReactionPickerForContextMenu'), - squareAvatars: defaultStore.makeGetterSetter('squareAvatars'), - aiChanMode: defaultStore.makeGetterSetter('aiChanMode'), - }, - - watch: { - lang() { - localStorage.setItem('lang', this.lang); - localStorage.removeItem('locale'); - this.reloadAsk(); - }, - - fontSize() { - if (this.fontSize == null) { - localStorage.removeItem('fontSize'); - } else { - localStorage.setItem('fontSize', this.fontSize); - } - this.reloadAsk(); - }, - - useSystemFont() { - if (this.useSystemFont) { - localStorage.setItem('useSystemFont', 't'); - } else { - localStorage.removeItem('useSystemFont'); - } - this.reloadAsk(); - }, - - enableInfiniteScroll() { - this.reloadAsk(); - }, - - squareAvatars() { - this.reloadAsk(); - }, - - aiChanMode() { - this.reloadAsk(); - }, - - showGapBetweenNotesInTimeline() { - this.reloadAsk(); - }, - - instanceTicker() { - this.reloadAsk(); - }, - }, - - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - methods: { - async reloadAsk() { - const { canceled } = await os.dialog({ - type: 'info', - text: this.$ts.reloadToApplySetting, - showCancelButton: true - }); - if (canceled) return; - - unisonReload(); - } - } -}); -</script> diff --git a/src/client/pages/settings/import-export.vue b/src/client/pages/settings/import-export.vue deleted file mode 100644 index eeaa1f1602..0000000000 --- a/src/client/pages/settings/import-export.vue +++ /dev/null @@ -1,112 +0,0 @@ -<template> -<div style="margin: 16px;"> - <FormSection> - <template #label>{{ $ts._exportOrImport.allNotes }}</template> - <MkButton :class="$style.button" inline @click="doExport('notes')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton> - </FormSection> - <FormSection> - <template #label>{{ $ts._exportOrImport.followingList }}</template> - <MkButton :class="$style.button" inline @click="doExport('following')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton> - <MkButton :class="$style.button" inline @click="doImport('following', $event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton> - </FormSection> - <FormSection> - <template #label>{{ $ts._exportOrImport.userLists }}</template> - <MkButton :class="$style.button" inline @click="doExport('user-lists')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton> - <MkButton :class="$style.button" inline @click="doImport('user-lists', $event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton> - </FormSection> - <FormSection> - <template #label>{{ $ts._exportOrImport.muteList }}</template> - <MkButton :class="$style.button" inline @click="doExport('muting')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton> - <MkButton :class="$style.button" inline @click="doImport('muting', $event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton> - </FormSection> - <FormSection> - <template #label>{{ $ts._exportOrImport.blockingList }}</template> - <MkButton :class="$style.button" inline @click="doExport('blocking')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton> - <MkButton :class="$style.button" inline @click="doImport('blocking', $event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton> - </FormSection> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import MkButton from '@client/components/ui/button.vue'; -import FormSection from '@client/components/form/section.vue'; -import * as os from '@client/os'; -import { selectFile } from '@client/scripts/select-file'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - FormSection, - MkButton, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.importAndExport, - icon: 'fas fa-boxes', - bg: 'var(--bg)', - }, - } - }, - - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - methods: { - doExport(target) { - os.api( - target === 'notes' ? 'i/export-notes' : - target === 'following' ? 'i/export-following' : - target === 'blocking' ? 'i/export-blocking' : - target === 'user-lists' ? 'i/export-user-lists' : - target === 'muting' ? 'i/export-mute' : - null, {}) - .then(() => { - os.dialog({ - type: 'info', - text: this.$ts.exportRequested - }); - }).catch((e: any) => { - os.dialog({ - type: 'error', - text: e.message - }); - }); - }, - - async doImport(target, e) { - const file = await selectFile(e.currentTarget || e.target); - - os.api( - target === 'following' ? 'i/import-following' : - target === 'user-lists' ? 'i/import-user-lists' : - target === 'muting' ? 'i/import-muting' : - target === 'blocking' ? 'i/import-blocking' : - null, { - fileId: file.id - }).then(() => { - os.dialog({ - type: 'info', - text: this.$ts.importRequested - }); - }).catch((e: any) => { - os.dialog({ - type: 'error', - text: e.message - }); - }); - }, - } -}); -</script> - -<style module> -.button { - margin-right: 16px; -} -</style> diff --git a/src/client/pages/settings/index.vue b/src/client/pages/settings/index.vue deleted file mode 100644 index cf053dbe63..0000000000 --- a/src/client/pages/settings/index.vue +++ /dev/null @@ -1,326 +0,0 @@ -<template> -<div class="vvcocwet" :class="{ wide: !narrow }" ref="el"> - <div class="nav" v-if="!narrow || page == null"> - <MkSpacer :content-max="700"> - <div class="baaadecd"> - <div class="title">{{ $ts.settings }}</div> - <MkInfo v-if="emailNotConfigured" warn class="info">{{ $ts.emailNotConfiguredWarning }} <MkA to="/settings/email" class="_link">{{ $ts.configure }}</MkA></MkInfo> - <MkSuperMenu :def="menuDef" :grid="page == null"></MkSuperMenu> - </div> - </MkSpacer> - </div> - <div class="main"> - <component :is="component" :key="page" v-bind="pageProps"/> - </div> -</div> -</template> - -<script lang="ts"> -import { computed, defineAsyncComponent, defineComponent, nextTick, onMounted, reactive, ref, watch } from 'vue'; -import { i18n } from '@client/i18n'; -import MkInfo from '@client/components/ui/info.vue'; -import MkSuperMenu from '@client/components/ui/super-menu.vue'; -import { scroll } from '@client/scripts/scroll'; -import { signout } from '@client/account'; -import { unisonReload } from '@client/scripts/unison-reload'; -import * as symbols from '@client/symbols'; -import { instance } from '@client/instance'; -import { $i } from '@client/account'; - -export default defineComponent({ - components: { - MkInfo, - MkSuperMenu, - }, - - props: { - initialPage: { - type: String, - required: false - } - }, - - setup(props, context) { - const indexInfo = { - title: i18n.locale.settings, - icon: 'fas fa-cog', - bg: 'var(--bg)', - hideHeader: true, - }; - const INFO = ref(indexInfo); - const page = ref(props.initialPage); - const narrow = ref(false); - const view = ref(null); - const el = ref(null); - const menuDef = computed(() => [{ - title: i18n.locale.basicSettings, - items: [{ - icon: 'fas fa-user', - text: i18n.locale.profile, - to: '/settings/profile', - active: page.value === 'profile', - }, { - icon: 'fas fa-lock-open', - text: i18n.locale.privacy, - to: '/settings/privacy', - active: page.value === 'privacy', - }, { - icon: 'fas fa-laugh', - text: i18n.locale.reaction, - to: '/settings/reaction', - active: page.value === 'reaction', - }, { - icon: 'fas fa-cloud', - text: i18n.locale.drive, - to: '/settings/drive', - active: page.value === 'drive', - }, { - icon: 'fas fa-bell', - text: i18n.locale.notifications, - to: '/settings/notifications', - active: page.value === 'notifications', - }, { - icon: 'fas fa-envelope', - text: i18n.locale.email, - to: '/settings/email', - active: page.value === 'email', - }, { - icon: 'fas fa-share-alt', - text: i18n.locale.integration, - to: '/settings/integration', - active: page.value === 'integration', - }, { - icon: 'fas fa-lock', - text: i18n.locale.security, - to: '/settings/security', - active: page.value === 'security', - }], - }, { - title: i18n.locale.clientSettings, - items: [{ - icon: 'fas fa-cogs', - text: i18n.locale.general, - to: '/settings/general', - active: page.value === 'general', - }, { - icon: 'fas fa-palette', - text: i18n.locale.theme, - to: '/settings/theme', - active: page.value === 'theme', - }, { - icon: 'fas fa-list-ul', - text: i18n.locale.menu, - to: '/settings/menu', - active: page.value === 'menu', - }, { - icon: 'fas fa-music', - text: i18n.locale.sounds, - to: '/settings/sounds', - active: page.value === 'sounds', - }, { - icon: 'fas fa-plug', - text: i18n.locale.plugins, - to: '/settings/plugin', - active: page.value === 'plugin', - }], - }, { - title: i18n.locale.otherSettings, - items: [{ - icon: 'fas fa-boxes', - text: i18n.locale.importAndExport, - to: '/settings/import-export', - active: page.value === 'import-export', - }, { - icon: 'fas fa-ban', - text: i18n.locale.muteAndBlock, - to: '/settings/mute-block', - active: page.value === 'mute-block', - }, { - icon: 'fas fa-comment-slash', - text: i18n.locale.wordMute, - to: '/settings/word-mute', - active: page.value === 'word-mute', - }, { - icon: 'fas fa-key', - text: 'API', - to: '/settings/api', - active: page.value === 'api', - }, { - icon: 'fas fa-ellipsis-h', - text: i18n.locale.other, - to: '/settings/other', - active: page.value === 'other', - }], - }, { - items: [{ - type: 'button', - icon: 'fas fa-trash', - text: i18n.locale.clearCache, - action: () => { - localStorage.removeItem('locale'); - localStorage.removeItem('theme'); - unisonReload(); - }, - }, { - type: 'button', - icon: 'fas fa-sign-in-alt fa-flip-horizontal', - text: i18n.locale.logout, - action: () => { - signout(); - }, - danger: true, - },], - }]); - - const pageProps = ref({}); - const component = computed(() => { - if (page.value == null) return null; - switch (page.value) { - case 'accounts': return defineAsyncComponent(() => import('./accounts.vue')); - case 'profile': return defineAsyncComponent(() => import('./profile.vue')); - case 'privacy': return defineAsyncComponent(() => import('./privacy.vue')); - case 'reaction': return defineAsyncComponent(() => import('./reaction.vue')); - case 'drive': return defineAsyncComponent(() => import('./drive.vue')); - case 'notifications': return defineAsyncComponent(() => import('./notifications.vue')); - case 'mute-block': return defineAsyncComponent(() => import('./mute-block.vue')); - case 'word-mute': return defineAsyncComponent(() => import('./word-mute.vue')); - case 'integration': return defineAsyncComponent(() => import('./integration.vue')); - case 'security': return defineAsyncComponent(() => import('./security.vue')); - case '2fa': return defineAsyncComponent(() => import('./2fa.vue')); - case 'api': return defineAsyncComponent(() => import('./api.vue')); - case 'apps': return defineAsyncComponent(() => import('./apps.vue')); - case 'other': return defineAsyncComponent(() => import('./other.vue')); - case 'general': return defineAsyncComponent(() => import('./general.vue')); - case 'email': return defineAsyncComponent(() => import('./email.vue')); - case 'email/address': return defineAsyncComponent(() => import('./email-address.vue')); - case 'email/notification': return defineAsyncComponent(() => import('./email-notification.vue')); - case 'theme': return defineAsyncComponent(() => import('./theme.vue')); - case 'theme/install': return defineAsyncComponent(() => import('./theme.install.vue')); - case 'theme/manage': return defineAsyncComponent(() => import('./theme.manage.vue')); - case 'menu': return defineAsyncComponent(() => import('./menu.vue')); - case 'sounds': return defineAsyncComponent(() => import('./sounds.vue')); - case 'custom-css': return defineAsyncComponent(() => import('./custom-css.vue')); - case 'deck': return defineAsyncComponent(() => import('./deck.vue')); - case 'plugin': return defineAsyncComponent(() => import('./plugin.vue')); - case 'plugin/install': return defineAsyncComponent(() => import('./plugin.install.vue')); - case 'plugin/manage': return defineAsyncComponent(() => import('./plugin.manage.vue')); - case 'import-export': return defineAsyncComponent(() => import('./import-export.vue')); - case 'account-info': return defineAsyncComponent(() => import('./account-info.vue')); - case 'update': return defineAsyncComponent(() => import('./update.vue')); - case 'registry': return defineAsyncComponent(() => import('./registry.vue')); - case 'delete-account': return defineAsyncComponent(() => import('./delete-account.vue')); - case 'experimental-features': return defineAsyncComponent(() => import('./experimental-features.vue')); - } - if (page.value.startsWith('registry/keys/system/')) { - return defineAsyncComponent(() => import('./registry.keys.vue')); - } - if (page.value.startsWith('registry/value/system/')) { - return defineAsyncComponent(() => import('./registry.value.vue')); - } - }); - - watch(component, () => { - pageProps.value = {}; - - if (page.value) { - if (page.value.startsWith('registry/keys/system/')) { - pageProps.value.scope = page.value.replace('registry/keys/system/', '').split('/'); - } - if (page.value.startsWith('registry/value/system/')) { - const path = page.value.replace('registry/value/system/', '').split('/'); - pageProps.value.xKey = path.pop(); - pageProps.value.scope = path; - } - } - - nextTick(() => { - scroll(el.value, { top: 0 }); - }); - }, { immediate: true }); - - watch(() => props.initialPage, () => { - if (props.initialPage == null && !narrow.value) { - page.value = 'profile'; - } else { - page.value = props.initialPage; - if (props.initialPage == null) { - INFO.value = indexInfo; - } - } - }); - - onMounted(() => { - narrow.value = el.value.offsetWidth < 800; - if (!narrow.value) { - page.value = 'profile'; - } - }); - - const emailNotConfigured = computed(() => instance.enableEmail && ($i.email == null || !$i.emailVerified)); - - return { - [symbols.PAGE_INFO]: INFO, - page, - menuDef, - narrow, - view, - el, - pageProps, - component, - emailNotConfigured, - }; - }, -}); -</script> - -<style lang="scss" scoped> -.vvcocwet { - > .nav { - .baaadecd { - > .title { - margin: 16px; - font-size: 1.5em; - font-weight: bold; - } - - > .info { - margin: 0 16px; - } - - > .accounts { - > .avatar { - display: block; - width: 50px; - height: 50px; - margin: 8px auto 16px auto; - } - } - } - } - - &.wide { - display: flex; - max-width: 1000px; - margin: 0 auto; - height: 100%; - - > .nav { - width: 32%; - box-sizing: border-box; - overflow: auto; - - .baaadecd { - > .title { - margin: 24px 0; - } - } - } - - > .main { - flex: 1; - min-width: 0; - overflow: auto; - } - } -} -</style> diff --git a/src/client/pages/settings/integration.vue b/src/client/pages/settings/integration.vue deleted file mode 100644 index 7f398dde9d..0000000000 --- a/src/client/pages/settings/integration.vue +++ /dev/null @@ -1,141 +0,0 @@ -<template> -<FormBase> - <div class="_debobigegoItem" v-if="enableTwitterIntegration"> - <div class="_debobigegoLabel"><i class="fab fa-twitter"></i> Twitter</div> - <div class="_debobigegoPanel" style="padding: 16px;"> - <p v-if="integrations.twitter">{{ $ts.connectedTo }}: <a :href="`https://twitter.com/${integrations.twitter.screenName}`" rel="nofollow noopener" target="_blank">@{{ integrations.twitter.screenName }}</a></p> - <MkButton v-if="integrations.twitter" @click="disconnectTwitter" danger>{{ $ts.disconnectService }}</MkButton> - <MkButton v-else @click="connectTwitter" primary>{{ $ts.connectService }}</MkButton> - </div> - </div> - - <div class="_debobigegoItem" v-if="enableDiscordIntegration"> - <div class="_debobigegoLabel"><i class="fab fa-discord"></i> Discord</div> - <div class="_debobigegoPanel" style="padding: 16px;"> - <p v-if="integrations.discord">{{ $ts.connectedTo }}: <a :href="`https://discord.com/users/${integrations.discord.id}`" rel="nofollow noopener" target="_blank">@{{ integrations.discord.username }}#{{ integrations.discord.discriminator }}</a></p> - <MkButton v-if="integrations.discord" @click="disconnectDiscord" danger>{{ $ts.disconnectService }}</MkButton> - <MkButton v-else @click="connectDiscord" primary>{{ $ts.connectService }}</MkButton> - </div> - </div> - - <div class="_debobigegoItem" v-if="enableGithubIntegration"> - <div class="_debobigegoLabel"><i class="fab fa-github"></i> GitHub</div> - <div class="_debobigegoPanel" style="padding: 16px;"> - <p v-if="integrations.github">{{ $ts.connectedTo }}: <a :href="`https://github.com/${integrations.github.login}`" rel="nofollow noopener" target="_blank">@{{ integrations.github.login }}</a></p> - <MkButton v-if="integrations.github" @click="disconnectGithub" danger>{{ $ts.disconnectService }}</MkButton> - <MkButton v-else @click="connectGithub" primary>{{ $ts.connectService }}</MkButton> - </div> - </div> -</FormBase> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import { apiUrl } from '@client/config'; -import FormBase from '@client/components/debobigego/base.vue'; -import MkButton from '@client/components/ui/button.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - FormBase, - MkButton - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.integration, - icon: 'fas fa-share-alt', - bg: 'var(--bg)', - }, - apiUrl, - twitterForm: null, - discordForm: null, - githubForm: null, - enableTwitterIntegration: false, - enableDiscordIntegration: false, - enableGithubIntegration: false, - }; - }, - - computed: { - integrations() { - return this.$i.integrations; - }, - - meta() { - return this.$instance; - }, - }, - - created() { - this.enableTwitterIntegration = this.meta.enableTwitterIntegration; - this.enableDiscordIntegration = this.meta.enableDiscordIntegration; - this.enableGithubIntegration = this.meta.enableGithubIntegration; - }, - - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - - document.cookie = `igi=${this.$i.token}; path=/;` + - ` max-age=31536000;` + - (document.location.protocol.startsWith('https') ? ' secure' : ''); - - this.$watch('integrations', () => { - if (this.integrations.twitter) { - if (this.twitterForm) this.twitterForm.close(); - } - if (this.integrations.discord) { - if (this.discordForm) this.discordForm.close(); - } - if (this.integrations.github) { - if (this.githubForm) this.githubForm.close(); - } - }, { - deep: true - }); - }, - - methods: { - connectTwitter() { - this.twitterForm = window.open(apiUrl + '/connect/twitter', - 'twitter_connect_window', - 'height=570, width=520'); - }, - - disconnectTwitter() { - window.open(apiUrl + '/disconnect/twitter', - 'twitter_disconnect_window', - 'height=570, width=520'); - }, - - connectDiscord() { - this.discordForm = window.open(apiUrl + '/connect/discord', - 'discord_connect_window', - 'height=570, width=520'); - }, - - disconnectDiscord() { - window.open(apiUrl + '/disconnect/discord', - 'discord_disconnect_window', - 'height=570, width=520'); - }, - - connectGithub() { - this.githubForm = window.open(apiUrl + '/connect/github', - 'github_connect_window', - 'height=570, width=520'); - }, - - disconnectGithub() { - window.open(apiUrl + '/disconnect/github', - 'github_disconnect_window', - 'height=570, width=520'); - }, - } -}); -</script> diff --git a/src/client/pages/settings/menu.vue b/src/client/pages/settings/menu.vue deleted file mode 100644 index 31472eb0c1..0000000000 --- a/src/client/pages/settings/menu.vue +++ /dev/null @@ -1,117 +0,0 @@ -<template> -<FormBase> - <FormTextarea v-model="items" tall manual-save> - <span>{{ $ts.menu }}</span> - <template #desc><button class="_textButton" @click="addItem">{{ $ts.addItem }}</button></template> - </FormTextarea> - - <FormRadios v-model="menuDisplay"> - <template #desc>{{ $ts.display }}</template> - <option value="sideFull">{{ $ts._menuDisplay.sideFull }}</option> - <option value="sideIcon">{{ $ts._menuDisplay.sideIcon }}</option> - <option value="top">{{ $ts._menuDisplay.top }}</option> - <!-- <MkRadio v-model="menuDisplay" value="hide" disabled>{{ $ts._menuDisplay.hide }}</MkRadio>--> <!-- TODO: ใตใคใใใผใๅฎๅ
จใซ้ ใใใใใซใใใจใๅฅ้ใใณใใผใฌใผใใฟใณใฎใใใชใใฎใUIใซ่กจ็คบใใๅฟ
่ฆใใใ้ขๅ --> - </FormRadios> - - <FormButton @click="reset()" danger><i class="fas fa-redo"></i> {{ $ts.default }}</FormButton> -</FormBase> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import FormTextarea from '@client/components/debobigego/textarea.vue'; -import FormRadios from '@client/components/debobigego/radios.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import * as os from '@client/os'; -import { menuDef } from '@client/menu'; -import { defaultStore } from '@client/store'; -import * as symbols from '@client/symbols'; -import { unisonReload } from '@client/scripts/unison-reload'; - -export default defineComponent({ - components: { - FormBase, - FormButton, - FormTextarea, - FormRadios, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.menu, - icon: 'fas fa-list-ul', - bg: 'var(--bg)', - }, - menuDef: menuDef, - items: defaultStore.state.menu.join('\n'), - } - }, - - computed: { - splited(): string[] { - return this.items.trim().split('\n').filter(x => x.trim() !== ''); - }, - - menuDisplay: defaultStore.makeGetterSetter('menuDisplay') - }, - - watch: { - menuDisplay() { - this.reloadAsk(); - }, - - items() { - this.save(); - }, - }, - - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - methods: { - async addItem() { - const menu = Object.keys(this.menuDef).filter(k => !this.$store.state.menu.includes(k)); - const { canceled, result: item } = await os.dialog({ - type: null, - title: this.$ts.addItem, - select: { - items: [...menu.map(k => ({ - value: k, text: this.$ts[this.menuDef[k].title] - })), ...[{ - value: '-', text: this.$ts.divider - }]] - }, - showCancelButton: true - }); - if (canceled) return; - this.items = [...this.splited, item].join('\n'); - }, - - save() { - this.$store.set('menu', this.splited); - this.reloadAsk(); - }, - - reset() { - this.$store.reset('menu'); - this.items = this.$store.state.menu.join('\n'); - }, - - async reloadAsk() { - const { canceled } = await os.dialog({ - type: 'info', - text: this.$ts.reloadToApplySetting, - showCancelButton: true - }); - if (canceled) return; - - unisonReload(); - } - }, -}); -</script> diff --git a/src/client/pages/settings/mute-block.vue b/src/client/pages/settings/mute-block.vue deleted file mode 100644 index 18b2fc0af4..0000000000 --- a/src/client/pages/settings/mute-block.vue +++ /dev/null @@ -1,85 +0,0 @@ -<template> -<FormBase> - <MkTab v-model="tab" style="margin-bottom: var(--margin);"> - <option value="mute">{{ $ts.mutedUsers }}</option> - <option value="block">{{ $ts.blockedUsers }}</option> - </MkTab> - <div v-if="tab === 'mute'"> - <MkPagination :pagination="mutingPagination" class="muting"> - <template #empty><FormInfo>{{ $ts.noUsers }}</FormInfo></template> - <template #default="{items}"> - <FormGroup> - <FormLink v-for="mute in items" :key="mute.id" :to="userPage(mute.mutee)"> - <MkAcct :user="mute.mutee"/> - </FormLink> - </FormGroup> - </template> - </MkPagination> - </div> - <div v-if="tab === 'block'"> - <MkPagination :pagination="blockingPagination" class="blocking"> - <template #empty><FormInfo>{{ $ts.noUsers }}</FormInfo></template> - <template #default="{items}"> - <FormGroup> - <FormLink v-for="block in items" :key="block.id" :to="userPage(block.blockee)"> - <MkAcct :user="block.blockee"/> - </FormLink> - </FormGroup> - </template> - </MkPagination> - </div> -</FormBase> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import MkPagination from '@client/components/ui/pagination.vue'; -import MkTab from '@client/components/tab.vue'; -import FormInfo from '@client/components/debobigego/info.vue'; -import FormLink from '@client/components/debobigego/link.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import { userPage } from '@client/filters/user'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - MkPagination, - MkTab, - FormInfo, - FormBase, - FormGroup, - FormLink, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.muteAndBlock, - icon: 'fas fa-ban', - bg: 'var(--bg)', - }, - tab: 'mute', - mutingPagination: { - endpoint: 'mute/list', - limit: 10, - }, - blockingPagination: { - endpoint: 'blocking/list', - limit: 10, - }, - } - }, - - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - methods: { - userPage - } -}); -</script> diff --git a/src/client/pages/settings/notifications.vue b/src/client/pages/settings/notifications.vue deleted file mode 100644 index 5f84349474..0000000000 --- a/src/client/pages/settings/notifications.vue +++ /dev/null @@ -1,77 +0,0 @@ -<template> -<FormBase> - <FormLink @click="configure">{{ $ts.notificationSetting }}</FormLink> - <FormGroup> - <FormButton @click="readAllNotifications">{{ $ts.markAsReadAllNotifications }}</FormButton> - <FormButton @click="readAllUnreadNotes">{{ $ts.markAsReadAllUnreadNotes }}</FormButton> - <FormButton @click="readAllMessagingMessages">{{ $ts.markAsReadAllTalkMessages }}</FormButton> - </FormGroup> -</FormBase> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import FormLink from '@client/components/debobigego/link.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import { notificationTypes } from '@/types'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - FormBase, - FormLink, - FormButton, - FormGroup, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.notifications, - icon: 'fas fa-bell', - bg: 'var(--bg)', - }, - } - }, - - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - methods: { - readAllUnreadNotes() { - os.api('i/read-all-unread-notes'); - }, - - readAllMessagingMessages() { - os.api('i/read-all-messaging-messages'); - }, - - readAllNotifications() { - os.api('notifications/mark-all-as-read'); - }, - - configure() { - const includingTypes = notificationTypes.filter(x => !this.$i.mutingNotificationTypes.includes(x)); - os.popup(import('@client/components/notification-setting-window.vue'), { - includingTypes, - showGlobalToggle: false, - }, { - done: async (res) => { - const { includingTypes: value } = res; - await os.apiWithDialog('i/update', { - mutingNotificationTypes: notificationTypes.filter(x => !value.includes(x)), - }).then(i => { - this.$i.mutingNotificationTypes = i.mutingNotificationTypes; - }); - } - }, 'closed'); - }, - } -}); -</script> diff --git a/src/client/pages/settings/other.vue b/src/client/pages/settings/other.vue deleted file mode 100644 index 2eb922453f..0000000000 --- a/src/client/pages/settings/other.vue +++ /dev/null @@ -1,97 +0,0 @@ -<template> -<FormBase> - <FormLink to="/settings/update">Misskey Update</FormLink> - - <FormSwitch :value="$i.injectFeaturedNote" @update:modelValue="onChangeInjectFeaturedNote"> - {{ $ts.showFeaturedNotesInTimeline }} - </FormSwitch> - - <FormSwitch v-model="reportError">{{ $ts.sendErrorReports }}<template #desc>{{ $ts.sendErrorReportsDescription }}</template></FormSwitch> - - <FormLink to="/settings/account-info">{{ $ts.accountInfo }}</FormLink> - <FormLink to="/settings/experimental-features">{{ $ts.experimentalFeatures }}</FormLink> - - <FormGroup> - <template #label>{{ $ts.developer }}</template> - <FormSwitch v-model="debug" @update:modelValue="changeDebug"> - DEBUG MODE - </FormSwitch> - <template v-if="debug"> - <FormButton @click="taskmanager">Task Manager</FormButton> - </template> - </FormGroup> - - <FormLink to="/settings/registry"><template #icon><i class="fas fa-cogs"></i></template>{{ $ts.registry }}</FormLink> - - <FormLink to="/bios" behavior="browser"><template #icon><i class="fas fa-door-open"></i></template>BIOS</FormLink> - <FormLink to="/cli" behavior="browser"><template #icon><i class="fas fa-door-open"></i></template>CLI</FormLink> - - <FormLink to="/settings/delete-account"><template #icon><i class="fas fa-exclamation-triangle"></i></template>{{ $ts.closeAccount }}</FormLink> -</FormBase> -</template> - -<script lang="ts"> -import { defineAsyncComponent, defineComponent } from 'vue'; -import FormSwitch from '@client/components/form/switch.vue'; -import FormSelect from '@client/components/form/select.vue'; -import FormLink from '@client/components/debobigego/link.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import * as os from '@client/os'; -import { debug } from '@client/config'; -import { defaultStore } from '@client/store'; -import { unisonReload } from '@client/scripts/unison-reload'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - FormBase, - FormSelect, - FormSwitch, - FormButton, - FormLink, - FormGroup, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.other, - icon: 'fas fa-ellipsis-h', - bg: 'var(--bg)', - }, - debug, - } - }, - - computed: { - reportError: defaultStore.makeGetterSetter('reportError'), - }, - - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - methods: { - changeDebug(v) { - console.log(v); - localStorage.setItem('debug', v.toString()); - unisonReload(); - }, - - onChangeInjectFeaturedNote(v) { - os.api('i/update', { - injectFeaturedNote: v - }); - }, - - taskmanager() { - os.popup(import('@client/components/taskmanager.vue'), { - }, {}, 'closed'); - }, - } -}); -</script> diff --git a/src/client/pages/settings/plugin.install.vue b/src/client/pages/settings/plugin.install.vue deleted file mode 100644 index 709ef11abb..0000000000 --- a/src/client/pages/settings/plugin.install.vue +++ /dev/null @@ -1,147 +0,0 @@ -<template> -<FormBase> - <FormInfo warn>{{ $ts._plugin.installWarn }}</FormInfo> - - <FormGroup> - <FormTextarea v-model="code" tall> - <span>{{ $ts.code }}</span> - </FormTextarea> - </FormGroup> - - <FormButton @click="install" :disabled="code == null" primary inline><i class="fas fa-check"></i> {{ $ts.install }}</FormButton> -</FormBase> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import { AiScript, parse } from '@syuilo/aiscript'; -import { serialize } from '@syuilo/aiscript/built/serializer'; -import { v4 as uuid } from 'uuid'; -import FormTextarea from '@client/components/form/textarea.vue'; -import FormSelect from '@client/components/form/select.vue'; -import FormRadios from '@client/components/form/radios.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import FormLink from '@client/components/debobigego/link.vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import FormInfo from '@client/components/debobigego/info.vue'; -import * as os from '@client/os'; -import { ColdDeviceStorage } from '@client/store'; -import { unisonReload } from '@client/scripts/unison-reload'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - FormTextarea, - FormSelect, - FormRadios, - FormBase, - FormGroup, - FormLink, - FormButton, - FormInfo, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts._plugin.install, - icon: 'fas fa-download', - bg: 'var(--bg)', - }, - code: null, - } - }, - - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - methods: { - installPlugin({ id, meta, ast, token }) { - ColdDeviceStorage.set('plugins', ColdDeviceStorage.get('plugins').concat({ - ...meta, - id, - active: true, - configData: {}, - token: token, - ast: ast - })); - }, - - async install() { - let ast; - try { - ast = parse(this.code); - } catch (e) { - os.dialog({ - type: 'error', - text: 'Syntax error :(' - }); - return; - } - const meta = AiScript.collectMetadata(ast); - if (meta == null) { - os.dialog({ - type: 'error', - text: 'No metadata found :(' - }); - return; - } - const data = meta.get(null); - if (data == null) { - os.dialog({ - type: 'error', - text: 'No metadata found :(' - }); - return; - } - const { name, version, author, description, permissions, config } = data; - if (name == null || version == null || author == null) { - os.dialog({ - type: 'error', - text: 'Required property not found :(' - }); - return; - } - - const token = permissions == null || permissions.length === 0 ? null : await new Promise((res, rej) => { - os.popup(import('@client/components/token-generate-window.vue'), { - title: this.$ts.tokenRequested, - information: this.$ts.pluginTokenRequestedDescription, - initialName: name, - initialPermissions: permissions - }, { - done: async result => { - const { name, permissions } = result; - const { token } = await os.api('miauth/gen-token', { - session: null, - name: name, - permission: permissions, - }); - - res(token); - } - }, 'closed'); - }); - - this.installPlugin({ - id: uuid(), - meta: { - name, version, author, description, permissions, config - }, - token, - ast: serialize(ast) - }); - - os.success(); - - this.$nextTick(() => { - unisonReload(); - }); - }, - } -}); -</script> diff --git a/src/client/pages/settings/plugin.manage.vue b/src/client/pages/settings/plugin.manage.vue deleted file mode 100644 index f1c27f1e3c..0000000000 --- a/src/client/pages/settings/plugin.manage.vue +++ /dev/null @@ -1,115 +0,0 @@ -<template> -<FormBase> - <FormGroup v-for="plugin in plugins" :key="plugin.id"> - <template #label><span style="display: flex;"><b>{{ plugin.name }}</b><span style="margin-left: auto;">v{{ plugin.version }}</span></span></template> - - <FormSwitch :value="plugin.active" @update:modelValue="changeActive(plugin, $event)">{{ $ts.makeActive }}</FormSwitch> - <div class="_debobigegoItem"> - <div class="_debobigegoPanel" style="padding: 16px;"> - <div class="_keyValue"> - <div>{{ $ts.author }}:</div> - <div>{{ plugin.author }}</div> - </div> - <div class="_keyValue"> - <div>{{ $ts.description }}:</div> - <div>{{ plugin.description }}</div> - </div> - <div class="_keyValue"> - <div>{{ $ts.permission }}:</div> - <div>{{ plugin.permissions }}</div> - </div> - </div> - </div> - <div class="_debobigegoItem"> - <div class="_debobigegoPanel" style="padding: 16px;"> - <MkButton @click="config(plugin)" inline v-if="plugin.config"><i class="fas fa-cog"></i> {{ $ts.settings }}</MkButton> - <MkButton @click="uninstall(plugin)" inline danger><i class="fas fa-trash-alt"></i> {{ $ts.uninstall }}</MkButton> - </div> - </div> - </FormGroup> -</FormBase> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import MkButton from '@client/components/ui/button.vue'; -import MkTextarea from '@client/components/form/textarea.vue'; -import MkSelect from '@client/components/form/select.vue'; -import FormSwitch from '@client/components/form/switch.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import * as os from '@client/os'; -import { ColdDeviceStorage } from '@client/store'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - MkButton, - MkTextarea, - MkSelect, - FormSwitch, - FormBase, - FormGroup, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts._plugin.manage, - icon: 'fas fa-plug', - bg: 'var(--bg)', - }, - plugins: ColdDeviceStorage.get('plugins'), - } - }, - - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - methods: { - uninstall(plugin) { - ColdDeviceStorage.set('plugins', this.plugins.filter(x => x.id !== plugin.id)); - os.success(); - this.$nextTick(() => { - unisonReload(); - }); - }, - - // TODO: ใใฎๅฆ็ใstoreๅดใซactionใจใใฆ็งปๅใใ่จญๅฎ็ป้ขใ้ใAiScriptAPIใๅฎ่ฃ
ใงใใใใใซใใ - async config(plugin) { - const config = plugin.config; - for (const key in plugin.configData) { - config[key].default = plugin.configData[key]; - } - - const { canceled, result } = await os.form(plugin.name, config); - if (canceled) return; - - const plugins = ColdDeviceStorage.get('plugins'); - plugins.find(p => p.id === plugin.id).configData = result; - ColdDeviceStorage.set('plugins', plugins); - - this.$nextTick(() => { - location.reload(); - }); - }, - - changeActive(plugin, active) { - const plugins = ColdDeviceStorage.get('plugins'); - plugins.find(p => p.id === plugin.id).active = active; - ColdDeviceStorage.set('plugins', plugins); - - this.$nextTick(() => { - location.reload(); - }); - } - }, -}); -</script> - -<style lang="scss" scoped> - -</style> diff --git a/src/client/pages/settings/plugin.vue b/src/client/pages/settings/plugin.vue deleted file mode 100644 index 23f263bbbd..0000000000 --- a/src/client/pages/settings/plugin.vue +++ /dev/null @@ -1,44 +0,0 @@ -<template> -<FormBase> - <FormLink to="/settings/plugin/install"><template #icon><i class="fas fa-download"></i></template>{{ $ts._plugin.install }}</FormLink> - <FormLink to="/settings/plugin/manage"><template #icon><i class="fas fa-folder-open"></i></template>{{ $ts._plugin.manage }}<template #suffix>{{ plugins }}</template></FormLink> -</FormBase> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import FormLink from '@client/components/debobigego/link.vue'; -import * as os from '@client/os'; -import { ColdDeviceStorage } from '@client/store'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - FormBase, - FormLink, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.plugins, - icon: 'fas fa-plug', - bg: 'var(--bg)', - }, - plugins: ColdDeviceStorage.get('plugins').length, - } - }, - - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, -}); -</script> - -<style lang="scss" scoped> - -</style> diff --git a/src/client/pages/settings/privacy.vue b/src/client/pages/settings/privacy.vue deleted file mode 100644 index 2a60ae1f46..0000000000 --- a/src/client/pages/settings/privacy.vue +++ /dev/null @@ -1,108 +0,0 @@ -<template> -<FormBase> - <FormGroup> - <FormSwitch v-model="isLocked" @update:modelValue="save()">{{ $ts.makeFollowManuallyApprove }}</FormSwitch> - <FormSwitch v-model="autoAcceptFollowed" :disabled="!isLocked" @update:modelValue="save()">{{ $ts.autoAcceptFollowed }}</FormSwitch> - <template #caption>{{ $ts.lockedAccountInfo }}</template> - </FormGroup> - <FormSwitch v-model="publicReactions" @update:modelValue="save()"> - {{ $ts.makeReactionsPublic }} - <template #desc>{{ $ts.makeReactionsPublicDescription }}</template> - </FormSwitch> - <FormSwitch v-model="hideOnlineStatus" @update:modelValue="save()"> - {{ $ts.hideOnlineStatus }} - <template #desc>{{ $ts.hideOnlineStatusDescription }}</template> - </FormSwitch> - <FormSwitch v-model="noCrawle" @update:modelValue="save()"> - {{ $ts.noCrawle }} - <template #desc>{{ $ts.noCrawleDescription }}</template> - </FormSwitch> - <FormSwitch v-model="isExplorable" @update:modelValue="save()"> - {{ $ts.makeExplorable }} - <template #desc>{{ $ts.makeExplorableDescription }}</template> - </FormSwitch> - <FormSwitch v-model="rememberNoteVisibility" @update:modelValue="save()">{{ $ts.rememberNoteVisibility }}</FormSwitch> - <FormGroup v-if="!rememberNoteVisibility"> - <template #label>{{ $ts.defaultNoteVisibility }}</template> - <FormSelect v-model="defaultNoteVisibility"> - <option value="public">{{ $ts._visibility.public }}</option> - <option value="home">{{ $ts._visibility.home }}</option> - <option value="followers">{{ $ts._visibility.followers }}</option> - <option value="specified">{{ $ts._visibility.specified }}</option> - </FormSelect> - <FormSwitch v-model="defaultNoteLocalOnly">{{ $ts._visibility.localOnly }}</FormSwitch> - </FormGroup> - <FormSwitch v-model="keepCw" @update:modelValue="save()">{{ $ts.keepCw }}</FormSwitch> -</FormBase> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import FormSwitch from '@client/components/debobigego/switch.vue'; -import FormSelect from '@client/components/debobigego/select.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import * as os from '@client/os'; -import { defaultStore } from '@client/store'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - FormBase, - FormSelect, - FormGroup, - FormSwitch, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.privacy, - icon: 'fas fa-lock-open', - bg: 'var(--bg)', - }, - isLocked: false, - autoAcceptFollowed: false, - noCrawle: false, - isExplorable: false, - hideOnlineStatus: false, - publicReactions: false, - } - }, - - computed: { - defaultNoteVisibility: defaultStore.makeGetterSetter('defaultNoteVisibility'), - defaultNoteLocalOnly: defaultStore.makeGetterSetter('defaultNoteLocalOnly'), - rememberNoteVisibility: defaultStore.makeGetterSetter('rememberNoteVisibility'), - keepCw: defaultStore.makeGetterSetter('keepCw'), - }, - - created() { - this.isLocked = this.$i.isLocked; - this.autoAcceptFollowed = this.$i.autoAcceptFollowed; - this.noCrawle = this.$i.noCrawle; - this.isExplorable = this.$i.isExplorable; - this.hideOnlineStatus = this.$i.hideOnlineStatus; - this.publicReactions = this.$i.publicReactions; - }, - - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - methods: { - save() { - os.api('i/update', { - isLocked: !!this.isLocked, - autoAcceptFollowed: !!this.autoAcceptFollowed, - noCrawle: !!this.noCrawle, - isExplorable: !!this.isExplorable, - hideOnlineStatus: !!this.hideOnlineStatus, - publicReactions: !!this.publicReactions, - }); - } - } -}); -</script> diff --git a/src/client/pages/settings/profile.vue b/src/client/pages/settings/profile.vue deleted file mode 100644 index b993b5fc72..0000000000 --- a/src/client/pages/settings/profile.vue +++ /dev/null @@ -1,281 +0,0 @@ -<template> -<FormBase> - <FormGroup> - <div class="_debobigegoItem _debobigegoPanel llvierxe" :style="{ backgroundImage: $i.bannerUrl ? `url(${ $i.bannerUrl })` : null }"> - <MkAvatar class="avatar" :user="$i"/> - </div> - <FormButton @click="changeAvatar" primary>{{ $ts._profile.changeAvatar }}</FormButton> - <FormButton @click="changeBanner" primary>{{ $ts._profile.changeBanner }}</FormButton> - </FormGroup> - - <FormInput v-model="name" :max="30" manual-save> - <span>{{ $ts._profile.name }}</span> - </FormInput> - - <FormTextarea v-model="description" :max="500" tall manual-save> - <span>{{ $ts._profile.description }}</span> - <template #desc>{{ $ts._profile.youCanIncludeHashtags }}</template> - </FormTextarea> - - <FormInput v-model="location" manual-save> - <span>{{ $ts.location }}</span> - <template #prefix><i class="fas fa-map-marker-alt"></i></template> - </FormInput> - - <FormInput v-model="birthday" type="date" manual-save> - <span>{{ $ts.birthday }}</span> - <template #prefix><i class="fas fa-birthday-cake"></i></template> - </FormInput> - - <FormSelect v-model="lang"> - <template #label>{{ $ts.language }}</template> - <option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option> - </FormSelect> - - <FormGroup> - <FormButton @click="editMetadata" primary>{{ $ts._profile.metadataEdit }}</FormButton> - <template #caption>{{ $ts._profile.metadataDescription }}</template> - </FormGroup> - - <FormSwitch v-model="isCat">{{ $ts.flagAsCat }}<template #desc>{{ $ts.flagAsCatDescription }}</template></FormSwitch> - - <FormSwitch v-model="isBot">{{ $ts.flagAsBot }}<template #desc>{{ $ts.flagAsBotDescription }}</template></FormSwitch> - - <FormSwitch v-model="alwaysMarkNsfw">{{ $ts.alwaysMarkSensitive }}</FormSwitch> -</FormBase> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import FormInput from '@client/components/debobigego/input.vue'; -import FormTextarea from '@client/components/debobigego/textarea.vue'; -import FormSwitch from '@client/components/debobigego/switch.vue'; -import FormSelect from '@client/components/debobigego/select.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import { host, langs } from '@client/config'; -import { selectFile } from '@client/scripts/select-file'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - FormButton, - FormInput, - FormTextarea, - FormSwitch, - FormSelect, - FormBase, - FormGroup, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.profile, - icon: 'fas fa-user', - bg: 'var(--bg)', - }, - host, - langs, - name: null, - description: null, - birthday: null, - lang: null, - location: null, - fieldName0: null, - fieldValue0: null, - fieldName1: null, - fieldValue1: null, - fieldName2: null, - fieldValue2: null, - fieldName3: null, - fieldValue3: null, - avatarId: null, - bannerId: null, - isBot: false, - isCat: false, - alwaysMarkNsfw: false, - saving: false, - } - }, - - created() { - this.name = this.$i.name; - this.description = this.$i.description; - this.location = this.$i.location; - this.birthday = this.$i.birthday; - this.lang = this.$i.lang; - this.avatarId = this.$i.avatarId; - this.bannerId = this.$i.bannerId; - this.isBot = this.$i.isBot; - this.isCat = this.$i.isCat; - this.alwaysMarkNsfw = this.$i.alwaysMarkNsfw; - - this.fieldName0 = this.$i.fields[0] ? this.$i.fields[0].name : null; - this.fieldValue0 = this.$i.fields[0] ? this.$i.fields[0].value : null; - this.fieldName1 = this.$i.fields[1] ? this.$i.fields[1].name : null; - this.fieldValue1 = this.$i.fields[1] ? this.$i.fields[1].value : null; - this.fieldName2 = this.$i.fields[2] ? this.$i.fields[2].name : null; - this.fieldValue2 = this.$i.fields[2] ? this.$i.fields[2].value : null; - this.fieldName3 = this.$i.fields[3] ? this.$i.fields[3].name : null; - this.fieldValue3 = this.$i.fields[3] ? this.$i.fields[3].value : null; - - this.$watch('name', this.save); - this.$watch('description', this.save); - this.$watch('location', this.save); - this.$watch('birthday', this.save); - this.$watch('lang', this.save); - this.$watch('isBot', this.save); - this.$watch('isCat', this.save); - this.$watch('alwaysMarkNsfw', this.save); - }, - - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - methods: { - changeAvatar(e) { - selectFile(e.currentTarget || e.target, this.$ts.avatar).then(file => { - os.api('i/update', { - avatarId: file.id, - }); - }); - }, - - changeBanner(e) { - selectFile(e.currentTarget || e.target, this.$ts.banner).then(file => { - os.api('i/update', { - bannerId: file.id, - }); - }); - }, - - async editMetadata() { - const { canceled, result } = await os.form(this.$ts._profile.metadata, { - fieldName0: { - type: 'string', - label: this.$ts._profile.metadataLabel + ' 1', - default: this.fieldName0, - }, - fieldValue0: { - type: 'string', - label: this.$ts._profile.metadataContent + ' 1', - default: this.fieldValue0, - }, - fieldName1: { - type: 'string', - label: this.$ts._profile.metadataLabel + ' 2', - default: this.fieldName1, - }, - fieldValue1: { - type: 'string', - label: this.$ts._profile.metadataContent + ' 2', - default: this.fieldValue1, - }, - fieldName2: { - type: 'string', - label: this.$ts._profile.metadataLabel + ' 3', - default: this.fieldName2, - }, - fieldValue2: { - type: 'string', - label: this.$ts._profile.metadataContent + ' 3', - default: this.fieldValue2, - }, - fieldName3: { - type: 'string', - label: this.$ts._profile.metadataLabel + ' 4', - default: this.fieldName3, - }, - fieldValue3: { - type: 'string', - label: this.$ts._profile.metadataContent + ' 4', - default: this.fieldValue3, - }, - }); - if (canceled) return; - - this.fieldName0 = result.fieldName0; - this.fieldValue0 = result.fieldValue0; - this.fieldName1 = result.fieldName1; - this.fieldValue1 = result.fieldValue1; - this.fieldName2 = result.fieldName2; - this.fieldValue2 = result.fieldValue2; - this.fieldName3 = result.fieldName3; - this.fieldValue3 = result.fieldValue3; - - const fields = [ - { name: this.fieldName0, value: this.fieldValue0 }, - { name: this.fieldName1, value: this.fieldValue1 }, - { name: this.fieldName2, value: this.fieldValue2 }, - { name: this.fieldName3, value: this.fieldValue3 }, - ]; - - os.api('i/update', { - fields, - }).then(i => { - os.success(); - }).catch(err => { - os.dialog({ - type: 'error', - text: err.id - }); - }); - }, - - save() { - this.saving = true; - - os.apiWithDialog('i/update', { - name: this.name || null, - description: this.description || null, - location: this.location || null, - birthday: this.birthday || null, - lang: this.lang || null, - isBot: !!this.isBot, - isCat: !!this.isCat, - alwaysMarkNsfw: !!this.alwaysMarkNsfw, - }).then(i => { - this.saving = false; - this.$i.avatarId = i.avatarId; - this.$i.avatarUrl = i.avatarUrl; - this.$i.bannerId = i.bannerId; - this.$i.bannerUrl = i.bannerUrl; - }).catch(err => { - this.saving = false; - }); - }, - } -}); -</script> - -<style lang="scss" scoped> -.llvierxe { - position: relative; - height: 150px; - background-size: cover; - background-position: center; - - > * { - pointer-events: none; - } - - > .avatar { - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - display: block; - width: 72px; - height: 72px; - margin: auto; - box-shadow: 0 0 0 6px rgba(0, 0, 0, 0.5); - } -} -</style> diff --git a/src/client/pages/settings/reaction.vue b/src/client/pages/settings/reaction.vue deleted file mode 100644 index a5ff46097d..0000000000 --- a/src/client/pages/settings/reaction.vue +++ /dev/null @@ -1,152 +0,0 @@ -<template> -<FormBase> - <div class="_debobigegoItem"> - <div class="_debobigegoLabel">{{ $ts.reactionSettingDescription }}</div> - <div class="_debobigegoPanel"> - <XDraggable class="zoaiodol" v-model="reactions" :item-key="item => item" animation="150" delay="100" delay-on-touch-only="true"> - <template #item="{element}"> - <button class="_button item" @click="remove(element, $event)"> - <MkEmoji :emoji="element" :normal="true"/> - </button> - </template> - <template #footer> - <button class="_button add" @click="chooseEmoji"><i class="fas fa-plus"></i></button> - </template> - </XDraggable> - </div> - <div class="_debobigegoCaption">{{ $ts.reactionSettingDescription2 }} <button class="_textButton" @click="preview">{{ $ts.preview }}</button></div> - </div> - - <FormRadios v-model="reactionPickerWidth"> - <template #desc>{{ $ts.width }}</template> - <option :value="1">{{ $ts.small }}</option> - <option :value="2">{{ $ts.medium }}</option> - <option :value="3">{{ $ts.large }}</option> - </FormRadios> - <FormRadios v-model="reactionPickerHeight"> - <template #desc>{{ $ts.height }}</template> - <option :value="1">{{ $ts.small }}</option> - <option :value="2">{{ $ts.medium }}</option> - <option :value="3">{{ $ts.large }}</option> - </FormRadios> - <FormButton @click="preview"><i class="fas fa-eye"></i> {{ $ts.preview }}</FormButton> - <FormButton danger @click="setDefault"><i class="fas fa-undo"></i> {{ $ts.default }}</FormButton> -</FormBase> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import XDraggable from 'vuedraggable'; -import FormInput from '@client/components/debobigego/input.vue'; -import FormRadios from '@client/components/debobigego/radios.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import * as os from '@client/os'; -import { defaultStore } from '@client/store'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - FormInput, - FormButton, - FormBase, - FormRadios, - XDraggable, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.reaction, - icon: 'fas fa-laugh', - action: { - icon: 'fas fa-eye', - handler: this.preview - }, - bg: 'var(--bg)', - }, - reactions: JSON.parse(JSON.stringify(this.$store.state.reactions)), - } - }, - - computed: { - reactionPickerWidth: defaultStore.makeGetterSetter('reactionPickerWidth'), - reactionPickerHeight: defaultStore.makeGetterSetter('reactionPickerHeight'), - }, - - watch: { - reactions: { - handler() { - this.save(); - }, - deep: true - } - }, - - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - methods: { - save() { - this.$store.set('reactions', this.reactions); - }, - - remove(reaction, ev) { - os.popupMenu([{ - text: this.$ts.remove, - action: () => { - this.reactions = this.reactions.filter(x => x !== reaction) - } - }], ev.currentTarget || ev.target); - }, - - preview(ev) { - os.popup(import('@client/components/emoji-picker-dialog.vue'), { - asReactionPicker: true, - src: ev.currentTarget || ev.target, - }, {}, 'closed'); - }, - - async setDefault() { - const { canceled } = await os.dialog({ - type: 'warning', - text: this.$ts.resetAreYouSure, - showCancelButton: true - }); - if (canceled) return; - - this.reactions = JSON.parse(JSON.stringify(this.$store.def.reactions.default)); - }, - - chooseEmoji(ev) { - os.pickEmoji(ev.currentTarget || ev.target, { - showPinned: false - }).then(emoji => { - if (!this.reactions.includes(emoji)) { - this.reactions.push(emoji); - } - }); - } - } -}); -</script> - -<style lang="scss" scoped> -.zoaiodol { - padding: 16px; - - > .item { - display: inline-block; - padding: 8px; - cursor: move; - } - - > .add { - display: inline-block; - padding: 8px; - } -} -</style> diff --git a/src/client/pages/settings/registry.keys.vue b/src/client/pages/settings/registry.keys.vue deleted file mode 100644 index d99002e50f..0000000000 --- a/src/client/pages/settings/registry.keys.vue +++ /dev/null @@ -1,114 +0,0 @@ -<template> -<FormBase> - <FormGroup> - <FormKeyValueView> - <template #key>{{ $ts._registry.domain }}</template> - <template #value>{{ $ts.system }}</template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>{{ $ts._registry.scope }}</template> - <template #value>{{ scope.join('/') }}</template> - </FormKeyValueView> - </FormGroup> - - <FormGroup v-if="keys"> - <template #label>{{ $ts._registry.keys }}</template> - <FormLink v-for="key in keys" :to="`/settings/registry/value/system/${scope.join('/')}/${key[0]}`" class="_monospace">{{ key[0] }}<template #suffix>{{ key[1].toUpperCase() }}</template></FormLink> - </FormGroup> - - <FormButton @click="createKey" primary>{{ $ts._registry.createKey }}</FormButton> -</FormBase> -</template> - -<script lang="ts"> -import { defineAsyncComponent, defineComponent } from 'vue'; -import * as JSON5 from 'json5'; -import FormSwitch from '@client/components/form/switch.vue'; -import FormSelect from '@client/components/form/select.vue'; -import FormLink from '@client/components/debobigego/link.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import FormKeyValueView from '@client/components/debobigego/key-value-view.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - FormBase, - FormSelect, - FormSwitch, - FormButton, - FormLink, - FormGroup, - FormKeyValueView, - }, - - props: { - scope: { - required: true - } - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.registry, - icon: 'fas fa-cogs', - bg: 'var(--bg)', - }, - keys: null, - } - }, - - watch: { - scope() { - this.fetch(); - } - }, - - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - this.fetch(); - }, - - methods: { - fetch() { - os.api('i/registry/keys-with-type', { - scope: this.scope - }).then(keys => { - this.keys = Object.entries(keys).sort((a, b) => a[0].localeCompare(b[0])); - }); - }, - - async createKey() { - const { canceled, result } = await os.form(this.$ts._registry.createKey, { - key: { - type: 'string', - label: this.$ts._registry.key, - }, - value: { - type: 'string', - multiline: true, - label: this.$ts.value, - }, - scope: { - type: 'string', - label: this.$ts._registry.scope, - default: this.scope.join('/') - } - }); - if (canceled) return; - os.apiWithDialog('i/registry/set', { - scope: result.scope.split('/'), - key: result.key, - value: JSON5.parse(result.value), - }).then(() => { - this.fetch(); - }); - } - } -}); -</script> diff --git a/src/client/pages/settings/registry.value.vue b/src/client/pages/settings/registry.value.vue deleted file mode 100644 index 06be5737e9..0000000000 --- a/src/client/pages/settings/registry.value.vue +++ /dev/null @@ -1,149 +0,0 @@ -<template> -<FormBase> - <FormInfo warn>{{ $ts.editTheseSettingsMayBreakAccount }}</FormInfo> - - <template v-if="value"> - <FormGroup> - <FormKeyValueView> - <template #key>{{ $ts._registry.domain }}</template> - <template #value>{{ $ts.system }}</template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>{{ $ts._registry.scope }}</template> - <template #value>{{ scope.join('/') }}</template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>{{ $ts._registry.key }}</template> - <template #value>{{ xKey }}</template> - </FormKeyValueView> - </FormGroup> - - <FormGroup> - <FormTextarea tall v-model="valueForEditor" class="_monospace" style="tab-size: 2;"> - <span>{{ $ts.value }} (JSON)</span> - </FormTextarea> - <FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton> - </FormGroup> - - <FormKeyValueView> - <template #key>{{ $ts.updatedAt }}</template> - <template #value><MkTime :time="value.updatedAt" mode="detail"/></template> - </FormKeyValueView> - - <FormButton danger @click="del"><i class="fas fa-trash"></i> {{ $ts.delete }}</FormButton> - </template> -</FormBase> -</template> - -<script lang="ts"> -import { defineAsyncComponent, defineComponent } from 'vue'; -import * as JSON5 from 'json5'; -import FormInfo from '@client/components/debobigego/info.vue'; -import FormSwitch from '@client/components/form/switch.vue'; -import FormSelect from '@client/components/form/select.vue'; -import FormTextarea from '@client/components/form/textarea.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import FormKeyValueView from '@client/components/debobigego/key-value-view.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - FormInfo, - FormBase, - FormSelect, - FormSwitch, - FormButton, - FormTextarea, - FormGroup, - FormKeyValueView, - }, - - props: { - scope: { - required: true - }, - xKey: { - required: true - }, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.registry, - icon: 'fas fa-cogs', - bg: 'var(--bg)', - }, - value: null, - valueForEditor: null, - } - }, - - watch: { - key() { - this.fetch(); - }, - }, - - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - this.fetch(); - }, - - methods: { - fetch() { - os.api('i/registry/get-detail', { - scope: this.scope, - key: this.xKey - }).then(value => { - this.value = value; - this.valueForEditor = JSON5.stringify(this.value.value, null, '\t'); - }); - }, - - save() { - try { - JSON5.parse(this.valueForEditor); - } catch (e) { - os.dialog({ - type: 'error', - text: this.$ts.invalidValue - }); - return; - } - - os.dialog({ - type: 'warning', - text: this.$ts.saveConfirm, - showCancelButton: true - }).then(({ canceled }) => { - if (canceled) return; - os.apiWithDialog('i/registry/set', { - scope: this.scope, - key: this.xKey, - value: JSON5.parse(this.valueForEditor) - }); - }); - }, - - del() { - os.dialog({ - type: 'warning', - text: this.$ts.deleteConfirm, - showCancelButton: true - }).then(({ canceled }) => { - if (canceled) return; - os.apiWithDialog('i/registry/remove', { - scope: this.scope, - key: this.xKey - }); - }); - } - } -}); -</script> diff --git a/src/client/pages/settings/registry.vue b/src/client/pages/settings/registry.vue deleted file mode 100644 index e4fb230d5c..0000000000 --- a/src/client/pages/settings/registry.vue +++ /dev/null @@ -1,90 +0,0 @@ -<template> -<FormBase> - <FormGroup v-if="scopes"> - <template #label>{{ $ts.system }}</template> - <FormLink v-for="scope in scopes" :to="`/settings/registry/keys/system/${scope.join('/')}`" class="_monospace">{{ scope.join('/') }}</FormLink> - </FormGroup> - <FormButton @click="createKey" primary>{{ $ts._registry.createKey }}</FormButton> -</FormBase> -</template> - -<script lang="ts"> -import { defineAsyncComponent, defineComponent } from 'vue'; -import * as JSON5 from 'json5'; -import FormSwitch from '@client/components/form/switch.vue'; -import FormSelect from '@client/components/form/select.vue'; -import FormLink from '@client/components/debobigego/link.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import FormKeyValueView from '@client/components/debobigego/key-value-view.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - FormBase, - FormSelect, - FormSwitch, - FormButton, - FormLink, - FormGroup, - FormKeyValueView, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.registry, - icon: 'fas fa-cogs', - bg: 'var(--bg)', - }, - scopes: null, - } - }, - - created() { - this.fetch(); - }, - - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - methods: { - fetch() { - os.api('i/registry/scopes').then(scopes => { - this.scopes = scopes.slice().sort((a, b) => a.join('/').localeCompare(b.join('/'))); - }); - }, - - async createKey() { - const { canceled, result } = await os.form(this.$ts._registry.createKey, { - key: { - type: 'string', - label: this.$ts._registry.key, - }, - value: { - type: 'string', - multiline: true, - label: this.$ts.value, - }, - scope: { - type: 'string', - label: this.$ts._registry.scope, - } - }); - if (canceled) return; - os.apiWithDialog('i/registry/set', { - scope: result.scope.split('/'), - key: result.key, - value: JSON5.parse(result.value), - }).then(() => { - this.fetch(); - }); - } - } -}); -</script> diff --git a/src/client/pages/settings/security.vue b/src/client/pages/settings/security.vue deleted file mode 100644 index e051685a82..0000000000 --- a/src/client/pages/settings/security.vue +++ /dev/null @@ -1,158 +0,0 @@ -<template> -<FormBase> - <X2fa/> - <FormLink to="/settings/2fa"><template #icon><i class="fas fa-mobile-alt"></i></template>{{ $ts.twoStepAuthentication }}</FormLink> - <FormButton primary @click="change()">{{ $ts.changePassword }}</FormButton> - <FormPagination :pagination="pagination"> - <template #label>{{ $ts.signinHistory }}</template> - <template #default="{items}"> - <div class="_debobigegoPanel timnmucd" v-for="item in items" :key="item.id"> - <header> - <i v-if="item.success" class="fas fa-check icon succ"></i> - <i v-else class="fas fa-times-circle icon fail"></i> - <code class="ip _monospace">{{ item.ip }}</code> - <MkTime :time="item.createdAt" class="time"/> - </header> - </div> - </template> - </FormPagination> - <FormGroup> - <FormButton danger @click="regenerateToken"><i class="fas fa-sync-alt"></i> {{ $ts.regenerateLoginToken }}</FormButton> - <template #caption>{{ $ts.regenerateLoginTokenDescription }}</template> - </FormGroup> -</FormBase> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormLink from '@client/components/debobigego/link.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import FormPagination from '@client/components/debobigego/pagination.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - FormBase, - FormLink, - FormButton, - FormPagination, - FormGroup, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.security, - icon: 'fas fa-lock', - bg: 'var(--bg)', - }, - pagination: { - endpoint: 'i/signin-history', - limit: 5, - }, - } - }, - - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - methods: { - async change() { - const { canceled: canceled1, result: currentPassword } = await os.dialog({ - title: this.$ts.currentPassword, - input: { - type: 'password' - } - }); - if (canceled1) return; - - const { canceled: canceled2, result: newPassword } = await os.dialog({ - title: this.$ts.newPassword, - input: { - type: 'password' - } - }); - if (canceled2) return; - - const { canceled: canceled3, result: newPassword2 } = await os.dialog({ - title: this.$ts.newPasswordRetype, - input: { - type: 'password' - } - }); - if (canceled3) return; - - if (newPassword !== newPassword2) { - os.dialog({ - type: 'error', - text: this.$ts.retypedNotMatch - }); - return; - } - - os.apiWithDialog('i/change-password', { - currentPassword, - newPassword - }); - }, - - regenerateToken() { - os.dialog({ - title: this.$ts.password, - input: { - type: 'password' - } - }).then(({ canceled, result: password }) => { - if (canceled) return; - os.api('i/regenerate_token', { - password: password - }); - }); - }, - } -}); -</script> - -<style lang="scss" scoped> -.timnmucd { - padding: 16px; - - > header { - display: flex; - align-items: center; - - > .icon { - width: 1em; - margin-right: 0.75em; - - &.succ { - color: var(--success); - } - - &.fail { - color: var(--error); - } - } - - > .ip { - flex: 1; - min-width: 0; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - margin-right: 12px; - } - - > .time { - margin-left: auto; - opacity: 0.7; - } - } -} -</style> diff --git a/src/client/pages/settings/sounds.vue b/src/client/pages/settings/sounds.vue deleted file mode 100644 index 07310619c8..0000000000 --- a/src/client/pages/settings/sounds.vue +++ /dev/null @@ -1,155 +0,0 @@ -<template> -<FormBase> - <FormRange v-model="masterVolume" :min="0" :max="1" :step="0.05"> - <template #label><i class="fas fa-volume-icon"></i> {{ $ts.masterVolume }}</template> - </FormRange> - - <FormGroup> - <template #label>{{ $ts.sounds }}</template> - <FormButton v-for="type in Object.keys(sounds)" :key="type" :center="false" @click="edit(type)"> - {{ $t('_sfx.' + type) }} - <template #suffix>{{ sounds[type].type || $ts.none }}</template> - <template #suffixIcon><i class="fas fa-chevron-down"></i></template> - </FormButton> - </FormGroup> - - <FormButton @click="reset()" danger><i class="fas fa-redo"></i> {{ $ts.default }}</FormButton> -</FormBase> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import FormRange from '@client/components/debobigego/range.vue'; -import FormSelect from '@client/components/debobigego/select.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import * as os from '@client/os'; -import { ColdDeviceStorage } from '@client/store'; -import { playFile } from '@client/scripts/sound'; -import * as symbols from '@client/symbols'; - -const soundsTypes = [ - null, - 'syuilo/up', - 'syuilo/down', - 'syuilo/pope1', - 'syuilo/pope2', - 'syuilo/waon', - 'syuilo/popo', - 'syuilo/triple', - 'syuilo/poi1', - 'syuilo/poi2', - 'syuilo/pirori', - 'syuilo/pirori-wet', - 'syuilo/pirori-square-wet', - 'syuilo/square-pico', - 'syuilo/reverved', - 'syuilo/ryukyu', - 'syuilo/kick', - 'syuilo/snare', - 'syuilo/queue-jammed', - 'aisha/1', - 'aisha/2', - 'aisha/3', - 'noizenecio/kick_gaba', - 'noizenecio/kick_gaba2', -]; - -export default defineComponent({ - components: { - FormSelect, - FormButton, - FormBase, - FormRange, - FormGroup, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.sounds, - icon: 'fas fa-music', - bg: 'var(--bg)', - }, - sounds: {}, - } - }, - - computed: { - masterVolume: { // TODO: (ๅค้จ)้ขๆฐใซcomputedใไฝฟใใฎใฏใขใฌใชใฎใง็ดใ - get() { return ColdDeviceStorage.get('sound_masterVolume'); }, - set(value) { ColdDeviceStorage.set('sound_masterVolume', value); } - }, - volumeIcon() { - return this.masterVolume === 0 ? 'fas fa-volume-mute' : 'fas fa-volume-up'; - } - }, - - created() { - this.sounds.note = ColdDeviceStorage.get('sound_note'); - this.sounds.noteMy = ColdDeviceStorage.get('sound_noteMy'); - this.sounds.notification = ColdDeviceStorage.get('sound_notification'); - this.sounds.chat = ColdDeviceStorage.get('sound_chat'); - this.sounds.chatBg = ColdDeviceStorage.get('sound_chatBg'); - this.sounds.antenna = ColdDeviceStorage.get('sound_antenna'); - this.sounds.channel = ColdDeviceStorage.get('sound_channel'); - this.sounds.reversiPutBlack = ColdDeviceStorage.get('sound_reversiPutBlack'); - this.sounds.reversiPutWhite = ColdDeviceStorage.get('sound_reversiPutWhite'); - }, - - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - methods: { - async edit(type) { - const { canceled, result } = await os.form(this.$t('_sfx.' + type), { - type: { - type: 'enum', - enum: soundsTypes.map(x => ({ - value: x, - label: x == null ? this.$ts.none : x, - })), - label: this.$ts.sound, - default: this.sounds[type].type, - }, - volume: { - type: 'range', - mim: 0, - max: 1, - step: 0.05, - label: this.$ts.volume, - default: this.sounds[type].volume - }, - listen: { - type: 'button', - content: this.$ts.listen, - action: (_, values) => { - playFile(values.type, values.volume); - } - } - }); - if (canceled) return; - - const v = { - type: result.type, - volume: result.volume, - }; - - ColdDeviceStorage.set('sound_' + type, v); - this.sounds[type] = v; - }, - - reset() { - for (const sound of Object.keys(this.sounds)) { - const v = ColdDeviceStorage.default['sound_' + sound]; - ColdDeviceStorage.set('sound_' + sound, v); - this.sounds[sound] = v; - } - } - } -}); -</script> diff --git a/src/client/pages/settings/theme.install.vue b/src/client/pages/settings/theme.install.vue deleted file mode 100644 index 9fbb28929d..0000000000 --- a/src/client/pages/settings/theme.install.vue +++ /dev/null @@ -1,105 +0,0 @@ -<template> -<FormBase> - <FormGroup> - <FormTextarea v-model="installThemeCode"> - <span>{{ $ts._theme.code }}</span> - </FormTextarea> - <FormButton @click="() => preview(installThemeCode)" :disabled="installThemeCode == null" inline><i class="fas fa-eye"></i> {{ $ts.preview }}</FormButton> - </FormGroup> - - <FormButton @click="() => install(installThemeCode)" :disabled="installThemeCode == null" primary inline><i class="fas fa-check"></i> {{ $ts.install }}</FormButton> -</FormBase> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import * as JSON5 from 'json5'; -import FormTextarea from '@client/components/form/textarea.vue'; -import FormSelect from '@client/components/form/select.vue'; -import FormRadios from '@client/components/form/radios.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import FormLink from '@client/components/debobigego/link.vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import { applyTheme, validateTheme } from '@client/scripts/theme'; -import * as os from '@client/os'; -import { ColdDeviceStorage } from '@client/store'; -import { addTheme, getThemes } from '@client/theme-store'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - FormTextarea, - FormSelect, - FormRadios, - FormBase, - FormGroup, - FormLink, - FormButton, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts._theme.install, - icon: 'fas fa-download', - bg: 'var(--bg)', - }, - installThemeCode: null, - } - }, - - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - methods: { - parseThemeCode(code) { - let theme; - - try { - theme = JSON5.parse(code); - } catch (e) { - os.dialog({ - type: 'error', - text: this.$ts._theme.invalid - }); - return false; - } - if (!validateTheme(theme)) { - os.dialog({ - type: 'error', - text: this.$ts._theme.invalid - }); - return false; - } - if (getThemes().some(t => t.id === theme.id)) { - os.dialog({ - type: 'info', - text: this.$ts._theme.alreadyInstalled - }); - return false; - } - - return theme; - }, - - preview(code) { - const theme = this.parseThemeCode(code); - if (theme) applyTheme(theme, false); - }, - - async install(code) { - const theme = this.parseThemeCode(code); - if (!theme) return; - await addTheme(theme); - os.dialog({ - type: 'success', - text: this.$t('_theme.installed', { name: theme.name }) - }); - }, - } -}); -</script> diff --git a/src/client/pages/settings/theme.manage.vue b/src/client/pages/settings/theme.manage.vue deleted file mode 100644 index 1a11a664f0..0000000000 --- a/src/client/pages/settings/theme.manage.vue +++ /dev/null @@ -1,105 +0,0 @@ -<template> -<FormBase> - <FormSelect v-model="selectedThemeId"> - <template #label>{{ $ts.theme }}</template> - <optgroup :label="$ts._theme.installedThemes"> - <option v-for="x in installedThemes" :value="x.id" :key="x.id">{{ x.name }}</option> - </optgroup> - <optgroup :label="$ts._theme.builtinThemes"> - <option v-for="x in builtinThemes" :value="x.id" :key="x.id">{{ x.name }}</option> - </optgroup> - </FormSelect> - <template v-if="selectedTheme"> - <FormInput readonly :modelValue="selectedTheme.author"> - <span>{{ $ts.author }}</span> - </FormInput> - <FormTextarea readonly :modelValue="selectedTheme.desc" v-if="selectedTheme.desc"> - <span>{{ $ts._theme.description }}</span> - </FormTextarea> - <FormTextarea readonly tall :modelValue="selectedThemeCode"> - <span>{{ $ts._theme.code }}</span> - <template #desc><button @click="copyThemeCode()" class="_textButton">{{ $ts.copy }}</button></template> - </FormTextarea> - <FormButton @click="uninstall()" danger v-if="!builtinThemes.some(t => t.id == selectedTheme.id)"><i class="fas fa-trash-alt"></i> {{ $ts.uninstall }}</FormButton> - </template> -</FormBase> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import * as JSON5 from 'json5'; -import FormTextarea from '@client/components/debobigego/textarea.vue'; -import FormSelect from '@client/components/debobigego/select.vue'; -import FormRadios from '@client/components/debobigego/radios.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import FormInput from '@client/components/debobigego/input.vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import { Theme, builtinThemes } from '@client/scripts/theme'; -import copyToClipboard from '@client/scripts/copy-to-clipboard'; -import * as os from '@client/os'; -import { ColdDeviceStorage } from '@client/store'; -import { getThemes, removeTheme } from '@client/theme-store'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - FormTextarea, - FormSelect, - FormRadios, - FormBase, - FormGroup, - FormInput, - FormButton, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts._theme.manage, - icon: 'fas fa-folder-open', - bg: 'var(--bg)', - }, - installedThemes: getThemes(), - builtinThemes, - selectedThemeId: null, - } - }, - - computed: { - themes(): Theme[] { - return this.builtinThemes.concat(this.installedThemes); - }, - - selectedTheme() { - if (this.selectedThemeId == null) return null; - return this.themes.find(x => x.id === this.selectedThemeId); - }, - - selectedThemeCode() { - if (this.selectedTheme == null) return null; - return JSON5.stringify(this.selectedTheme, null, '\t'); - }, - }, - - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - methods: { - copyThemeCode() { - copyToClipboard(this.selectedThemeCode); - os.success(); - }, - - uninstall() { - removeTheme(this.selectedTheme); - this.installedThemes = this.installedThemes.filter(t => t.id !== this.selectedThemeId); - this.selectedThemeId = null; - os.success(); - }, - } -}); -</script> diff --git a/src/client/pages/settings/theme.vue b/src/client/pages/settings/theme.vue deleted file mode 100644 index c6be42251c..0000000000 --- a/src/client/pages/settings/theme.vue +++ /dev/null @@ -1,424 +0,0 @@ -<template> -<FormBase> - <FormGroup> - <div class="rfqxtzch _debobigegoItem _debobigegoPanel"> - <div class="darkMode"> - <div class="toggleWrapper"> - <input type="checkbox" class="dn" id="dn" v-model="darkMode"/> - <label for="dn" class="toggle"> - <span class="before">{{ $ts.light }}</span> - <span class="after">{{ $ts.dark }}</span> - <span class="toggle__handler"> - <span class="crater crater--1"></span> - <span class="crater crater--2"></span> - <span class="crater crater--3"></span> - </span> - <span class="star star--1"></span> - <span class="star star--2"></span> - <span class="star star--3"></span> - <span class="star star--4"></span> - <span class="star star--5"></span> - <span class="star star--6"></span> - </label> - </div> - </div> - </div> - <FormSwitch v-model="syncDeviceDarkMode">{{ $ts.syncDeviceDarkMode }}</FormSwitch> - </FormGroup> - - <template v-if="darkMode"> - <FormSelect v-model="darkThemeId"> - <template #label>{{ $ts.themeForDarkMode }}</template> - <optgroup :label="$ts.darkThemes"> - <option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option> - </optgroup> - <optgroup :label="$ts.lightThemes"> - <option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option> - </optgroup> - </FormSelect> - <FormSelect v-model="lightThemeId"> - <template #label>{{ $ts.themeForLightMode }}</template> - <optgroup :label="$ts.lightThemes"> - <option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option> - </optgroup> - <optgroup :label="$ts.darkThemes"> - <option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option> - </optgroup> - </FormSelect> - </template> - <template v-else> - <FormSelect v-model="lightThemeId"> - <template #label>{{ $ts.themeForLightMode }}</template> - <optgroup :label="$ts.lightThemes"> - <option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option> - </optgroup> - <optgroup :label="$ts.darkThemes"> - <option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option> - </optgroup> - </FormSelect> - <FormSelect v-model="darkThemeId"> - <template #label>{{ $ts.themeForDarkMode }}</template> - <optgroup :label="$ts.darkThemes"> - <option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option> - </optgroup> - <optgroup :label="$ts.lightThemes"> - <option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option> - </optgroup> - </FormSelect> - </template> - - <FormButton primary v-if="wallpaper == null" @click="setWallpaper">{{ $ts.setWallpaper }}</FormButton> - <FormButton primary v-else @click="wallpaper = null">{{ $ts.removeWallpaper }}</FormButton> - - <FormGroup> - <FormLink to="https://assets.misskey.io/theme/list" external><template #icon><i class="fas fa-globe"></i></template>{{ $ts._theme.explore }}</FormLink> - <FormLink to="/settings/theme/install"><template #icon><i class="fas fa-download"></i></template>{{ $ts._theme.install }}</FormLink> - </FormGroup> - - <FormGroup> - <FormLink to="/theme-editor"><template #icon><i class="fas fa-paint-roller"></i></template>{{ $ts._theme.make }}</FormLink> - <!--<FormLink to="/advanced-theme-editor"><template #icon><i class="fas fa-paint-roller"></i></template>{{ $ts._theme.make }} ({{ $ts.advanced }})</FormLink>--> - </FormGroup> - - <FormLink to="/settings/theme/manage"><template #icon><i class="fas fa-folder-open"></i></template>{{ $ts._theme.manage }}<template #suffix>{{ themesCount }}</template></FormLink> -</FormBase> -</template> - -<script lang="ts"> -import { computed, defineComponent, onActivated, onMounted, ref, watch } from 'vue'; -import FormSwitch from '@client/components/debobigego/switch.vue'; -import FormSelect from '@client/components/debobigego/select.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import FormLink from '@client/components/debobigego/link.vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import { builtinThemes } from '@client/scripts/theme'; -import { selectFile } from '@client/scripts/select-file'; -import { isDeviceDarkmode } from '@client/scripts/is-device-darkmode'; -import { ColdDeviceStorage } from '@client/store'; -import { i18n } from '@client/i18n'; -import { defaultStore } from '@client/store'; -import { fetchThemes, getThemes } from '@client/theme-store'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - FormSwitch, - FormSelect, - FormBase, - FormGroup, - FormLink, - FormButton, - }, - - emits: ['info'], - - setup(props, { emit }) { - const INFO = { - title: i18n.locale.theme, - icon: 'fas fa-palette', - bg: 'var(--bg)', - }; - - const installedThemes = ref(getThemes()); - const themes = computed(() => builtinThemes.concat(installedThemes.value)); - const darkThemes = computed(() => themes.value.filter(t => t.base == 'dark' || t.kind == 'dark')); - const lightThemes = computed(() => themes.value.filter(t => t.base == 'light' || t.kind == 'light')); - const darkTheme = ColdDeviceStorage.ref('darkTheme'); - const darkThemeId = computed({ - get() { - return darkTheme.value.id; - }, - set(id) { - ColdDeviceStorage.set('darkTheme', themes.value.find(x => x.id === id)) - } - }); - const lightTheme = ColdDeviceStorage.ref('lightTheme'); - const lightThemeId = computed({ - get() { - return lightTheme.value.id; - }, - set(id) { - ColdDeviceStorage.set('lightTheme', themes.value.find(x => x.id === id)) - } - }); - const darkMode = computed(defaultStore.makeGetterSetter('darkMode')); - const syncDeviceDarkMode = computed(ColdDeviceStorage.makeGetterSetter('syncDeviceDarkMode')); - const wallpaper = ref(localStorage.getItem('wallpaper')); - const themesCount = installedThemes.value.length; - - watch(syncDeviceDarkMode, () => { - if (syncDeviceDarkMode) { - defaultStore.set('darkMode', isDeviceDarkmode()); - } - }); - - watch(wallpaper, () => { - if (wallpaper.value == null) { - localStorage.removeItem('wallpaper'); - } else { - localStorage.setItem('wallpaper', wallpaper.value); - } - location.reload(); - }); - - onMounted(() => { - emit('info', INFO); - }); - - onActivated(() => { - fetchThemes().then(() => { - installedThemes.value = getThemes(); - }); - }); - - fetchThemes().then(() => { - installedThemes.value = getThemes(); - }); - - return { - [symbols.PAGE_INFO]: INFO, - darkThemes, - lightThemes, - darkThemeId, - lightThemeId, - darkMode, - syncDeviceDarkMode, - themesCount, - wallpaper, - setWallpaper(e) { - selectFile(e.currentTarget || e.target, null, false).then(file => { - wallpaper.value = file.url; - }); - }, - }; - } -}); -</script> - -<style lang="scss" scoped> -.rfqxtzch { - padding: 16px; - - > .darkMode { - position: relative; - padding: 32px 0; - - &.disabled { - opacity: 0.7; - - &, * { - cursor: not-allowed !important; - } - } - - .toggleWrapper { - position: absolute; - top: 50%; - left: 50%; - overflow: hidden; - padding: 0 100px; - transform: translate3d(-50%, -50%, 0); - - input { - position: absolute; - left: -99em; - } - } - - .toggle { - cursor: pointer; - display: inline-block; - position: relative; - width: 90px; - height: 50px; - background-color: #83D8FF; - border-radius: 90px - 6; - transition: background-color 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important; - - > .before, > .after { - position: absolute; - top: 15px; - font-size: 18px; - transition: color 1s ease; - } - - > .before { - left: -70px; - color: var(--accent); - } - - > .after { - right: -68px; - color: var(--fg); - } - } - - .toggle__handler { - display: inline-block; - position: relative; - z-index: 1; - top: 3px; - left: 3px; - width: 50px - 6; - height: 50px - 6; - background-color: #FFCF96; - border-radius: 50px; - box-shadow: 0 2px 6px rgba(0,0,0,.3); - transition: all 400ms cubic-bezier(0.68, -0.55, 0.265, 1.55) !important; - transform: rotate(-45deg); - - .crater { - position: absolute; - background-color: #E8CDA5; - opacity: 0; - transition: opacity 200ms ease-in-out !important; - border-radius: 100%; - } - - .crater--1 { - top: 18px; - left: 10px; - width: 4px; - height: 4px; - } - - .crater--2 { - top: 28px; - left: 22px; - width: 6px; - height: 6px; - } - - .crater--3 { - top: 10px; - left: 25px; - width: 8px; - height: 8px; - } - } - - .star { - position: absolute; - background-color: #ffffff; - transition: all 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important; - border-radius: 50%; - } - - .star--1 { - top: 10px; - left: 35px; - z-index: 0; - width: 30px; - height: 3px; - } - - .star--2 { - top: 18px; - left: 28px; - z-index: 1; - width: 30px; - height: 3px; - } - - .star--3 { - top: 27px; - left: 40px; - z-index: 0; - width: 30px; - height: 3px; - } - - .star--4, - .star--5, - .star--6 { - opacity: 0; - transition: all 300ms 0 cubic-bezier(0.445, 0.05, 0.55, 0.95) !important; - } - - .star--4 { - top: 16px; - left: 11px; - z-index: 0; - width: 2px; - height: 2px; - transform: translate3d(3px,0,0); - } - - .star--5 { - top: 32px; - left: 17px; - z-index: 0; - width: 3px; - height: 3px; - transform: translate3d(3px,0,0); - } - - .star--6 { - top: 36px; - left: 28px; - z-index: 0; - width: 2px; - height: 2px; - transform: translate3d(3px,0,0); - } - - input:checked { - + .toggle { - background-color: #749DD6; - - > .before { - color: var(--fg); - } - - > .after { - color: var(--accent); - } - - .toggle__handler { - background-color: #FFE5B5; - transform: translate3d(40px, 0, 0) rotate(0); - - .crater { opacity: 1; } - } - - .star--1 { - width: 2px; - height: 2px; - } - - .star--2 { - width: 4px; - height: 4px; - transform: translate3d(-5px, 0, 0); - } - - .star--3 { - width: 2px; - height: 2px; - transform: translate3d(-7px, 0, 0); - } - - .star--4, - .star--5, - .star--6 { - opacity: 1; - transform: translate3d(0,0,0); - } - - .star--4 { - transition: all 300ms 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important; - } - - .star--5 { - transition: all 300ms 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important; - } - - .star--6 { - transition: all 300ms 400ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important; - } - } - } - } -} -</style> diff --git a/src/client/pages/settings/update.vue b/src/client/pages/settings/update.vue deleted file mode 100644 index 8bc459e936..0000000000 --- a/src/client/pages/settings/update.vue +++ /dev/null @@ -1,95 +0,0 @@ -<template> -<FormBase> - <template v-if="meta"> - <FormInfo v-if="version === meta.version">{{ $ts.youAreRunningUpToDateClient }}</FormInfo> - <FormInfo v-else warn>{{ $ts.newVersionOfClientAvailable }}</FormInfo> - </template> - <FormGroup> - <template #label>{{ instanceName }}</template> - <FormKeyValueView> - <template #key>{{ $ts.currentVersion }}</template> - <template #value>{{ version }}</template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>{{ $ts.latestVersion }}</template> - <template #value v-if="meta">{{ meta.version }}</template> - <template #value v-else><MkEllipsis/></template> - </FormKeyValueView> - </FormGroup> - <FormGroup> - <template #label>Misskey</template> - <FormKeyValueView> - <template #key>{{ $ts.latestVersion }}</template> - <template #value v-if="releases">{{ releases[0].tag_name }}</template> - <template #value v-else><MkEllipsis/></template> - </FormKeyValueView> - <template #caption v-if="releases"><MkTime :time="releases[0].published_at" mode="detail"/></template> - </FormGroup> -</FormBase> -</template> - -<script lang="ts"> -import { defineAsyncComponent, defineComponent } from 'vue'; -import FormSwitch from '@client/components/form/switch.vue'; -import FormSelect from '@client/components/form/select.vue'; -import FormLink from '@client/components/debobigego/link.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import FormKeyValueView from '@client/components/debobigego/key-value-view.vue'; -import FormInfo from '@client/components/debobigego/info.vue'; -import * as os from '@client/os'; -import { version, instanceName } from '@client/config'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - FormBase, - FormSelect, - FormSwitch, - FormButton, - FormLink, - FormGroup, - FormKeyValueView, - FormInfo, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: 'Misskey Update', - icon: 'fas fa-sync-alt', - bg: 'var(--bg)', - }, - version, - instanceName, - releases: null, - meta: null - } - }, - - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - - os.api('meta', { - detail: false - }).then(meta => { - this.meta = meta; - localStorage.setItem('v', meta.version); - }); - - fetch('https://api.github.com/repos/misskey-dev/misskey/releases', { - method: 'GET', - }) - .then(res => res.json()) - .then(res => { - this.releases = res; - }); - }, - - methods: { - } -}); -</script> diff --git a/src/client/pages/settings/word-mute.vue b/src/client/pages/settings/word-mute.vue deleted file mode 100644 index 53948b1b1e..0000000000 --- a/src/client/pages/settings/word-mute.vue +++ /dev/null @@ -1,110 +0,0 @@ -<template> -<div> - <MkTab v-model="tab"> - <option value="soft">{{ $ts._wordMute.soft }}</option> - <option value="hard">{{ $ts._wordMute.hard }}</option> - </MkTab> - <FormBase> - <div class="_debobigegoItem"> - <div v-show="tab === 'soft'"> - <FormInfo>{{ $ts._wordMute.softDescription }}</FormInfo> - <FormTextarea v-model="softMutedWords"> - <span>{{ $ts._wordMute.muteWords }}</span> - <template #desc>{{ $ts._wordMute.muteWordsDescription }}<br>{{ $ts._wordMute.muteWordsDescription2 }}</template> - </FormTextarea> - </div> - <div v-show="tab === 'hard'"> - <FormInfo>{{ $ts._wordMute.hardDescription }}</FormInfo> - <FormTextarea v-model="hardMutedWords"> - <span>{{ $ts._wordMute.muteWords }}</span> - <template #desc>{{ $ts._wordMute.muteWordsDescription }}<br>{{ $ts._wordMute.muteWordsDescription2 }}</template> - </FormTextarea> - <FormKeyValueView v-if="hardWordMutedNotesCount != null"> - <template #key>{{ $ts._wordMute.mutedNotes }}</template> - <template #value>{{ number(hardWordMutedNotesCount) }}</template> - </FormKeyValueView> - </div> - </div> - <FormButton @click="save()" primary inline :disabled="!changed"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton> - </FormBase> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import FormTextarea from '@client/components/form/textarea.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormKeyValueView from '@client/components/debobigego/key-value-view.vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import FormInfo from '@client/components/debobigego/info.vue'; -import MkTab from '@client/components/tab.vue'; -import * as os from '@client/os'; -import number from '@client/filters/number'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - FormBase, - FormButton, - FormTextarea, - FormKeyValueView, - MkTab, - FormInfo, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.wordMute, - icon: 'fas fa-comment-slash', - bg: 'var(--bg)', - }, - tab: 'soft', - softMutedWords: '', - hardMutedWords: '', - hardWordMutedNotesCount: null, - changed: false, - } - }, - - watch: { - softMutedWords: { - handler() { - this.changed = true; - }, - deep: true - }, - hardMutedWords: { - handler() { - this.changed = true; - }, - deep: true - }, - }, - - async created() { - this.softMutedWords = this.$store.state.mutedWords.map(x => x.join(' ')).join('\n'); - this.hardMutedWords = this.$i.mutedWords.map(x => x.join(' ')).join('\n'); - - this.hardWordMutedNotesCount = (await os.api('i/get-word-muted-notes-count', {})).count; - }, - - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - methods: { - async save() { - this.$store.set('mutedWords', this.softMutedWords.trim().split('\n').map(x => x.trim().split(' '))); - await os.api('i/update', { - mutedWords: this.hardMutedWords.trim().split('\n').map(x => x.trim().split(' ')), - }); - this.changed = false; - }, - - number - } -}); -</script> diff --git a/src/client/pages/share.vue b/src/client/pages/share.vue deleted file mode 100644 index 70a9661dd0..0000000000 --- a/src/client/pages/share.vue +++ /dev/null @@ -1,184 +0,0 @@ -<template> -<div class=""> - <section class="_section"> - <div class="_content"> - <XPostForm - v-if="state === 'writing'" - fixed - :share="true" - :initial-text="initialText" - :initial-visibility="visibility" - :initial-files="files" - :initial-local-only="localOnly" - :reply="reply" - :renote="renote" - :visible-users="visibleUsers" - @posted="state = 'posted'" - class="_panel" - /> - <MkButton v-else-if="state === 'posted'" primary @click="close()" class="close">{{ $ts.close }}</MkButton> - </div> - </section> -</div> -</template> - -<script lang="ts"> -// SPECIFICATION: /src/docs/ja-JP/advanced/share-page.md - -import { defineComponent } from 'vue'; -import MkButton from '@client/components/ui/button.vue'; -import XPostForm from '@client/components/post-form.vue'; -import * as os from '@client/os'; -import { noteVisibilities } from '@/types'; -import { parseAcct } from '@/misc/acct'; -import * as symbols from '@client/symbols'; -import * as Misskey from 'misskey-js'; - -export default defineComponent({ - components: { - XPostForm, - MkButton, - }, - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.share, - icon: 'fas fa-share-alt' - }, - state: 'fetching' as 'fetching' | 'writing' | 'posted', - - title: null as string | null, - initialText: null as string | null, - reply: null as Misskey.entities.Note | null, - renote: null as Misskey.entities.Note | null, - visibility: null as string | null, - localOnly: null as boolean | null, - files: [] as Misskey.entities.DriveFile[], - visibleUsers: [] as Misskey.entities.User[], - } - }, - - async created() { - const urlParams = new URLSearchParams(window.location.search); - - this.title = urlParams.get('title'); - const text = urlParams.get('text'); - const url = urlParams.get('url'); - - let noteText = ''; - if (this.title) noteText += `[ ${this.title} ]\n`; - // Googleใใฅใผในๅฏพ็ญ - if (text?.startsWith(`${this.title}.\n`)) noteText += text.replace(`${this.title}.\n`, ''); - else if (text && this.title !== text) noteText += `${text}\n`; - if (url) noteText += `${url}`; - this.initialText = noteText.trim(); - - const visibility = urlParams.get('visibility'); - if (noteVisibilities.includes(visibility)) { - this.visibility = visibility; - } - - if (this.visibility === 'specified') { - const visibleUserIds = urlParams.get('visibleUserIds'); - const visibleAccts = urlParams.get('visibleAccts'); - await Promise.all( - [ - ...(visibleUserIds ? visibleUserIds.split(',').map(userId => ({ userId })) : []), - ...(visibleAccts ? visibleAccts.split(',').map(parseAcct) : []) - ] - // TypeScriptใฎๆ็คบ้ใใซๅคๆใใ - .map(q => 'username' in q ? { username: q.username, host: q.host === null ? undefined : q.host } : q) - .map(q => os.api('users/show', q) - .then(user => { - this.visibleUsers.push(user); - }, () => { - console.error(`Invalid user query: ${JSON.stringify(q)}`); - }) - ) - ); - } - - const localOnly = urlParams.get('localOnly'); - if (localOnly === '0') this.localOnly = false; - else if (localOnly === '1') this.localOnly = true; - - try { - //#region Reply - const replyId = urlParams.get('replyId'); - const replyUri = urlParams.get('replyUri'); - if (replyId) { - this.reply = await os.api('notes/show', { - noteId: replyId - }); - } else if (replyUri) { - const obj = await os.api('ap/show', { - uri: replyUri - }); - if (obj.type === 'Note') { - this.reply = obj.object; - } - } - //#endregion - - //#region Renote - const renoteId = urlParams.get('renoteId'); - const renoteUri = urlParams.get('renoteUri'); - if (renoteId) { - this.renote = await os.api('notes/show', { - noteId: renoteId - }); - } else if (renoteUri) { - const obj = await os.api('ap/show', { - uri: renoteUri - }); - if (obj.type === 'Note') { - this.renote = obj.object; - } - } - //#endregion - - //#region Drive files - const fileIds = urlParams.get('fileIds'); - if (fileIds) { - await Promise.all( - fileIds.split(',') - .map(fileId => os.api('drive/files/show', { fileId }) - .then(file => { - this.files.push(file); - }, () => { - console.error(`Failed to fetch a file ${fileId}`); - }) - ) - ); - } - //#endregion - } catch (e) { - os.dialog({ - type: 'error', - title: e.message, - text: e.name - }); - } - - this.state = 'writing'; - }, - - methods: { - close() { - window.close(); - - // ้ใใชใใใฐ100msๅพใฟใคใ ใฉใคใณใซ - setTimeout(() => { - this.$router.push('/'); - }, 100); - } - } -}); -</script> - -<style lang="scss" scoped> -.close { - margin: 16px auto; -} -</style> diff --git a/src/client/pages/signup-complete.vue b/src/client/pages/signup-complete.vue deleted file mode 100644 index dada92031a..0000000000 --- a/src/client/pages/signup-complete.vue +++ /dev/null @@ -1,50 +0,0 @@ -<template> -<div> - {{ $ts.processing }} -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; -import { login } from '@client/account'; - -export default defineComponent({ - components: { - - }, - - props: { - code: { - type: String, - required: true - } - }, - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.signup, - icon: 'fas fa-user' - }, - } - }, - - mounted() { - os.apiWithDialog('signup-pending', { - code: this.code, - }).then(res => { - login(res.i, '/'); - }); - }, - - methods: { - - } -}); -</script> - -<style lang="scss" scoped> - -</style> diff --git a/src/client/pages/tag.vue b/src/client/pages/tag.vue deleted file mode 100644 index 3ca9fe5c0c..0000000000 --- a/src/client/pages/tag.vue +++ /dev/null @@ -1,57 +0,0 @@ -<template> -<div class="_section"> - <XNotes ref="notes" class="_content" :pagination="pagination" @before="before" @after="after"/> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import Progress from '@client/scripts/loading'; -import XNotes from '@client/components/notes.vue'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - XNotes - }, - - props: { - tag: { - type: String, - required: true - } - }, - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.tag, - icon: 'fas fa-hashtag' - }, - pagination: { - endpoint: 'notes/search-by-tag', - limit: 10, - params: () => ({ - tag: this.tag, - }) - }, - }; - }, - - watch: { - tag() { - (this.$refs.notes as any).reload(); - } - }, - - methods: { - before() { - Progress.start(); - }, - - after() { - Progress.done(); - } - } -}); -</script> diff --git a/src/client/pages/test.vue b/src/client/pages/test.vue deleted file mode 100644 index fbab0112ed..0000000000 --- a/src/client/pages/test.vue +++ /dev/null @@ -1,259 +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 '@client/components/ui/button.vue'; -import MkInput from '@client/components/form/input.vue'; -import MkSwitch from '@client/components/form/switch.vue'; -import MkTextarea from '@client/components/form/textarea.vue'; -import MkRadio from '@client/components/form/radio.vue'; -import * as os from '@client/os'; -import * as symbols from '@client/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> diff --git a/src/client/pages/theme-editor.vue b/src/client/pages/theme-editor.vue deleted file mode 100644 index 3b10396ab8..0000000000 --- a/src/client/pages/theme-editor.vue +++ /dev/null @@ -1,306 +0,0 @@ -<template> -<FormBase class="cwepdizn"> - <div class="_debobigegoItem colorPicker"> - <div class="_debobigegoLabel">{{ $ts.backgroundColor }}</div> - <div class="_debobigegoPanel colors"> - <div class="row"> - <button v-for="color in bgColors.filter(x => x.kind === 'light')" :key="color.color" @click="setBgColor(color)" class="color _button" :class="{ active: theme.props.bg === color.color }"> - <div class="preview" :style="{ background: color.forPreview }"></div> - </button> - </div> - <div class="row"> - <button v-for="color in bgColors.filter(x => x.kind === 'dark')" :key="color.color" @click="setBgColor(color)" class="color _button" :class="{ active: theme.props.bg === color.color }"> - <div class="preview" :style="{ background: color.forPreview }"></div> - </button> - </div> - </div> - </div> - <div class="_debobigegoItem colorPicker"> - <div class="_debobigegoLabel">{{ $ts.accentColor }}</div> - <div class="_debobigegoPanel colors"> - <div class="row"> - <button v-for="color in accentColors" :key="color" @click="setAccentColor(color)" class="color rounded _button" :class="{ active: theme.props.accent === color }"> - <div class="preview" :style="{ background: color }"></div> - </button> - </div> - </div> - </div> - <div class="_debobigegoItem colorPicker"> - <div class="_debobigegoLabel">{{ $ts.textColor }}</div> - <div class="_debobigegoPanel colors"> - <div class="row"> - <button v-for="color in fgColors" :key="color" @click="setFgColor(color)" class="color char _button" :class="{ active: (theme.props.fg === color.forLight) || (theme.props.fg === color.forDark) }"> - <div class="preview" :style="{ color: color.forPreview ? color.forPreview : theme.base === 'light' ? '#5f5f5f' : '#dadada' }">A</div> - </button> - </div> - </div> - </div> - - <FormGroup v-if="codeEnabled"> - <FormTextarea v-model="themeCode" tall> - <span>{{ $ts._theme.code }}</span> - </FormTextarea> - <FormButton @click="applyThemeCode" primary>{{ $ts.apply }}</FormButton> - </FormGroup> - <FormButton v-else @click="codeEnabled = true"><i class="fas fa-code"></i> {{ $ts.editCode }}</FormButton> - - <FormGroup v-if="descriptionEnabled"> - <FormTextarea v-model="description"> - <span>{{ $ts._theme.description }}</span> - </FormTextarea> - </FormGroup> - <FormButton v-else @click="descriptionEnabled = true">{{ $ts.addDescription }}</FormButton> - - <FormGroup> - <FormButton @click="showPreview"><i class="fas fa-eye"></i> {{ $ts.preview }}</FormButton> - <FormButton @click="saveAs" primary><i class="fas fa-save"></i> {{ $ts.saveAs }}</FormButton> - </FormGroup> -</FormBase> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import { toUnicode } from 'punycode/'; -import * as tinycolor from 'tinycolor2'; -import { v4 as uuid} from 'uuid'; -import * as JSON5 from 'json5'; - -import FormBase from '@client/components/debobigego/base.vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import FormTextarea from '@client/components/debobigego/textarea.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; - -import { Theme, applyTheme, validateTheme, darkTheme, lightTheme } from '@client/scripts/theme'; -import { host } from '@client/config'; -import * as os from '@client/os'; -import { ColdDeviceStorage } from '@client/store'; -import { addTheme } from '@client/theme-store'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - FormBase, - FormButton, - FormTextarea, - FormGroup, - }, - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.themeEditor, - icon: 'fas fa-palette', - }, - theme: { - base: 'light', - props: lightTheme.props - } as Theme, - codeEnabled: false, - descriptionEnabled: false, - description: null, - themeCode: null, - bgColors: [ - { color: '#f5f5f5', kind: 'light', forPreview: '#f5f5f5' }, - { color: '#f0eee9', kind: 'light', forPreview: '#f3e2b9' }, - { color: '#e9eff0', kind: 'light', forPreview: '#bfe3e8' }, - { color: '#f0e9ee', kind: 'light', forPreview: '#f1d1e8' }, - { color: '#dce2e0', kind: 'light', forPreview: '#a4dccc' }, - { color: '#e2e0dc', kind: 'light', forPreview: '#d8c7a5' }, - { color: '#d5dbe0', kind: 'light', forPreview: '#b0cae0' }, - { color: '#dad5d5', kind: 'light', forPreview: '#d6afaf' }, - { color: '#2b2b2b', kind: 'dark', forPreview: '#444444' }, - { color: '#362e29', kind: 'dark', forPreview: '#735c4d' }, - { color: '#303629', kind: 'dark', forPreview: '#506d2f' }, - { color: '#293436', kind: 'dark', forPreview: '#258192' }, - { color: '#2e2936', kind: 'dark', forPreview: '#504069' }, - { color: '#252722', kind: 'dark', forPreview: '#3c462f' }, - { color: '#212525', kind: 'dark', forPreview: '#303e3e' }, - { color: '#191919', kind: 'dark', forPreview: '#272727' }, - ], - accentColors: ['#e36749', '#f29924', '#98c934', '#34c9a9', '#34a1c9', '#606df7', '#8d34c9', '#e84d83'], - fgColors: [ - { color: 'none', forLight: '#5f5f5f', forDark: '#dadada', forPreview: null }, - { color: 'red', forLight: '#7f6666', forDark: '#e4d1d1', forPreview: '#ca4343' }, - { color: 'yellow', forLight: '#736955', forDark: '#e0d5c0', forPreview: '#d49923' }, - { color: 'green', forLight: '#586d5b', forDark: '#d1e4d4', forPreview: '#4cbd5c' }, - { color: 'cyan', forLight: '#5d7475', forDark: '#d1e3e4', forPreview: '#2abdc3' }, - { color: 'blue', forLight: '#676880', forDark: '#d1d2e4', forPreview: '#7275d8' }, - { color: 'pink', forLight: '#84667d', forDark: '#e4d1e0', forPreview: '#b12390' }, - ], - changed: false, - } - }, - - created() { - this.$watch('theme', this.apply, { deep: true }); - window.addEventListener('beforeunload', this.beforeunload); - }, - - beforeUnmount() { - window.removeEventListener('beforeunload', this.beforeunload); - }, - - async beforeRouteLeave(to, from) { - if (this.changed && !(await this.leaveConfirm())) { - return false; - } - }, - - methods: { - beforeunload(e: BeforeUnloadEvent) { - if (this.changed) { - e.preventDefault(); - e.returnValue = ''; - } - }, - - async leaveConfirm(): Promise<boolean> { - const { canceled } = await os.dialog({ - type: 'warning', - text: this.$ts.leaveConfirm, - showCancelButton: true - }); - return !canceled; - }, - - showPreview() { - os.pageWindow('preview'); - }, - - setBgColor(color) { - if (this.theme.base != color.kind) { - const base = color.kind === 'dark' ? darkTheme : lightTheme; - for (const prop of Object.keys(base.props)) { - if (prop === 'accent') continue; - if (prop === 'fg') continue; - this.theme.props[prop] = base.props[prop]; - } - } - this.theme.base = color.kind; - this.theme.props.bg = color.color; - - if (this.theme.props.fg) { - const matchedFgColor = this.fgColors.find(x => [tinycolor(x.forLight).toRgbString(), tinycolor(x.forDark).toRgbString()].includes(tinycolor(this.theme.props.fg).toRgbString())); - if (matchedFgColor) this.setFgColor(matchedFgColor); - } - }, - - setAccentColor(color) { - this.theme.props.accent = color; - }, - - setFgColor(color) { - this.theme.props.fg = this.theme.base === 'light' ? color.forLight : color.forDark; - }, - - apply() { - this.themeCode = JSON5.stringify(this.theme, null, '\t'); - applyTheme(this.theme, false); - this.changed = true; - }, - - applyThemeCode() { - let parsed; - - try { - parsed = JSON5.parse(this.themeCode); - } catch (e) { - os.dialog({ - type: 'error', - text: this.$ts._theme.invalid - }); - return; - } - - this.theme = parsed; - }, - - async saveAs() { - const { canceled, result: name } = await os.dialog({ - title: this.$ts.name, - input: { - allowEmpty: false - } - }); - if (canceled) return; - - this.theme.id = uuid(); - this.theme.name = name; - this.theme.author = `@${this.$i.username}@${toUnicode(host)}`; - if (this.description) this.theme.desc = this.description; - addTheme(this.theme); - applyTheme(this.theme); - if (this.$store.state.darkMode) { - ColdDeviceStorage.set('darkTheme', this.theme); - } else { - ColdDeviceStorage.set('lightTheme', this.theme); - } - this.changed = false; - os.dialog({ - type: 'success', - text: this.$t('_theme.installed', { name: this.theme.name }) - }); - } - } -}); -</script> - -<style lang="scss" scoped> -.cwepdizn { - max-width: 800px; - margin: 0 auto; - - > .colorPicker { - > .colors { - padding: 32px; - text-align: center; - - > .row { - > .color { - display: inline-block; - position: relative; - width: 64px; - height: 64px; - border-radius: 8px; - - > .preview { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - margin: auto; - width: 42px; - height: 42px; - border-radius: 4px; - box-shadow: 0 2px 4px rgb(0 0 0 / 30%); - transition: transform 0.15s ease; - } - - &:hover { - > .preview { - transform: scale(1.1); - } - } - - &.active { - box-shadow: 0 0 0 2px var(--divider) inset; - } - - &.rounded { - border-radius: 999px; - - > .preview { - border-radius: 999px; - } - } - - &.char { - line-height: 42px; - } - } - } - } - } -} -</style> diff --git a/src/client/pages/timeline.tutorial.vue b/src/client/pages/timeline.tutorial.vue deleted file mode 100644 index 620994c0da..0000000000 --- a/src/client/pages/timeline.tutorial.vue +++ /dev/null @@ -1,131 +0,0 @@ -<template> -<div class="_card tbkwesmv"> - <div class="_title"><i class="fas fa-info-circle"></i> {{ $ts._tutorial.title }}</div> - <div class="_content" v-if="tutorial === 0"> - <div>{{ $ts._tutorial.step1_1 }}</div> - <div>{{ $ts._tutorial.step1_2 }}</div> - <div>{{ $ts._tutorial.step1_3 }}</div> - </div> - <div class="_content" v-else-if="tutorial === 1"> - <div>{{ $ts._tutorial.step2_1 }}</div> - <div>{{ $ts._tutorial.step2_2 }}</div> - <MkA class="_link" to="/settings/profile">{{ $ts.editProfile }}</MkA> - </div> - <div class="_content" v-else-if="tutorial === 2"> - <div>{{ $ts._tutorial.step3_1 }}</div> - <div>{{ $ts._tutorial.step3_2 }}</div> - <div>{{ $ts._tutorial.step3_3 }}</div> - <small>{{ $ts._tutorial.step3_4 }}</small> - </div> - <div class="_content" v-else-if="tutorial === 3"> - <div>{{ $ts._tutorial.step4_1 }}</div> - <div>{{ $ts._tutorial.step4_2 }}</div> - </div> - <div class="_content" v-else-if="tutorial === 4"> - <div>{{ $ts._tutorial.step5_1 }}</div> - <I18n :src="$ts._tutorial.step5_2" tag="div"> - <template #featured> - <MkA class="_link" to="/featured">{{ $ts.featured }}</MkA> - </template> - <template #explore> - <MkA class="_link" to="/explore">{{ $ts.explore }}</MkA> - </template> - </I18n> - <div>{{ $ts._tutorial.step5_3 }}</div> - <small>{{ $ts._tutorial.step5_4 }}</small> - </div> - <div class="_content" v-else-if="tutorial === 5"> - <div>{{ $ts._tutorial.step6_1 }}</div> - <div>{{ $ts._tutorial.step6_2 }}</div> - <div>{{ $ts._tutorial.step6_3 }}</div> - </div> - <div class="_content" v-else-if="tutorial === 6"> - <div>{{ $ts._tutorial.step7_1 }}</div> - <I18n :src="$ts._tutorial.step7_2" tag="div"> - <template #help> - <MkA class="_link" to="/docs">{{ $ts.help }}</MkA> - </template> - </I18n> - <div>{{ $ts._tutorial.step7_3 }}</div> - </div> - - <div class="_footer navigation"> - <div class="step"> - <button class="arrow _button" @click="tutorial--" :disabled="tutorial === 0"> - <i class="fas fa-chevron-left"></i> - </button> - <span>{{ tutorial + 1 }} / 7</span> - <button class="arrow _button" @click="tutorial++" :disabled="tutorial === 6"> - <i class="fas fa-chevron-right"></i> - </button> - </div> - <MkButton class="ok" @click="tutorial = -1" primary v-if="tutorial === 6"><i class="fas fa-check"></i> {{ $ts.gotIt }}</MkButton> - <MkButton class="ok" @click="tutorial++" primary v-else><i class="fas fa-check"></i> {{ $ts.next }}</MkButton> - </div> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import MkButton from '@client/components/ui/button.vue'; - -export default defineComponent({ - components: { - MkButton, - }, - - data() { - return { - } - }, - - computed: { - tutorial: { - get() { return this.$store.reactiveState.tutorial.value || 0; }, - set(value) { this.$store.set('tutorial', value); } - }, - }, -}); -</script> - -<style lang="scss" scoped> -.tbkwesmv { - > ._content { - > small { - opacity: 0.7; - } - } - - > .navigation { - display: flex; - flex-direction: row; - align-items: baseline; - - > .step { - > .arrow { - padding: 4px; - - &:disabled { - opacity: 0.5; - } - - &:first-child { - padding-right: 8px; - } - - &:last-child { - padding-left: 8px; - } - } - - > span { - margin: 0 4px; - } - } - - > .ok { - margin-left: auto; - } - } -} -</style> diff --git a/src/client/pages/timeline.vue b/src/client/pages/timeline.vue deleted file mode 100644 index 7b17d585f8..0000000000 --- a/src/client/pages/timeline.vue +++ /dev/null @@ -1,225 +0,0 @@ -<template> -<div class="cmuxhskf" v-size="{ min: [800] }" v-hotkey.global="keymap"> - <XTutorial v-if="$store.reactiveState.tutorial.value != -1" class="tutorial _block"/> - <XPostForm v-if="$store.reactiveState.showFixedPostForm.value" class="post-form _block" fixed/> - - <div class="new" v-if="queue > 0"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div> - <div class="tl _block"> - <XTimeline ref="tl" class="tl" - :key="src" - :src="src" - :sound="true" - @before="before()" - @after="after()" - @queue="queueUpdated" - /> - </div> -</div> -</template> - -<script lang="ts"> -import { defineComponent, defineAsyncComponent, computed } from 'vue'; -import Progress from '@client/scripts/loading'; -import XTimeline from '@client/components/timeline.vue'; -import XPostForm from '@client/components/post-form.vue'; -import { scroll } from '@client/scripts/scroll'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - name: 'timeline', - - components: { - XTimeline, - XTutorial: defineAsyncComponent(() => import('./timeline.tutorial.vue')), - XPostForm, - }, - - data() { - return { - src: 'home', - queue: 0, - [symbols.PAGE_INFO]: computed(() => ({ - title: this.$ts.timeline, - icon: this.src === 'local' ? 'fas fa-comments' : this.src === 'social' ? 'fas fa-share-alt' : this.src === 'global' ? 'fas fa-globe' : 'fas fa-home', - bg: 'var(--bg)', - actions: [{ - icon: 'fas fa-list-ul', - text: this.$ts.lists, - handler: this.chooseList - }, { - icon: 'fas fa-satellite', - text: this.$ts.antennas, - handler: this.chooseAntenna - }, { - icon: 'fas fa-satellite-dish', - text: this.$ts.channel, - handler: this.chooseChannel - }, { - icon: 'fas fa-calendar-alt', - text: this.$ts.jumpToSpecifiedDate, - handler: this.timetravel - }], - tabs: [{ - active: this.src === 'home', - title: this.$ts._timelines.home, - icon: 'fas fa-home', - iconOnly: true, - onClick: () => { this.src = 'home'; this.saveSrc(); }, - }, { - active: this.src === 'local', - title: this.$ts._timelines.local, - icon: 'fas fa-comments', - iconOnly: true, - onClick: () => { this.src = 'local'; this.saveSrc(); }, - }, { - active: this.src === 'social', - title: this.$ts._timelines.social, - icon: 'fas fa-share-alt', - iconOnly: true, - onClick: () => { this.src = 'social'; this.saveSrc(); }, - }, { - active: this.src === 'global', - title: this.$ts._timelines.global, - icon: 'fas fa-globe', - iconOnly: true, - onClick: () => { this.src = 'global'; this.saveSrc(); }, - }], - })), - }; - }, - - computed: { - keymap(): any { - return { - 't': this.focus - }; - }, - - isLocalTimelineAvailable(): boolean { - return !this.$instance.disableLocalTimeline || this.$i.isModerator || this.$i.isAdmin; - }, - - isGlobalTimelineAvailable(): boolean { - return !this.$instance.disableGlobalTimeline || this.$i.isModerator || this.$i.isAdmin; - }, - }, - - watch: { - src() { - this.showNav = false; - }, - }, - - created() { - this.src = this.$store.state.tl.src; - }, - - methods: { - before() { - Progress.start(); - }, - - after() { - Progress.done(); - }, - - queueUpdated(q) { - this.queue = q; - }, - - top() { - scroll(this.$el, { top: 0 }); - }, - - async chooseList(ev) { - const lists = await os.api('users/lists/list'); - const items = lists.map(list => ({ - type: 'link', - text: list.name, - to: `/timeline/list/${list.id}` - })); - os.popupMenu(items, ev.currentTarget || ev.target); - }, - - async chooseAntenna(ev) { - const antennas = await os.api('antennas/list'); - const items = antennas.map(antenna => ({ - type: 'link', - text: antenna.name, - indicate: antenna.hasUnreadNote, - to: `/timeline/antenna/${antenna.id}` - })); - os.popupMenu(items, ev.currentTarget || ev.target); - }, - - async chooseChannel(ev) { - const channels = await os.api('channels/followed'); - const items = channels.map(channel => ({ - type: 'link', - text: channel.name, - indicate: channel.hasUnreadNote, - to: `/channels/${channel.id}` - })); - os.popupMenu(items, ev.currentTarget || ev.target); - }, - - saveSrc() { - this.$store.set('tl', { - src: this.src, - }); - }, - - async timetravel() { - const { canceled, result: date } = await os.dialog({ - title: this.$ts.date, - input: { - type: 'date' - } - }); - if (canceled) return; - - this.$refs.tl.timetravel(new Date(date)); - }, - - focus() { - (this.$refs.tl as any).focus(); - } - } -}); -</script> - -<style lang="scss" scoped> -.cmuxhskf { - padding: var(--margin); - - > .new { - position: sticky; - top: calc(var(--stickyTop, 0px) + 16px); - z-index: 1000; - width: 100%; - - > button { - display: block; - margin: var(--margin) auto 0 auto; - padding: 8px 16px; - border-radius: 32px; - } - } - - > .post-form { - border-radius: var(--radius); - } - - > .tl { - background: var(--bg); - border-radius: var(--radius); - overflow: clip; - } - - &.min-width_800px { - max-width: 800px; - margin: 0 auto; - } -} -</style> diff --git a/src/client/pages/user-ap-info.vue b/src/client/pages/user-ap-info.vue deleted file mode 100644 index cbdff874ed..0000000000 --- a/src/client/pages/user-ap-info.vue +++ /dev/null @@ -1,124 +0,0 @@ -<template> -<FormBase> - <FormSuspense :p="apPromiseFactory" v-slot="{ result: ap }"> - <FormGroup> - <template #label>ActivityPub</template> - <FormKeyValueView> - <template #key>Type</template> - <template #value><span class="_monospace">{{ ap.type }}</span></template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>URI</template> - <template #value><span class="_monospace">{{ ap.id }}</span></template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>URL</template> - <template #value><span class="_monospace">{{ ap.url }}</span></template> - </FormKeyValueView> - <FormGroup> - <FormKeyValueView> - <template #key>Inbox</template> - <template #value><span class="_monospace">{{ ap.inbox }}</span></template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>Shared Inbox</template> - <template #value><span class="_monospace">{{ ap.sharedInbox || ap.endpoints.sharedInbox }}</span></template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>Outbox</template> - <template #value><span class="_monospace">{{ ap.outbox }}</span></template> - </FormKeyValueView> - </FormGroup> - <FormTextarea readonly tall code pre :value="ap.publicKey.publicKeyPem"> - <span>Public Key</span> - </FormTextarea> - <FormKeyValueView> - <template #key>Discoverable</template> - <template #value>{{ ap.discoverable ? $ts.yes : $ts.no }}</template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>ManuallyApprovesFollowers</template> - <template #value>{{ ap.manuallyApprovesFollowers ? $ts.yes : $ts.no }}</template> - </FormKeyValueView> - <FormObjectView tall :value="ap"> - <span>Raw</span> - </FormObjectView> - <FormGroup> - <FormLink :to="`https://${user.host}/.well-known/webfinger?resource=acct:${user.username}`" external>WebFinger</FormLink> - </FormGroup> - <FormLink v-if="user.host" :to="`/instance-info/${user.host}`">{{ $ts.instanceInfo }}<template #suffix>{{ user.host }}</template></FormLink> - <FormKeyValueView v-else> - <template #key>{{ $ts.instanceInfo }}</template> - <template #value>(Local user)</template> - </FormKeyValueView> - </FormGroup> - </FormSuspense> -</FormBase> -</template> - -<script lang="ts"> -import { defineAsyncComponent, defineComponent } from 'vue'; -import FormObjectView from '@client/components/debobigego/object-view.vue'; -import FormTextarea from '@client/components/debobigego/textarea.vue'; -import FormLink from '@client/components/debobigego/link.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import FormKeyValueView from '@client/components/debobigego/key-value-view.vue'; -import FormSuspense from '@client/components/debobigego/suspense.vue'; -import * as os from '@client/os'; -import number from '@client/filters/number'; -import bytes from '@client/filters/bytes'; -import * as symbols from '@client/symbols'; -import { url } from '@client/config'; - -export default defineComponent({ - components: { - FormBase, - FormTextarea, - FormObjectView, - FormButton, - FormLink, - FormGroup, - FormKeyValueView, - FormSuspense, - }, - - props: { - userId: { - type: String, - required: true - } - }, - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.userInfo, - icon: 'fas fa-info-circle' - }, - user: null, - apPromiseFactory: null, - } - }, - - mounted() { - this.fetch(); - }, - - methods: { - number, - bytes, - - async fetch() { - this.user = await os.api('users/show', { - userId: this.userId - }); - - this.apPromiseFactory = () => os.api('ap/get', { - uri: this.user.uri || `${url}/users/${this.user.id}` - }); - } - } -}); -</script> diff --git a/src/client/pages/user-info.vue b/src/client/pages/user-info.vue deleted file mode 100644 index bf67fc853a..0000000000 --- a/src/client/pages/user-info.vue +++ /dev/null @@ -1,245 +0,0 @@ -<template> -<FormBase> - <FormSuspense :p="init"> - <div class="_debobigegoItem aeakzknw"> - <MkAvatar class="avatar" :user="user" :show-indicator="true"/> - </div> - - <FormLink :to="userPage(user)">Profile</FormLink> - - <FormGroup> - <FormKeyValueView> - <template #key>Acct</template> - <template #value><span class="_monospace">{{ acct(user) }}</span></template> - </FormKeyValueView> - - <FormKeyValueView> - <template #key>ID</template> - <template #value><span class="_monospace">{{ user.id }}</span></template> - </FormKeyValueView> - </FormGroup> - - <FormGroup v-if="iAmModerator"> - <FormSwitch v-if="user.host == null && $i.isAdmin && (moderator || !user.isAdmin)" @update:modelValue="toggleModerator" v-model="moderator">{{ $ts.moderator }}</FormSwitch> - <FormSwitch @update:modelValue="toggleSilence" v-model="silenced">{{ $ts.silence }}</FormSwitch> - <FormSwitch @update:modelValue="toggleSuspend" v-model="suspended">{{ $ts.suspend }}</FormSwitch> - </FormGroup> - - <FormGroup> - <FormButton v-if="user.host != null" @click="updateRemoteUser"><i class="fas fa-sync"></i> {{ $ts.updateRemoteUser }}</FormButton> - <FormButton v-if="user.host == null && iAmModerator" @click="resetPassword"><i class="fas fa-key"></i> {{ $ts.resetPassword }}</FormButton> - </FormGroup> - - <FormGroup> - <FormLink :to="`/user-ap-info/${user.id}`">ActivityPub</FormLink> - - <FormLink v-if="user.host" :to="`/instance-info/${user.host}`">{{ $ts.instanceInfo }}<template #suffix>{{ user.host }}</template></FormLink> - <FormKeyValueView v-else> - <template #key>{{ $ts.instanceInfo }}</template> - <template #value>(Local user)</template> - </FormKeyValueView> - </FormGroup> - - <FormGroup> - <FormKeyValueView> - <template #key>{{ $ts.updatedAt }}</template> - <template #value><MkTime v-if="user.lastFetchedAt" mode="detail" :time="user.lastFetchedAt"/><span v-else>N/A</span></template> - </FormKeyValueView> - </FormGroup> - - <FormObjectView tall :value="user"> - <span>Raw</span> - </FormObjectView> - </FormSuspense> -</FormBase> -</template> - -<script lang="ts"> -import { computed, defineAsyncComponent, defineComponent } from 'vue'; -import FormObjectView from '@client/components/debobigego/object-view.vue'; -import FormTextarea from '@client/components/debobigego/textarea.vue'; -import FormSwitch from '@client/components/debobigego/switch.vue'; -import FormLink from '@client/components/debobigego/link.vue'; -import FormBase from '@client/components/debobigego/base.vue'; -import FormGroup from '@client/components/debobigego/group.vue'; -import FormButton from '@client/components/debobigego/button.vue'; -import FormKeyValueView from '@client/components/debobigego/key-value-view.vue'; -import FormSuspense from '@client/components/debobigego/suspense.vue'; -import * as os from '@client/os'; -import number from '@client/filters/number'; -import bytes from '@client/filters/bytes'; -import * as symbols from '@client/symbols'; -import { url } from '@client/config'; -import { userPage, acct } from '@client/filters/user'; - -export default defineComponent({ - components: { - FormBase, - FormTextarea, - FormSwitch, - FormObjectView, - FormButton, - FormLink, - FormGroup, - FormKeyValueView, - FormSuspense, - }, - - props: { - userId: { - type: String, - required: true - } - }, - - data() { - return { - [symbols.PAGE_INFO]: computed(() => ({ - title: this.user ? acct(this.user) : this.$ts.userInfo, - icon: 'fas fa-info-circle', - actions: this.user ? [this.user.url ? { - text: this.user.url, - icon: 'fas fa-external-link-alt', - handler: () => { - window.open(this.user.url, '_blank'); - } - } : undefined].filter(x => x !== undefined) : [], - })), - init: null, - user: null, - info: null, - moderator: false, - silenced: false, - suspended: false, - } - }, - - computed: { - iAmModerator(): boolean { - return this.$i && (this.$i.isAdmin || this.$i.isModerator); - } - }, - - watch: { - userId: { - handler() { - this.init = this.createFetcher(); - }, - immediate: true - } - }, - - methods: { - number, - bytes, - userPage, - acct, - - createFetcher() { - if (this.iAmModerator) { - return () => Promise.all([os.api('users/show', { - userId: this.userId - }), os.api('admin/show-user', { - userId: this.userId - })]).then(([user, info]) => { - this.user = user; - this.info = info; - this.moderator = this.info.isModerator; - this.silenced = this.info.isSilenced; - this.suspended = this.info.isSuspended; - }); - } else { - return () => os.api('users/show', { - userId: this.userId - }).then((user) => { - this.user = user; - }); - } - }, - - refreshUser() { - this.init = this.createFetcher(); - }, - - async updateRemoteUser() { - await os.apiWithDialog('federation/update-remote-user', { userId: this.user.id }); - this.refreshUser(); - }, - - async resetPassword() { - const { password } = await os.api('admin/reset-password', { - userId: this.user.id, - }); - - os.dialog({ - type: 'success', - text: this.$t('newPasswordIs', { password }) - }); - }, - - async toggleSilence(v) { - const confirm = await os.dialog({ - type: 'warning', - showCancelButton: true, - text: v ? this.$ts.silenceConfirm : this.$ts.unsilenceConfirm, - }); - if (confirm.canceled) { - this.silenced = !v; - } else { - await os.api(v ? 'admin/silence-user' : 'admin/unsilence-user', { userId: this.user.id }); - await this.refreshUser(); - } - }, - - async toggleSuspend(v) { - const confirm = await os.dialog({ - type: 'warning', - showCancelButton: true, - text: v ? this.$ts.suspendConfirm : this.$ts.unsuspendConfirm, - }); - if (confirm.canceled) { - this.suspended = !v; - } else { - await os.api(v ? 'admin/suspend-user' : 'admin/unsuspend-user', { userId: this.user.id }); - await this.refreshUser(); - } - }, - - async toggleModerator(v) { - await os.api(v ? 'admin/moderators/add' : 'admin/moderators/remove', { userId: this.user.id }); - await this.refreshUser(); - }, - - async deleteAllFiles() { - const confirm = await os.dialog({ - type: 'warning', - showCancelButton: true, - text: this.$ts.deleteAllFilesConfirm, - }); - if (confirm.canceled) return; - const process = async () => { - await os.api('admin/delete-all-files-of-a-user', { userId: this.user.id }); - os.success(); - }; - await process().catch(e => { - os.dialog({ - type: 'error', - text: e.toString() - }); - }); - await this.refreshUser(); - }, - } -}); -</script> - -<style lang="scss" scoped> -.aeakzknw { - > .avatar { - display: block; - margin: 0 auto; - width: 64px; - height: 64px; - } -} -</style> diff --git a/src/client/pages/user-list-timeline.vue b/src/client/pages/user-list-timeline.vue deleted file mode 100644 index b5e37d4843..0000000000 --- a/src/client/pages/user-list-timeline.vue +++ /dev/null @@ -1,147 +0,0 @@ -<template> -<div class="eqqrhokj" v-hotkey.global="keymap" v-size="{ min: [800] }"> - <div class="new" v-if="queue > 0"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div> - <div class="tl _block"> - <XTimeline ref="tl" class="tl" - :key="listId" - src="list" - :list="listId" - :sound="true" - @before="before()" - @after="after()" - @queue="queueUpdated" - /> - </div> -</div> -</template> - -<script lang="ts"> -import { defineComponent, defineAsyncComponent, computed } from 'vue'; -import Progress from '@client/scripts/loading'; -import XTimeline from '@client/components/timeline.vue'; -import { scroll } from '@client/scripts/scroll'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - XTimeline, - }, - - props: { - listId: { - type: String, - required: true - } - }, - - data() { - return { - list: null, - queue: 0, - [symbols.PAGE_INFO]: computed(() => this.list ? { - title: this.list.name, - icon: 'fas fa-list-ul', - bg: 'var(--bg)', - actions: [{ - icon: 'fas fa-calendar-alt', - text: this.$ts.jumpToSpecifiedDate, - handler: this.timetravel - }, { - icon: 'fas fa-cog', - text: this.$ts.settings, - handler: this.settings - }], - } : null), - }; - }, - - computed: { - keymap(): any { - return { - 't': this.focus - }; - }, - }, - - watch: { - listId: { - async handler() { - this.list = await os.api('users/lists/show', { - listId: this.listId - }); - }, - immediate: true - } - }, - - methods: { - before() { - Progress.start(); - }, - - after() { - Progress.done(); - }, - - queueUpdated(q) { - this.queue = q; - }, - - top() { - scroll(this.$el, { top: 0 }); - }, - - settings() { - this.$router.push(`/my/lists/${this.listId}`); - }, - - async timetravel() { - const { canceled, result: date } = await os.dialog({ - title: this.$ts.date, - input: { - type: 'date' - } - }); - if (canceled) return; - - this.$refs.tl.timetravel(new Date(date)); - }, - - focus() { - (this.$refs.tl as any).focus(); - } - } -}); -</script> - -<style lang="scss" scoped> -.eqqrhokj { - padding: var(--margin); - - > .new { - position: sticky; - top: calc(var(--stickyTop, 0px) + 16px); - z-index: 1000; - width: 100%; - - > button { - display: block; - margin: var(--margin) auto 0 auto; - padding: 8px 16px; - border-radius: 32px; - } - } - - > .tl { - background: var(--bg); - border-radius: var(--radius); - overflow: clip; - } - - &.min-width_800px { - max-width: 800px; - margin: 0 auto; - } -} -</style> diff --git a/src/client/pages/user/clips.vue b/src/client/pages/user/clips.vue deleted file mode 100644 index 53ee554383..0000000000 --- a/src/client/pages/user/clips.vue +++ /dev/null @@ -1,50 +0,0 @@ -<template> -<div> - <MkPagination :pagination="pagination" #default="{items}" ref="list"> - <MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _gap"> - <b>{{ item.name }}</b> - <div v-if="item.description" class="description">{{ item.description }}</div> - </MkA> - </MkPagination> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import MkPagination from '@client/components/ui/pagination.vue'; - -export default defineComponent({ - components: { - MkPagination, - }, - - props: { - user: { - type: Object, - required: true - }, - }, - - data() { - return { - pagination: { - endpoint: 'users/clips', - limit: 20, - params: { - userId: this.user.id, - } - }, - }; - }, - - watch: { - user() { - this.$refs.list.reload(); - } - }, -}); -</script> - -<style lang="scss" scoped> - -</style> diff --git a/src/client/pages/user/follow-list.vue b/src/client/pages/user/follow-list.vue deleted file mode 100644 index 1f5ab5993c..0000000000 --- a/src/client/pages/user/follow-list.vue +++ /dev/null @@ -1,65 +0,0 @@ -<template> -<div> - <MkPagination :pagination="pagination" #default="{items}" class="mk-following-or-followers" ref="list"> - <div class="users _isolated"> - <MkUserInfo class="user" v-for="user in items.map(x => type === 'following' ? x.followee : x.follower)" :user="user" :key="user.id"/> - </div> - </MkPagination> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import MkUserInfo from '@client/components/user-info.vue'; -import MkPagination from '@client/components/ui/pagination.vue'; - -export default defineComponent({ - components: { - MkPagination, - MkUserInfo, - }, - - props: { - user: { - type: Object, - required: true - }, - type: { - type: String, - required: true - }, - }, - - data() { - return { - pagination: { - endpoint: () => this.type === 'following' ? 'users/following' : 'users/followers', - limit: 20, - params: { - userId: this.user.id, - } - }, - }; - }, - - watch: { - type() { - this.$refs.list.reload(); - }, - - user() { - this.$refs.list.reload(); - } - } -}); -</script> - -<style lang="scss" scoped> -.mk-following-or-followers { - > .users { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); - grid-gap: var(--margin); - } -} -</style> diff --git a/src/client/pages/user/gallery.vue b/src/client/pages/user/gallery.vue deleted file mode 100644 index c21b3e6428..0000000000 --- a/src/client/pages/user/gallery.vue +++ /dev/null @@ -1,56 +0,0 @@ -<template> -<div> - <MkPagination :pagination="pagination" #default="{items}"> - <div class="jrnovfpt"> - <MkGalleryPostPreview v-for="post in items" :post="post" :key="post.id" class="post"/> - </div> - </MkPagination> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import MkGalleryPostPreview from '@client/components/gallery-post-preview.vue'; -import MkPagination from '@client/components/ui/pagination.vue'; - -export default defineComponent({ - components: { - MkPagination, - MkGalleryPostPreview, - }, - - props: { - user: { - type: Object, - required: true - }, - }, - - data() { - return { - pagination: { - endpoint: 'users/gallery/posts', - limit: 6, - params: () => ({ - userId: this.user.id - }) - }, - }; - }, - - watch: { - user() { - this.$refs.list.reload(); - } - } -}); -</script> - -<style lang="scss" scoped> -.jrnovfpt { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); - grid-gap: 12px; - margin: var(--margin); -} -</style> diff --git a/src/client/pages/user/index.activity.vue b/src/client/pages/user/index.activity.vue deleted file mode 100644 index be85c252e8..0000000000 --- a/src/client/pages/user/index.activity.vue +++ /dev/null @@ -1,34 +0,0 @@ -<template> -<MkContainer> - <template #header><i class="fas fa-chart-bar" style="margin-right: 0.5em;"></i>{{ $ts.activity }}</template> - - <div style="padding: 8px;"> - <MkChart src="per-user-notes" :args="{ user, withoutAll: true }" span="day" :limit="limit" :stacked="true" :detailed="false" :aspect-ratio="6"/> - </div> -</MkContainer> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import * as os from '@client/os'; -import MkContainer from '@client/components/ui/container.vue'; -import MkChart from '@client/components/chart.vue'; - -export default defineComponent({ - components: { - MkContainer, - MkChart, - }, - props: { - user: { - type: Object, - required: true - }, - limit: { - type: Number, - required: false, - default: 40 - } - }, -}); -</script> diff --git a/src/client/pages/user/index.photos.vue b/src/client/pages/user/index.photos.vue deleted file mode 100644 index 5029c3feec..0000000000 --- a/src/client/pages/user/index.photos.vue +++ /dev/null @@ -1,107 +0,0 @@ -<template> -<MkContainer :max-height="300" :foldable="true"> - <template #header><i class="fas fa-image" style="margin-right: 0.5em;"></i>{{ $ts.images }}</template> - <div class="ujigsodd"> - <MkLoading v-if="fetching"/> - <div class="stream" v-if="!fetching && images.length > 0"> - <MkA v-for="image in images" - class="img" - :to="notePage(image.note)" - :key="image.id" - > - <ImgWithBlurhash :hash="image.blurhash" :src="thumbnail(image.file)" :alt="image.name" :title="image.name"/> - </MkA> - </div> - <p class="empty" v-if="!fetching && images.length == 0">{{ $ts.nothing }}</p> - </div> -</MkContainer> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import { getStaticImageUrl } from '@client/scripts/get-static-image-url'; -import notePage from '@client/filters/note'; -import * as os from '@client/os'; -import MkContainer from '@client/components/ui/container.vue'; -import ImgWithBlurhash from '@client/components/img-with-blurhash.vue'; - -export default defineComponent({ - components: { - MkContainer, - ImgWithBlurhash, - }, - props: { - user: { - type: Object, - required: true - }, - }, - data() { - return { - fetching: true, - images: [], - }; - }, - mounted() { - const image = [ - 'image/jpeg', - 'image/png', - 'image/gif', - 'image/apng', - 'image/vnd.mozilla.apng', - ]; - os.api('users/notes', { - userId: this.user.id, - fileType: image, - excludeNsfw: this.$store.state.nsfw !== 'ignore', - limit: 10, - }).then(notes => { - for (const note of notes) { - for (const file of note.files) { - this.images.push({ - note, - file - }); - } - } - this.fetching = false; - }); - }, - methods: { - thumbnail(image: any): string { - return this.$store.state.disableShowingAnimatedImages - ? getStaticImageUrl(image.thumbnailUrl) - : image.thumbnailUrl; - }, - notePage - }, -}); -</script> - -<style lang="scss" scoped> -.ujigsodd { - padding: 8px; - - > .stream { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); - grid-gap: 6px; - - > .img { - height: 128px; - border-radius: 6px; - overflow: clip; - } - } - - > .empty { - margin: 0; - padding: 16px; - text-align: center; - - > i { - margin-right: 4px; - } - } -} -</style> diff --git a/src/client/pages/user/index.timeline.vue b/src/client/pages/user/index.timeline.vue deleted file mode 100644 index c3444f26f6..0000000000 --- a/src/client/pages/user/index.timeline.vue +++ /dev/null @@ -1,68 +0,0 @@ -<template> -<div class="yrzkoczt" v-sticky-container> - <MkTab v-model="with_" class="tab"> - <option :value="null">{{ $ts.notes }}</option> - <option value="replies">{{ $ts.notesAndReplies }}</option> - <option value="files">{{ $ts.withFiles }}</option> - </MkTab> - <XNotes ref="timeline" :no-gap="true" :pagination="pagination" @before="$emit('before')" @after="e => $emit('after', e)"/> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import XNotes from '@client/components/notes.vue'; -import MkTab from '@client/components/tab.vue'; -import * as os from '@client/os'; - -export default defineComponent({ - components: { - XNotes, - MkTab, - }, - - props: { - user: { - type: Object, - required: true, - }, - }, - - watch: { - user() { - this.$refs.timeline.reload(); - }, - - with_() { - this.$refs.timeline.reload(); - }, - }, - - data() { - return { - date: null, - with_: null, - pagination: { - endpoint: 'users/notes', - limit: 10, - params: init => ({ - userId: this.user.id, - includeReplies: this.with_ === 'replies', - withFiles: this.with_ === 'files', - untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined), - }) - } - }; - }, -}); -</script> - -<style lang="scss" scoped> -.yrzkoczt { - > .tab { - margin: calc(var(--margin) / 2) 0; - padding: calc(var(--margin) / 2) 0; - background: var(--bg); - } -} -</style> diff --git a/src/client/pages/user/index.vue b/src/client/pages/user/index.vue deleted file mode 100644 index 04585f3fd0..0000000000 --- a/src/client/pages/user/index.vue +++ /dev/null @@ -1,829 +0,0 @@ -<template> -<div> -<transition name="fade" mode="out-in"> - <div class="ftskorzw wide" v-if="user && narrow === false"> - <MkRemoteCaution v-if="user.host != null" :href="user.url"/> - - <div class="banner-container" :style="style"> - <div class="banner" ref="banner" :style="style"></div> - </div> - <div class="contents"> - <div class="side _forceContainerFull_"> - <MkAvatar class="avatar" :user="user" :disable-preview="true" :show-indicator="true"/> - <div class="name"> - <MkUserName :user="user" :nowrap="false" class="name"/> - <MkAcct :user="user" :detail="true" class="acct"/> - </div> - <div class="followed" v-if="$i && $i.id != user.id && user.isFollowed"><span>{{ $ts.followsYou }}</span></div> - <div class="status"> - <MkA :to="userPage(user)" :class="{ active: page === 'index' }"> - <b>{{ number(user.notesCount) }}</b> - <span>{{ $ts.notes }}</span> - </MkA> - <MkA :to="userPage(user, 'following')" :class="{ active: page === 'following' }"> - <b>{{ number(user.followingCount) }}</b> - <span>{{ $ts.following }}</span> - </MkA> - <MkA :to="userPage(user, 'followers')" :class="{ active: page === 'followers' }"> - <b>{{ number(user.followersCount) }}</b> - <span>{{ $ts.followers }}</span> - </MkA> - </div> - <div class="description"> - <Mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$i" :custom-emojis="user.emojis"/> - <p v-else class="empty">{{ $ts.noAccountDescription }}</p> - </div> - <div class="fields system"> - <dl class="field" v-if="user.location"> - <dt class="name"><i class="fas fa-map-marker fa-fw"></i> {{ $ts.location }}</dt> - <dd class="value">{{ user.location }}</dd> - </dl> - <dl class="field" v-if="user.birthday"> - <dt class="name"><i class="fas fa-birthday-cake fa-fw"></i> {{ $ts.birthday }}</dt> - <dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ $t('yearsOld', { age }) }})</dd> - </dl> - <dl class="field"> - <dt class="name"><i class="fas fa-calendar-alt fa-fw"></i> {{ $ts.registeredDate }}</dt> - <dd class="value">{{ new Date(user.createdAt).toLocaleString() }} (<MkTime :time="user.createdAt"/>)</dd> - </dl> - </div> - <div class="fields" v-if="user.fields.length > 0"> - <dl class="field" v-for="(field, i) in user.fields" :key="i"> - <dt class="name"> - <Mfm :text="field.name" :plain="true" :custom-emojis="user.emojis" :colored="false"/> - </dt> - <dd class="value"> - <Mfm :text="field.value" :author="user" :i="$i" :custom-emojis="user.emojis" :colored="false"/> - </dd> - </dl> - </div> - <XActivity :user="user" :key="user.id" class="_gap"/> - <XPhotos :user="user" :key="user.id" class="_gap"/> - </div> - <div class="main"> - <div class="actions"> - <button @click="menu" class="menu _button"><i class="fas fa-ellipsis-h"></i></button> - <MkFollowButton v-if="!$i || $i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" large class="koudoku"/> - </div> - <template v-if="page === 'index'"> - <div v-if="user.pinnedNotes.length > 0" class="_gap"> - <XNote v-for="note in user.pinnedNotes" class="note _gap" :note="note" @update:note="pinnedNoteUpdated(note, $event)" :key="note.id" :pinned="true"/> - </div> - <div class="_gap"> - <XUserTimeline :user="user"/> - </div> - </template> - <XFollowList v-else-if="page === 'following'" type="following" :user="user" class="_gap"/> - <XFollowList v-else-if="page === 'followers'" type="followers" :user="user" class="_gap"/> - <XClips v-else-if="page === 'clips'" :user="user" class="_gap"/> - <XPages v-else-if="page === 'pages'" :user="user" class="_gap"/> - </div> - </div> - </div> - <MkSpacer v-else-if="user && narrow === true" :content-max="800"> - <div class="ftskorzw narrow" v-size="{ max: [500] }"> - <!-- TODO --> - <!-- <div class="punished" v-if="user.isSuspended"><i class="fas fa-exclamation-triangle" style="margin-right: 8px;"></i> {{ $ts.userSuspended }}</div> --> - <!-- <div class="punished" v-if="user.isSilenced"><i class="fas fa-exclamation-triangle" style="margin-right: 8px;"></i> {{ $ts.userSilenced }}</div> --> - - <div class="profile"> - <MkRemoteCaution v-if="user.host != null" :href="user.url" class="warn"/> - - <div class="_block main" :key="user.id"> - <div class="banner-container" :style="style"> - <div class="banner" ref="banner" :style="style"></div> - <div class="fade"></div> - <div class="title"> - <MkUserName class="name" :user="user" :nowrap="true"/> - <div class="bottom"> - <span class="username"><MkAcct :user="user" :detail="true" /></span> - <span v-if="user.isAdmin" :title="$ts.isAdmin" style="color: var(--badge);"><i class="fas fa-bookmark"></i></span> - <span v-if="!user.isAdmin && user.isModerator" :title="$ts.isModerator" style="color: var(--badge);"><i class="far fa-bookmark"></i></span> - <span v-if="user.isLocked" :title="$ts.isLocked"><i class="fas fa-lock"></i></span> - <span v-if="user.isBot" :title="$ts.isBot"><i class="fas fa-robot"></i></span> - </div> - </div> - <span class="followed" v-if="$i && $i.id != user.id && user.isFollowed">{{ $ts.followsYou }}</span> - <div class="actions" v-if="$i"> - <button @click="menu" class="menu _button"><i class="fas fa-ellipsis-h"></i></button> - <MkFollowButton v-if="$i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/> - </div> - </div> - <MkAvatar class="avatar" :user="user" :disable-preview="true" :show-indicator="true"/> - <div class="title"> - <MkUserName :user="user" :nowrap="false" class="name"/> - <div class="bottom"> - <span class="username"><MkAcct :user="user" :detail="true" /></span> - <span v-if="user.isAdmin" :title="$ts.isAdmin" style="color: var(--badge);"><i class="fas fa-bookmark"></i></span> - <span v-if="!user.isAdmin && user.isModerator" :title="$ts.isModerator" style="color: var(--badge);"><i class="far fa-bookmark"></i></span> - <span v-if="user.isLocked" :title="$ts.isLocked"><i class="fas fa-lock"></i></span> - <span v-if="user.isBot" :title="$ts.isBot"><i class="fas fa-robot"></i></span> - </div> - </div> - <div class="description"> - <Mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$i" :custom-emojis="user.emojis"/> - <p v-else class="empty">{{ $ts.noAccountDescription }}</p> - </div> - <div class="fields system"> - <dl class="field" v-if="user.location"> - <dt class="name"><i class="fas fa-map-marker fa-fw"></i> {{ $ts.location }}</dt> - <dd class="value">{{ user.location }}</dd> - </dl> - <dl class="field" v-if="user.birthday"> - <dt class="name"><i class="fas fa-birthday-cake fa-fw"></i> {{ $ts.birthday }}</dt> - <dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ $t('yearsOld', { age }) }})</dd> - </dl> - <dl class="field"> - <dt class="name"><i class="fas fa-calendar-alt fa-fw"></i> {{ $ts.registeredDate }}</dt> - <dd class="value">{{ new Date(user.createdAt).toLocaleString() }} (<MkTime :time="user.createdAt"/>)</dd> - </dl> - </div> - <div class="fields" v-if="user.fields.length > 0"> - <dl class="field" v-for="(field, i) in user.fields" :key="i"> - <dt class="name"> - <Mfm :text="field.name" :plain="true" :custom-emojis="user.emojis" :colored="false"/> - </dt> - <dd class="value"> - <Mfm :text="field.value" :author="user" :i="$i" :custom-emojis="user.emojis" :colored="false"/> - </dd> - </dl> - </div> - <div class="status"> - <MkA :to="userPage(user)" :class="{ active: page === 'index' }" v-click-anime> - <b>{{ number(user.notesCount) }}</b> - <span>{{ $ts.notes }}</span> - </MkA> - <MkA :to="userPage(user, 'following')" :class="{ active: page === 'following' }" v-click-anime> - <b>{{ number(user.followingCount) }}</b> - <span>{{ $ts.following }}</span> - </MkA> - <MkA :to="userPage(user, 'followers')" :class="{ active: page === 'followers' }" v-click-anime> - <b>{{ number(user.followersCount) }}</b> - <span>{{ $ts.followers }}</span> - </MkA> - </div> - </div> - </div> - - <div class="contents"> - <template v-if="page === 'index'"> - <div> - <div v-if="user.pinnedNotes.length > 0" class="_gap"> - <XNote v-for="note in user.pinnedNotes" class="note _block" :note="note" @update:note="pinnedNoteUpdated(note, $event)" :key="note.id" :pinned="true"/> - </div> - <MkInfo v-else-if="$i && $i.id === user.id">{{ $ts.userPagePinTip }}</MkInfo> - <XPhotos :user="user" :key="user.id"/> - <XActivity :user="user" :key="user.id" style="margin-top: var(--margin);"/> - </div> - <div> - <XUserTimeline :user="user"/> - </div> - </template> - <XFollowList v-else-if="page === 'following'" type="following" :user="user" class="_content _gap"/> - <XFollowList v-else-if="page === 'followers'" type="followers" :user="user" class="_content _gap"/> - <XReactions v-else-if="page === 'reactions'" :user="user" class="_gap"/> - <XClips v-else-if="page === 'clips'" :user="user" class="_gap"/> - <XPages v-else-if="page === 'pages'" :user="user" class="_gap"/> - <XGallery v-else-if="page === 'gallery'" :user="user" class="_gap"/> - </div> - </div> - </MkSpacer> - <MkError v-else-if="error" @retry="fetch()"/> - <MkLoading v-else/> -</transition> -</div> -</template> - -<script lang="ts"> -import { defineComponent, defineAsyncComponent, computed } from 'vue'; -import * as age from 's-age'; -import XUserTimeline from './index.timeline.vue'; -import XNote from '@client/components/note.vue'; -import MkFollowButton from '@client/components/follow-button.vue'; -import MkContainer from '@client/components/ui/container.vue'; -import MkFolder from '@client/components/ui/folder.vue'; -import MkRemoteCaution from '@client/components/remote-caution.vue'; -import MkTab from '@client/components/tab.vue'; -import MkInfo from '@client/components/ui/info.vue'; -import Progress from '@client/scripts/loading'; -import { parseAcct } from '@/misc/acct'; -import { getScrollPosition } from '@client/scripts/scroll'; -import { getUserMenu } from '@client/scripts/get-user-menu'; -import number from '@client/filters/number'; -import { userPage, acct as getAcct } from '@client/filters/user'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - XUserTimeline, - XNote, - MkFollowButton, - MkContainer, - MkRemoteCaution, - MkFolder, - MkTab, - MkInfo, - XFollowList: defineAsyncComponent(() => import('./follow-list.vue')), - XReactions: defineAsyncComponent(() => import('./reactions.vue')), - XClips: defineAsyncComponent(() => import('./clips.vue')), - XPages: defineAsyncComponent(() => import('./pages.vue')), - XGallery: defineAsyncComponent(() => import('./gallery.vue')), - XPhotos: defineAsyncComponent(() => import('./index.photos.vue')), - XActivity: defineAsyncComponent(() => import('./index.activity.vue')), - }, - - props: { - acct: { - type: String, - required: true - }, - page: { - type: String, - required: false, - default: 'index' - } - }, - - data() { - return { - [symbols.PAGE_INFO]: computed(() => this.user ? { - icon: 'fas fa-user', - title: this.user.name ? `${this.user.name} (@${this.user.username})` : `@${this.user.username}`, - subtitle: `@${getAcct(this.user)}`, - userName: this.user, - avatar: this.user, - path: `/@${this.user.username}`, - share: { - title: this.user.name, - }, - bg: 'var(--bg)', - tabs: [{ - active: this.page === 'index', - title: this.$ts.overview, - icon: 'fas fa-home', - onClick: () => { this.$router.push('/@' + getAcct(this.user)); }, - }, ...(this.$i && (this.$i.id === this.user.id)) || this.user.publicReactions ? [{ - active: this.page === 'reactions', - title: this.$ts.reaction, - icon: 'fas fa-laugh', - onClick: () => { this.$router.push('/@' + getAcct(this.user) + '/reactions'); }, - }] : [], { - active: this.page === 'clips', - title: this.$ts.clips, - icon: 'fas fa-paperclip', - onClick: () => { this.$router.push('/@' + getAcct(this.user) + '/clips'); }, - }, { - active: this.page === 'pages', - title: this.$ts.pages, - icon: 'fas fa-file-alt', - onClick: () => { this.$router.push('/@' + getAcct(this.user) + '/pages'); }, - }, { - active: this.page === 'gallery', - title: this.$ts.gallery, - icon: 'fas fa-icons', - onClick: () => { this.$router.push('/@' + getAcct(this.user) + '/gallery'); }, - }], - } : null), - user: null, - error: null, - parallaxAnimationId: null, - narrow: null, - }; - }, - - computed: { - style(): any { - if (this.user.bannerUrl == null) return {}; - return { - backgroundImage: `url(${ this.user.bannerUrl })` - }; - }, - - age(): number { - return age(this.user.birthday); - } - }, - - watch: { - acct: 'fetch' - }, - - created() { - this.fetch(); - }, - - mounted() { - window.requestAnimationFrame(this.parallaxLoop); - this.narrow = true//this.$el.clientWidth < 1000; - }, - - beforeUnmount() { - window.cancelAnimationFrame(this.parallaxAnimationId); - }, - - methods: { - getAcct, - - fetch() { - if (this.acct == null) return; - this.user = null; - Progress.start(); - os.api('users/show', parseAcct(this.acct)).then(user => { - this.user = user; - }).catch(e => { - this.error = e; - }).finally(() => { - Progress.done(); - }); - }, - - menu(ev) { - os.popupMenu(getUserMenu(this.user), ev.currentTarget || ev.target); - }, - - parallaxLoop() { - this.parallaxAnimationId = window.requestAnimationFrame(this.parallaxLoop); - this.parallax(); - }, - - parallax() { - const banner = this.$refs.banner as any; - if (banner == null) return; - - const top = getScrollPosition(this.$el); - - if (top < 0) return; - - const z = 1.75; // ๅฅฅ่กใ(ๅฐใใใปใฉๅฅฅ) - const pos = -(top / z); - banner.style.backgroundPosition = `center calc(50% - ${pos}px)`; - }, - - pinnedNoteUpdated(oldValue, newValue) { - const i = this.user.pinnedNotes.findIndex(n => n === oldValue); - this.user.pinnedNotes[i] = newValue; - }, - - number, - - userPage - } -}); -</script> - -<style lang="scss" scoped> -.fade-enter-active, -.fade-leave-active { - transition: opacity 0.125s ease; -} -.fade-enter-from, -.fade-leave-to { - opacity: 0; -} - -.ftskorzw.wide { - - > .banner-container { - position: relative; - height: 300px; - overflow: hidden; - background-size: cover; - background-position: center; - - > .banner { - height: 100%; - background-color: #4c5e6d; - background-size: cover; - background-position: center; - box-shadow: 0 0 128px rgba(0, 0, 0, 0.5) inset; - will-change: background-position; - } - } - - > .contents { - display: flex; - padding: 16px; - - > .side { - width: 360px; - - > .avatar { - display: block; - width: 180px; - height: 180px; - margin: -130px auto 0 auto; - } - - > .name { - padding: 16px 0px 20px 0; - text-align: center; - - > .name { - display: block; - font-size: 1.75em; - font-weight: bold; - } - } - - > .followed { - text-align: center; - - > span { - display: inline-block; - font-size: 80%; - padding: 8px 12px; - margin-bottom: 20px; - border: solid 0.5px var(--divider); - border-radius: 999px; - } - } - - > .status { - display: flex; - padding: 20px 16px; - border-top: solid 0.5px var(--divider); - font-size: 90%; - - > a { - flex: 1; - text-align: center; - - &.active { - color: var(--accent); - } - - &:hover { - text-decoration: none; - } - - > b { - display: block; - line-height: 16px; - } - - > span { - font-size: 75%; - } - } - } - - > .description { - padding: 20px 16px; - border-top: solid 0.5px var(--divider); - font-size: 90%; - } - - > .fields { - padding: 20px 16px; - border-top: solid 0.5px var(--divider); - font-size: 90%; - - > .field { - display: flex; - padding: 0; - margin: 0; - align-items: center; - - &:not(:last-child) { - margin-bottom: 8px; - } - - > .name { - width: 30%; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - font-weight: bold; - } - - > .value { - width: 70%; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - margin: 0; - } - } - } - } - - > .main { - flex: 1; - margin-left: var(--margin); - min-width: 0; - - > .nav { - display: flex; - align-items: center; - margin-top: var(--margin); - //font-size: 120%; - font-weight: bold; - - > .link { - display: inline-block; - padding: 15px 24px 12px 24px; - text-align: center; - border-bottom: solid 3px transparent; - - &:hover { - text-decoration: none; - } - - &.active { - color: var(--accent); - border-bottom-color: var(--accent); - } - - &:not(.active):hover { - color: var(--fgHighlighted); - } - - > .icon { - margin-right: 6px; - } - } - - > .actions { - display: flex; - align-items: center; - margin-left: auto; - - > .menu { - padding: 12px 16px; - } - } - } - } - } -} - -.ftskorzw.narrow { - box-sizing: border-box; - overflow: clip; - background: var(--bg); - - > .punished { - font-size: 0.8em; - padding: 16px; - } - - > .profile { - - > .main { - position: relative; - overflow: hidden; - - > .banner-container { - position: relative; - height: 250px; - overflow: hidden; - background-size: cover; - background-position: center; - - > .banner { - height: 100%; - background-color: #4c5e6d; - background-size: cover; - background-position: center; - box-shadow: 0 0 128px rgba(0, 0, 0, 0.5) inset; - will-change: background-position; - } - - > .fade { - position: absolute; - bottom: 0; - left: 0; - width: 100%; - height: 78px; - background: linear-gradient(transparent, rgba(#000, 0.7)); - } - - > .followed { - position: absolute; - top: 12px; - left: 12px; - padding: 4px 8px; - color: #fff; - background: rgba(0, 0, 0, 0.7); - font-size: 0.7em; - border-radius: 6px; - } - - > .actions { - position: absolute; - top: 12px; - right: 12px; - -webkit-backdrop-filter: var(--blur, blur(8px)); - backdrop-filter: var(--blur, blur(8px)); - background: rgba(0, 0, 0, 0.2); - padding: 8px; - border-radius: 24px; - - > .menu { - vertical-align: bottom; - height: 31px; - width: 31px; - color: #fff; - text-shadow: 0 0 8px #000; - font-size: 16px; - } - - > .koudoku { - margin-left: 4px; - vertical-align: bottom; - } - } - - > .title { - position: absolute; - bottom: 0; - left: 0; - width: 100%; - padding: 0 0 8px 154px; - box-sizing: border-box; - color: #fff; - - > .name { - display: block; - margin: 0; - line-height: 32px; - font-weight: bold; - font-size: 1.8em; - text-shadow: 0 0 8px #000; - } - - > .bottom { - > * { - display: inline-block; - margin-right: 16px; - line-height: 20px; - opacity: 0.8; - - &.username { - font-weight: bold; - } - } - } - } - } - - > .title { - display: none; - text-align: center; - padding: 50px 8px 16px 8px; - font-weight: bold; - border-bottom: solid 0.5px var(--divider); - - > .bottom { - > * { - display: inline-block; - margin-right: 8px; - opacity: 0.8; - } - } - } - - > .avatar { - display: block; - position: absolute; - top: 170px; - left: 16px; - z-index: 2; - width: 120px; - height: 120px; - box-shadow: 1px 1px 3px rgba(#000, 0.2); - } - - > .description { - padding: 24px 24px 24px 154px; - font-size: 0.95em; - - > .empty { - margin: 0; - opacity: 0.5; - } - } - - > .fields { - padding: 24px; - font-size: 0.9em; - border-top: solid 0.5px var(--divider); - - > .field { - display: flex; - padding: 0; - margin: 0; - align-items: center; - - &:not(:last-child) { - margin-bottom: 8px; - } - - > .name { - width: 30%; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - font-weight: bold; - text-align: center; - } - - > .value { - width: 70%; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - margin: 0; - } - } - - &.system > .field > .name { - } - } - - > .status { - display: flex; - padding: 24px; - border-top: solid 0.5px var(--divider); - - > a { - flex: 1; - text-align: center; - - &.active { - color: var(--accent); - } - - &:hover { - text-decoration: none; - } - - > b { - display: block; - line-height: 16px; - } - - > span { - font-size: 70%; - } - } - } - } - } - - > .contents { - > .content { - margin-bottom: var(--margin); - } - } - - &.max-width_500px { - > .profile > .main { - > .banner-container { - height: 140px; - - > .fade { - display: none; - } - - > .title { - display: none; - } - } - - > .title { - display: block; - } - - > .avatar { - top: 90px; - left: 0; - right: 0; - width: 92px; - height: 92px; - margin: auto; - } - - > .description { - padding: 16px; - text-align: center; - } - - > .fields { - padding: 16px; - } - - > .status { - padding: 16px; - } - } - - > .contents { - > .nav { - font-size: 80%; - } - } - } -} -</style> diff --git a/src/client/pages/user/pages.vue b/src/client/pages/user/pages.vue deleted file mode 100644 index ece418cf62..0000000000 --- a/src/client/pages/user/pages.vue +++ /dev/null @@ -1,49 +0,0 @@ -<template> -<div> - <MkPagination :pagination="pagination" #default="{items}" ref="list"> - <MkPagePreview v-for="page in items" :page="page" :key="page.id" class="_gap"/> - </MkPagination> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import MkPagePreview from '@client/components/page-preview.vue'; -import MkPagination from '@client/components/ui/pagination.vue'; - -export default defineComponent({ - components: { - MkPagination, - MkPagePreview, - }, - - props: { - user: { - type: Object, - required: true - }, - }, - - data() { - return { - pagination: { - endpoint: 'users/pages', - limit: 20, - params: { - userId: this.user.id, - } - }, - }; - }, - - watch: { - user() { - this.$refs.list.reload(); - } - } -}); -</script> - -<style lang="scss" scoped> - -</style> diff --git a/src/client/pages/user/reactions.vue b/src/client/pages/user/reactions.vue deleted file mode 100644 index 5ac7e01027..0000000000 --- a/src/client/pages/user/reactions.vue +++ /dev/null @@ -1,81 +0,0 @@ -<template> -<div> - <MkPagination :pagination="pagination" #default="{items}" ref="list"> - <div v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _gap afdcfbfb"> - <div class="header"> - <MkAvatar class="avatar" :user="user"/> - <MkReactionIcon class="reaction" :reaction="item.type" :custom-emojis="item.note.emojis" :no-style="true"/> - <MkTime :time="item.createdAt" class="createdAt"/> - </div> - <MkNote :note="item.note" @update:note="updated(note, $event)" :key="item.id"/> - </div> - </MkPagination> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import MkPagination from '@client/components/ui/pagination.vue'; -import MkNote from '@client/components/note.vue'; -import MkReactionIcon from '@client/components/reaction-icon.vue'; - -export default defineComponent({ - components: { - MkPagination, - MkNote, - MkReactionIcon, - }, - - props: { - user: { - type: Object, - required: true - }, - }, - - data() { - return { - pagination: { - endpoint: 'users/reactions', - limit: 20, - params: { - userId: this.user.id, - } - }, - }; - }, - - watch: { - user() { - this.$refs.list.reload(); - } - }, -}); -</script> - -<style lang="scss" scoped> -.afdcfbfb { - > .header { - display: flex; - align-items: center; - padding: 8px 16px; - margin-bottom: 8px; - border-bottom: solid 2px var(--divider); - - > .avatar { - width: 24px; - height: 24px; - margin-right: 8px; - } - - > .reaction { - width: 32px; - height: 32px; - } - - > .createdAt { - margin-left: auto; - } - } -} -</style> diff --git a/src/client/pages/v.vue b/src/client/pages/v.vue deleted file mode 100644 index 4440e8070e..0000000000 --- a/src/client/pages/v.vue +++ /dev/null @@ -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 '@client/config'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - data() { - return { - [symbols.PAGE_INFO]: { - title: 'Misskey', - icon: null - }, - version, - } - }, -}); -</script> diff --git a/src/client/pages/welcome.entrance.a.vue b/src/client/pages/welcome.entrance.a.vue deleted file mode 100644 index 13f0993793..0000000000 --- a/src/client/pages/welcome.entrance.a.vue +++ /dev/null @@ -1,320 +0,0 @@ -<template> -<div class="rsqzvsbo" v-if="meta"> - <div class="top"> - <MkFeaturedPhotos class="bg"/> - <XTimeline class="tl"/> - <div class="shape1"></div> - <div class="shape2"></div> - <img src="/static-assets/client/misskey.svg" class="misskey"/> - <div class="emojis"> - <MkEmoji :normal="true" :no-style="true" emoji="๐"/> - <MkEmoji :normal="true" :no-style="true" emoji="โค"/> - <MkEmoji :normal="true" :no-style="true" emoji="๐"/> - <MkEmoji :normal="true" :no-style="true" emoji="๐"/> - <MkEmoji :normal="true" :no-style="true" emoji="๐ฎ"/> - </div> - <div class="main _panel"> - <div class="bg"> - <div class="fade"></div> - </div> - <div class="fg"> - <h1> - <!-- ่ๆฏ่ฒใซใใฃใฆใฏใญใดใ่ฆใใชใใชใใฎใงใจใใใใ็กๅนใซ --> - <!-- <img class="logo" v-if="meta.logoImageUrl" :src="meta.logoImageUrl"><span v-else class="text">{{ instanceName }}</span> --> - <span class="text">{{ instanceName }}</span> - </h1> - <div class="about"> - <div class="desc" v-html="meta.description || $ts.headlineMisskey"></div> - </div> - <div class="action"> - <MkButton @click="signup()" inline gradate data-cy-signup style="margin-right: 12px;">{{ $ts.signup }}</MkButton> - <MkButton @click="signin()" inline data-cy-signin>{{ $ts.login }}</MkButton> - </div> - <div class="status" v-if="onlineUsersCount && stats"> - <div> - <I18n :src="$ts.nUsers" text-tag="span" class="users"> - <template #n><b>{{ number(stats.originalUsersCount) }}</b></template> - </I18n> - <I18n :src="$ts.nNotes" text-tag="span" class="notes"> - <template #n><b>{{ number(stats.originalNotesCount) }}</b></template> - </I18n> - </div> - <I18n :src="$ts.onlineUsersCount" text-tag="span" class="online"> - <template #n><b>{{ onlineUsersCount }}</b></template> - </I18n> - </div> - <button class="_button _acrylic menu" @click="showMenu"><i class="fas fa-ellipsis-h"></i></button> - </div> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import { toUnicode } from 'punycode/'; -import XSigninDialog from '@client/components/signin-dialog.vue'; -import XSignupDialog from '@client/components/signup-dialog.vue'; -import MkButton from '@client/components/ui/button.vue'; -import XNote from '@client/components/note.vue'; -import MkFeaturedPhotos from '@client/components/featured-photos.vue'; -import XTimeline from './welcome.timeline.vue'; -import { host, instanceName } from '@client/config'; -import * as os from '@client/os'; -import number from '@client/filters/number'; - -export default defineComponent({ - components: { - MkButton, - XNote, - MkFeaturedPhotos, - XTimeline, - }, - - data() { - return { - host: toUnicode(host), - instanceName, - meta: null, - stats: null, - tags: [], - onlineUsersCount: null, - }; - }, - - created() { - os.api('meta', { detail: true }).then(meta => { - this.meta = meta; - }); - - os.api('stats').then(stats => { - this.stats = stats; - }); - - os.api('get-online-users-count').then(res => { - this.onlineUsersCount = res.count; - }); - - os.api('hashtags/list', { - sort: '+mentionedLocalUsers', - limit: 8 - }).then(tags => { - this.tags = tags; - }); - }, - - methods: { - signin() { - os.popup(XSigninDialog, { - autoSet: true - }, {}, 'closed'); - }, - - signup() { - os.popup(XSignupDialog, { - autoSet: true - }, {}, 'closed'); - }, - - showMenu(ev) { - os.popupMenu([{ - text: this.$t('aboutX', { x: instanceName }), - icon: 'fas fa-info-circle', - action: () => { - os.pageWindow('/about'); - } - }, { - text: this.$ts.aboutMisskey, - icon: 'fas fa-info-circle', - action: () => { - os.pageWindow('/about-misskey'); - } - }, null, { - text: this.$ts.help, - icon: 'fas fa-question-circle', - action: () => { - os.pageWindow('/docs'); - } - }], ev.currentTarget || ev.target); - }, - - number - } -}); -</script> - -<style lang="scss" scoped> -.rsqzvsbo { - > .top { - display: flex; - text-align: center; - min-height: 100vh; - box-sizing: border-box; - padding: 16px; - - > .bg { - position: absolute; - top: 0; - right: 0; - width: 80%; // 100%ใใshapeใฎๅน
ใๅผใใฆใใ - height: 100%; - } - - > .tl { - position: absolute; - top: 0; - bottom: 0; - right: 64px; - margin: auto; - width: 500px; - height: calc(100% - 128px); - overflow: hidden; - -webkit-mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 128px, rgba(0,0,0,1) calc(100% - 128px), rgba(0,0,0,0) 100%); - mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 128px, rgba(0,0,0,1) calc(100% - 128px), rgba(0,0,0,0) 100%); - - @media (max-width: 1200px) { - display: none; - } - } - - > .shape1 { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: var(--accent); - clip-path: polygon(0% 0%, 45% 0%, 20% 100%, 0% 100%); - } - > .shape2 { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: var(--accent); - clip-path: polygon(0% 0%, 25% 0%, 35% 100%, 0% 100%); - opacity: 0.5; - } - - > .misskey { - position: absolute; - top: 42px; - left: 42px; - width: 160px; - - @media (max-width: 450px) { - width: 130px; - } - } - - > .emojis { - position: absolute; - bottom: 32px; - left: 35px; - - > * { - margin-right: 8px; - } - - @media (max-width: 1200px) { - display: none; - } - } - - > .main { - position: relative; - width: min(480px, 100%); - margin: auto auto auto 128px; - box-shadow: 0 12px 32px rgb(0 0 0 / 25%); - - @media (max-width: 1200px) { - margin: auto; - } - - > .bg { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 128px; - background-position: center; - background-size: cover; - opacity: 0.75; - - > .fade { - position: absolute; - bottom: 0; - left: 0; - width: 100%; - height: 128px; - background: linear-gradient(0deg, var(--panel), var(--X15)); - } - } - - > .fg { - position: relative; - z-index: 1; - - > h1 { - display: block; - margin: 0; - padding: 32px 32px 24px 32px; - font-size: 1.5em; - - > .logo { - vertical-align: bottom; - max-height: 120px; - max-width: min(100%, 300px); - } - } - - > .about { - padding: 0 32px; - } - - > .action { - padding: 32px; - - > * { - line-height: 28px; - } - } - - > .status { - border-top: solid 0.5px var(--divider); - padding: 32px; - font-size: 90%; - - > div { - > span:not(:last-child) { - padding-right: 1em; - margin-right: 1em; - border-right: solid 0.5px var(--divider); - } - } - - > .online { - ::v-deep(b) { - color: #41b781; - } - - ::v-deep(span) { - opacity: 0.7; - } - } - } - - > .menu { - position: absolute; - top: 16px; - right: 16px; - width: 32px; - height: 32px; - border-radius: 8px; - } - } - } - } -} -</style> diff --git a/src/client/pages/welcome.entrance.b.vue b/src/client/pages/welcome.entrance.b.vue deleted file mode 100644 index 163fc1e35f..0000000000 --- a/src/client/pages/welcome.entrance.b.vue +++ /dev/null @@ -1,236 +0,0 @@ -<template> -<div class="rsqzvsbo" v-if="meta"> - <div class="top"> - <MkFeaturedPhotos class="bg"/> - <XTimeline class="tl"/> - <div class="shape"></div> - <div class="main"> - <h1> - <img class="logo" v-if="meta.logoImageUrl" :src="meta.logoImageUrl"><span v-else class="text">{{ instanceName }}</span> - </h1> - <div class="about"> - <div class="desc" v-html="meta.description || $ts.headlineMisskey"></div> - </div> - <div class="action"> - <MkButton class="signup" @click="signup()" inline gradate>{{ $ts.signup }}</MkButton> - <MkButton class="signin" @click="signin()" inline>{{ $ts.login }}</MkButton> - </div> - <div class="status" v-if="onlineUsersCount && stats"> - <div> - <I18n :src="$ts.nUsers" text-tag="span" class="users"> - <template #n><b>{{ number(stats.originalUsersCount) }}</b></template> - </I18n> - <I18n :src="$ts.nNotes" text-tag="span" class="notes"> - <template #n><b>{{ number(stats.originalNotesCount) }}</b></template> - </I18n> - </div> - <I18n :src="$ts.onlineUsersCount" text-tag="span" class="online"> - <template #n><b>{{ onlineUsersCount }}</b></template> - </I18n> - </div> - </div> - <img src="/static-assets/client/misskey.svg" class="misskey"/> - </div> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import { toUnicode } from 'punycode/'; -import XSigninDialog from '@client/components/signin-dialog.vue'; -import XSignupDialog from '@client/components/signup-dialog.vue'; -import MkButton from '@client/components/ui/button.vue'; -import XNote from '@client/components/note.vue'; -import MkFeaturedPhotos from '@client/components/featured-photos.vue'; -import XTimeline from './welcome.timeline.vue'; -import { host, instanceName } from '@client/config'; -import * as os from '@client/os'; -import number from '@client/filters/number'; - -export default defineComponent({ - components: { - MkButton, - XNote, - XTimeline, - MkFeaturedPhotos, - }, - - data() { - return { - host: toUnicode(host), - instanceName, - meta: null, - stats: null, - tags: [], - onlineUsersCount: null, - }; - }, - - created() { - os.api('meta', { detail: true }).then(meta => { - this.meta = meta; - }); - - os.api('stats').then(stats => { - this.stats = stats; - }); - - os.api('get-online-users-count').then(res => { - this.onlineUsersCount = res.count; - }); - - os.api('hashtags/list', { - sort: '+mentionedLocalUsers', - limit: 8 - }).then(tags => { - this.tags = tags; - }); - }, - - methods: { - signin() { - os.popup(XSigninDialog, { - autoSet: true - }, {}, 'closed'); - }, - - signup() { - os.popup(XSignupDialog, { - autoSet: true - }, {}, 'closed'); - }, - - showMenu(ev) { - os.popupMenu([{ - text: this.$t('aboutX', { x: instanceName }), - icon: 'fas fa-info-circle', - action: () => { - os.pageWindow('/about'); - } - }, { - text: this.$ts.aboutMisskey, - icon: 'fas fa-info-circle', - action: () => { - os.pageWindow('/about-misskey'); - } - }, null, { - text: this.$ts.help, - icon: 'fas fa-question-circle', - action: () => { - os.pageWindow('/docs'); - } - }], ev.currentTarget || ev.target); - }, - - number - } -}); -</script> - -<style lang="scss" scoped> -.rsqzvsbo { - > .top { - min-height: 100vh; - box-sizing: border-box; - - > .bg { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - } - - > .tl { - position: absolute; - top: 0; - bottom: 0; - right: 64px; - margin: auto; - width: 500px; - height: calc(100% - 128px); - overflow: hidden; - -webkit-mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 128px, rgba(0,0,0,1) calc(100% - 128px), rgba(0,0,0,0) 100%); - mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 128px, rgba(0,0,0,1) calc(100% - 128px), rgba(0,0,0,0) 100%); - } - - > .shape { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: var(--accent); - clip-path: polygon(0% 0%, 40% 0%, 22% 100%, 0% 100%); - } - - > .misskey { - position: absolute; - bottom: 64px; - left: 64px; - width: 160px; - } - - > .main { - position: relative; - width: min(450px, 100%); - padding: 64px; - color: #fff; - font-size: 1.1em; - - @media (max-width: 1200px) { - margin: auto; - } - - > h1 { - display: block; - margin: 0 0 32px 0; - padding: 0; - - > .logo { - vertical-align: bottom; - max-height: 100px; - } - } - - > .about { - padding: 0; - } - - > .action { - margin: 32px 0; - - > * { - line-height: 32px; - } - - > .signup { - background: var(--panel); - color: var(--fg); - } - - > .signin { - background: var(--accent); - color: inherit; - } - } - - > .status { - margin: 32px 0; - border-top: solid 1px rgba(255, 255, 255, 0.5); - font-size: 90%; - - > div { - padding: 16px 0; - - > span:not(:last-child) { - padding-right: 1em; - margin-right: 1em; - border-right: solid 1px rgba(255, 255, 255, 0.5); - } - } - } - } - } -} -</style> diff --git a/src/client/pages/welcome.entrance.c.vue b/src/client/pages/welcome.entrance.c.vue deleted file mode 100644 index bf1c9b1998..0000000000 --- a/src/client/pages/welcome.entrance.c.vue +++ /dev/null @@ -1,305 +0,0 @@ -<template> -<div class="rsqzvsbo" v-if="meta"> - <div class="top"> - <MkFeaturedPhotos class="bg"/> - <div class="fade"></div> - <div class="emojis"> - <MkEmoji :normal="true" :no-style="true" emoji="๐"/> - <MkEmoji :normal="true" :no-style="true" emoji="โค"/> - <MkEmoji :normal="true" :no-style="true" emoji="๐"/> - <MkEmoji :normal="true" :no-style="true" emoji="๐"/> - <MkEmoji :normal="true" :no-style="true" emoji="๐ฎ"/> - </div> - <div class="main"> - <img src="/static-assets/client/misskey.svg" class="misskey"/> - <div class="form _panel"> - <div class="bg"> - <div class="fade"></div> - </div> - <div class="fg"> - <h1> - <img class="logo" v-if="meta.logoImageUrl" :src="meta.logoImageUrl"><span v-else class="text">{{ instanceName }}</span> - </h1> - <div class="about"> - <div class="desc" v-html="meta.description || $ts.headlineMisskey"></div> - </div> - <div class="action"> - <MkButton @click="signup()" inline gradate>{{ $ts.signup }}</MkButton> - <MkButton @click="signin()" inline>{{ $ts.login }}</MkButton> - </div> - <div class="status" v-if="onlineUsersCount && stats"> - <div> - <I18n :src="$ts.nUsers" text-tag="span" class="users"> - <template #n><b>{{ number(stats.originalUsersCount) }}</b></template> - </I18n> - <I18n :src="$ts.nNotes" text-tag="span" class="notes"> - <template #n><b>{{ number(stats.originalNotesCount) }}</b></template> - </I18n> - </div> - <I18n :src="$ts.onlineUsersCount" text-tag="span" class="online"> - <template #n><b>{{ onlineUsersCount }}</b></template> - </I18n> - </div> - <button class="_button _acrylic menu" @click="showMenu"><i class="fas fa-ellipsis-h"></i></button> - </div> - </div> - <nav class="nav"> - <MkA to="/announcements">{{ $ts.announcements }}</MkA> - <MkA to="/explore">{{ $ts.explore }}</MkA> - <MkA to="/channels">{{ $ts.channel }}</MkA> - <MkA to="/featured">{{ $ts.featured }}</MkA> - </nav> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import { toUnicode } from 'punycode/'; -import XSigninDialog from '@client/components/signin-dialog.vue'; -import XSignupDialog from '@client/components/signup-dialog.vue'; -import MkButton from '@client/components/ui/button.vue'; -import XNote from '@client/components/note.vue'; -import MkFeaturedPhotos from '@client/components/featured-photos.vue'; -import XTimeline from './welcome.timeline.vue'; -import { host, instanceName } from '@client/config'; -import * as os from '@client/os'; -import number from '@client/filters/number'; - -export default defineComponent({ - components: { - MkButton, - XNote, - MkFeaturedPhotos, - XTimeline, - }, - - data() { - return { - host: toUnicode(host), - instanceName, - meta: null, - stats: null, - tags: [], - onlineUsersCount: null, - }; - }, - - created() { - os.api('meta', { detail: true }).then(meta => { - this.meta = meta; - }); - - os.api('stats').then(stats => { - this.stats = stats; - }); - - os.api('get-online-users-count').then(res => { - this.onlineUsersCount = res.count; - }); - - os.api('hashtags/list', { - sort: '+mentionedLocalUsers', - limit: 8 - }).then(tags => { - this.tags = tags; - }); - }, - - methods: { - signin() { - os.popup(XSigninDialog, { - autoSet: true - }, {}, 'closed'); - }, - - signup() { - os.popup(XSignupDialog, { - autoSet: true - }, {}, 'closed'); - }, - - showMenu(ev) { - os.popupMenu([{ - text: this.$t('aboutX', { x: instanceName }), - icon: 'fas fa-info-circle', - action: () => { - os.pageWindow('/about'); - } - }, { - text: this.$ts.aboutMisskey, - icon: 'fas fa-info-circle', - action: () => { - os.pageWindow('/about-misskey'); - } - }, null, { - text: this.$ts.help, - icon: 'fas fa-question-circle', - action: () => { - os.pageWindow('/docs'); - } - }], ev.currentTarget || ev.target); - }, - - number - } -}); -</script> - -<style lang="scss" scoped> -.rsqzvsbo { - > .top { - display: flex; - text-align: center; - min-height: 100vh; - box-sizing: border-box; - padding: 16px; - - > .bg { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - } - - > .fade { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.25); - } - - > .emojis { - position: absolute; - bottom: 32px; - left: 35px; - - > * { - margin-right: 8px; - } - - @media (max-width: 1200px) { - display: none; - } - } - - > .main { - position: relative; - width: min(460px, 100%); - margin: auto; - - > .misskey { - width: 150px; - margin-bottom: 16px; - - @media (max-width: 450px) { - width: 130px; - } - } - - > .form { - position: relative; - box-shadow: 0 12px 32px rgb(0 0 0 / 25%); - - > .bg { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 128px; - background-position: center; - background-size: cover; - opacity: 0.75; - - > .fade { - position: absolute; - bottom: 0; - left: 0; - width: 100%; - height: 128px; - background: linear-gradient(0deg, var(--panel), var(--X15)); - } - } - - > .fg { - position: relative; - z-index: 1; - - > h1 { - display: block; - margin: 0; - padding: 32px 32px 24px 32px; - - > .logo { - vertical-align: bottom; - max-height: 120px; - } - } - - > .about { - padding: 0 32px; - } - - > .action { - padding: 32px; - - > * { - line-height: 28px; - } - } - - > .status { - border-top: solid 0.5px var(--divider); - padding: 32px; - font-size: 90%; - - > div { - > span:not(:last-child) { - padding-right: 1em; - margin-right: 1em; - border-right: solid 0.5px var(--divider); - } - } - - > .online { - ::v-deep(b) { - color: #41b781; - } - - ::v-deep(span) { - opacity: 0.7; - } - } - } - - > .menu { - position: absolute; - top: 16px; - right: 16px; - width: 32px; - height: 32px; - border-radius: 8px; - } - } - } - - > .nav { - position: relative; - z-index: 2; - margin-top: 20px; - color: #fff; - text-shadow: 0 0 8px black; - font-size: 0.9em; - - > *:not(:last-child) { - margin-right: 1.5em; - } - } - } - } -} -</style> diff --git a/src/client/pages/welcome.setup.vue b/src/client/pages/welcome.setup.vue deleted file mode 100644 index dfefecc8fa..0000000000 --- a/src/client/pages/welcome.setup.vue +++ /dev/null @@ -1,102 +0,0 @@ -<template> -<form class="mk-setup" @submit.prevent="submit()"> - <h1>Welcome to Misskey!</h1> - <div> - <p>{{ $ts.intro }}</p> - <MkInput v-model="username" pattern="^[a-zA-Z0-9_]{1,20}$" spellcheck="false" required data-cy-admin-username> - <template #label>{{ $ts.username }}</template> - <template #prefix>@</template> - <template #suffix>@{{ host }}</template> - </MkInput> - <MkInput v-model="password" type="password" data-cy-admin-password> - <template #label>{{ $ts.password }}</template> - <template #prefix><i class="fas fa-lock"></i></template> - </MkInput> - <footer> - <MkButton primary type="submit" :disabled="submitting" data-cy-admin-ok> - {{ submitting ? $ts.processing : $ts.done }}<MkEllipsis v-if="submitting"/> - </MkButton> - </footer> - </div> -</form> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import MkButton from '@client/components/ui/button.vue'; -import MkInput from '@client/components/form/input.vue'; -import { host } from '@client/config'; -import * as os from '@client/os'; -import { login } from '@client/account'; - -export default defineComponent({ - components: { - MkButton, - MkInput, - }, - - data() { - return { - username: '', - password: '', - submitting: false, - host, - } - }, - - methods: { - submit() { - if (this.submitting) return; - this.submitting = true; - - os.api('admin/accounts/create', { - username: this.username, - password: this.password, - }).then(res => { - return login(res.token); - }).catch(() => { - this.submitting = false; - - os.dialog({ - type: 'error', - text: this.$ts.somethingHappened - }); - }); - } - } -}); -</script> - -<style lang="scss" scoped> -.mk-setup { - border-radius: var(--radius); - box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1); - overflow: hidden; - max-width: 500px; - margin: 32px auto; - - > h1 { - margin: 0; - font-size: 1.5em; - text-align: center; - padding: 32px; - background: var(--accent); - color: #fff; - } - - > div { - padding: 32px; - background: var(--panel); - - > p { - margin-top: 0; - } - - > footer { - > * { - margin: 0 auto; - } - } - } -} -</style> diff --git a/src/client/pages/welcome.timeline.vue b/src/client/pages/welcome.timeline.vue deleted file mode 100644 index bd07ac78db..0000000000 --- a/src/client/pages/welcome.timeline.vue +++ /dev/null @@ -1,99 +0,0 @@ -<template> -<div class="civpbkhh"> - <div class="scrollbox" ref="scroll" v-bind:class="{ scroll: isScrolling }"> - <div v-for="note in notes" class="note"> - <div class="content _panel"> - <div class="body"> - <MkA class="reply" v-if="note.replyId" :to="`/notes/${note.replyId}`"><i class="fas fa-reply"></i></MkA> - <Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i" :custom-emojis="note.emojis"/> - <MkA class="rp" v-if="note.renoteId" :to="`/notes/${note.renoteId}`">RN: ...</MkA> - </div> - <div v-if="note.files.length > 0" class="richcontent"> - <XMediaList :media-list="note.files"/> - </div> - <div v-if="note.poll"> - <XPoll :note="note" :readOnly="true" /> - </div> - </div> - <XReactionsViewer :note="note" ref="reactionsViewer"/> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import XReactionsViewer from '@client/components/reactions-viewer.vue'; -import XMediaList from '@client/components/media-list.vue'; -import XPoll from '@client/components/poll.vue'; -import * as os from '@client/os'; - -export default defineComponent({ - components: { - XReactionsViewer, - XMediaList, - XPoll - }, - - data() { - return { - notes: [], - isScrolling: false, - } - }, - - created() { - os.api('notes/featured').then(notes => { - this.notes = notes; - }); - }, - - updated() { - if (this.$refs.scroll.clientHeight > window.innerHeight) { - this.isScrolling = true; - } - } -}); -</script> - -<style lang="scss" scoped> -@keyframes scroll { - 0% { - transform: translate3d(0, 0, 0); - } - 5% { - transform: translate3d(0, 0, 0); - } - 75% { - transform: translate3d(0, calc(-100% + 90vh), 0); - } - 90% { - transform: translate3d(0, calc(-100% + 90vh), 0); - } -} - -.civpbkhh { - text-align: right; - - > .scrollbox { - &.scroll { - animation: scroll 45s linear infinite; - } - - > .note { - margin: 16px 0 16px auto; - - > .content { - padding: 16px; - margin: 0 0 0 auto; - max-width: max-content; - border-radius: 16px; - - > .richcontent { - min-width: 250px; - } - } - } - } -} -</style> diff --git a/src/client/pages/welcome.vue b/src/client/pages/welcome.vue deleted file mode 100644 index b6a715830d..0000000000 --- a/src/client/pages/welcome.vue +++ /dev/null @@ -1,38 +0,0 @@ -<template> -<div v-if="meta"> - <XSetup v-if="meta.requireSetup"/> - <XEntrance v-else/> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import XSetup from './welcome.setup.vue'; -import XEntrance from './welcome.entrance.a.vue'; -import { instanceName } from '@client/config'; -import * as os from '@client/os'; -import * as symbols from '@client/symbols'; - -export default defineComponent({ - components: { - XSetup, - XEntrance, - }, - - data() { - return { - [symbols.PAGE_INFO]: { - title: instanceName, - icon: null - }, - meta: null - } - }, - - created() { - os.api('meta', { detail: true }).then(meta => { - this.meta = meta; - }); - } -}); -</script> |