diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2022-01-27 00:17:13 +0900 |
|---|---|---|
| committer | syuilo <Syuilotan@yahoo.co.jp> | 2022-01-27 00:17:13 +0900 |
| commit | 5f5f68cdcd31653cef2ae6bd29ce8bfcf60113ff (patch) | |
| tree | 51e9e6179f6d1bda3013d1412f6e43f9f8f70e86 /packages/client/src/pages | |
| parent | Merge branch 'develop' (diff) | |
| parent | 12.102.0 (diff) | |
| download | misskey-5f5f68cdcd31653cef2ae6bd29ce8bfcf60113ff.tar.gz misskey-5f5f68cdcd31653cef2ae6bd29ce8bfcf60113ff.tar.bz2 misskey-5f5f68cdcd31653cef2ae6bd29ce8bfcf60113ff.zip | |
Merge branch 'develop'
Diffstat (limited to 'packages/client/src/pages')
129 files changed, 2926 insertions, 7325 deletions
diff --git a/packages/client/src/pages/_error_.vue b/packages/client/src/pages/_error_.vue index 2f8f08b5cf..7540995707 100644 --- a/packages/client/src/pages/_error_.vue +++ b/packages/client/src/pages/_error_.vue @@ -1,68 +1,61 @@ <template> -<MkLoading v-if="!loaded" /> +<MkLoading v-if="!loaded"/> <transition :name="$store.state.animation ? 'zoom' : ''" appear> <div v-show="loaded" class="mjndxjch"> <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> + <p><b><i class="fas fa-exclamation-triangle"></i> {{ i18n.locale.pageLoadError }}</b></p> + <p v-if="meta && (version === meta.version)">{{ i18n.locale.pageLoadErrorDescription }}</p> + <p v-else-if="serverIsDead">{{ i18n.locale.serverIsDead }}</p> <template v-else> - <p>{{ $ts.newVersionOfClientAvailable }}</p> - <p>{{ $ts.youShouldUpgradeClient }}</p> - <MkButton class="button primary" @click="reload">{{ $ts.reload }}</MkButton> + <p>{{ i18n.locale.newVersionOfClientAvailable }}</p> + <p>{{ i18n.locale.youShouldUpgradeClient }}</p> + <MkButton class="button primary" @click="reload">{{ i18n.locale.reload }}</MkButton> </template> - <p><MkA to="/docs/general/troubleshooting" class="_link">{{ $ts.troubleshooting }}</MkA></p> + <p><MkA to="/docs/general/troubleshooting" class="_link">{{ i18n.locale.troubleshooting }}</MkA></p> <p v-if="error" class="error">ERROR: {{ error }}</p> </div> </transition> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; +import * as misskey from 'misskey-js'; import MkButton from '@/components/ui/button.vue'; import * as symbols from '@/symbols'; import { version } from '@/config'; import * as os from '@/os'; import { unisonReload } from '@/scripts/unison-reload'; +import { i18n } from '@/i18n'; -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(); - }, +const props = withDefaults(defineProps<{ + error?: Error; +}>(), { +}); + +let loaded = $ref(false); +let serverIsDead = $ref(false); +let meta = $ref<misskey.entities.LiteInstanceMetadata | null>(null); + +os.api('meta', { + detail: false, +}).then(res => { + loaded = true; + serverIsDead = false; + meta = res; + localStorage.setItem('v', res.version); +}, () => { + loaded = true; + serverIsDead = true; +}); + +function reload() { + unisonReload(); +} + +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.locale.error, + icon: 'fas fa-exclamation-triangle', }, }); </script> diff --git a/packages/client/src/pages/_loading_.vue b/packages/client/src/pages/_loading_.vue index 05c6af1cd7..1dd2e46e10 100644 --- a/packages/client/src/pages/_loading_.vue +++ b/packages/client/src/pages/_loading_.vue @@ -2,9 +2,5 @@ <MkLoading/> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; -import * as os from '@/os'; - -export default defineComponent({}); +<script lang="ts" setup> </script> diff --git a/packages/client/src/pages/about-misskey.vue b/packages/client/src/pages/about-misskey.vue index 855a21e493..8119f33051 100644 --- a/packages/client/src/pages/about-misskey.vue +++ b/packages/client/src/pages/about-misskey.vue @@ -3,36 +3,39 @@ <MkSpacer :content-max="600" :margin-min="20"> <div class="_formRoot znqjceqz"> <div id="debug"></div> - <div ref="about" v-panel class="_formBlock about" :class="{ playing: easterEggEngine != null }"> + <div ref="containerEl" v-panel class="_formBlock about" :class="{ playing: easterEggEngine != null }"> <img src="/client-assets/about-icon.png" alt="" class="icon" draggable="false" @load="iconLoaded" @click="gravity"/> <div class="misskey">Misskey</div> <div class="version">v{{ version }}</div> <span v-for="emoji in easterEggEmojis" :key="emoji.id" class="emoji" :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> <div class="_formBlock" style="text-align: center;"> - {{ $ts._aboutMisskey.about }}<br><a href="https://misskey-hub.net/docs/misskey.html" target="_blank" class="_link">{{ $ts.learnMore }}</a> + {{ i18n.locale._aboutMisskey.about }}<br><a href="https://misskey-hub.net/docs/misskey.html" target="_blank" class="_link">{{ i18n.locale.learnMore }}</a> + </div> + <div class="_formBlock" style="text-align: center;"> + <MkButton primary rounded inline @click="iLoveMisskey">I <Mfm text="$[jelly ❤]"/> #Misskey</MkButton> </div> <FormSection> <div class="_formLinks"> <FormLink to="https://github.com/misskey-dev/misskey" external> <template #icon><i class="fas fa-code"></i></template> - {{ $ts._aboutMisskey.source }} + {{ i18n.locale._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 }} + {{ i18n.locale._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 }} + {{ i18n.locale._aboutMisskey.donate }} <template #suffix>Patreon</template> </FormLink> </div> </FormSection> <FormSection> - <template #label>{{ $ts._aboutMisskey.contributors }}</template> + <template #label>{{ i18n.locale._aboutMisskey.contributors }}</template> <div class="_formLinks"> <FormLink to="https://github.com/syuilo" external>@syuilo</FormLink> <FormLink to="https://github.com/AyaMorisawa" external>@AyaMorisawa</FormLink> @@ -44,27 +47,30 @@ <FormLink to="https://github.com/u1-liquid" external>@u1-liquid</FormLink> <FormLink to="https://github.com/marihachi" external>@marihachi</FormLink> </div> - <template #caption><MkLink url="https://github.com/misskey-dev/misskey/graphs/contributors">{{ $ts._aboutMisskey.allContributors }}</MkLink></template> + <template #caption><MkLink url="https://github.com/misskey-dev/misskey/graphs/contributors">{{ i18n.locale._aboutMisskey.allContributors }}</MkLink></template> </FormSection> <FormSection> - <template #label><Mfm text="$[jelly ❤]"/> {{ $ts._aboutMisskey.patrons }}</template> + <template #label><Mfm text="$[jelly ❤]"/> {{ i18n.locale._aboutMisskey.patrons }}</template> <div v-for="patron in patrons" :key="patron">{{ patron }}</div> - <template #caption>{{ $ts._aboutMisskey.morePatrons }}</template> + <template #caption>{{ i18n.locale._aboutMisskey.morePatrons }}</template> </FormSection> </div> </MkSpacer> </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { nextTick, onBeforeUnmount } from 'vue'; import { version } from '@/config'; import FormLink from '@/components/form/link.vue'; import FormSection from '@/components/form/section.vue'; -import MkKeyValue from '@/components/key-value.vue'; +import MkButton from '@/components/ui/button.vue'; import MkLink from '@/components/link.vue'; import { physics } from '@/scripts/physics'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; +import { defaultStore } from '@/store'; +import * as os from '@/os'; const patrons = [ 'まっちゃとーにゅ', @@ -145,59 +151,53 @@ const patrons = [ '蝉暮せせせ', ]; -export default defineComponent({ - components: { - FormSection, - FormLink, - MkKeyValue, - MkLink, - }, +let easterEggReady = false; +let easterEggEmojis = $ref([]); +let easterEggEngine = $ref(null); +const containerEl = $ref<HTMLElement>(); - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.aboutMisskey, - icon: null - }, - version, - patrons, - easterEggReady: false, - easterEggEmojis: [], - easterEggEngine: null, - } - }, +function iconLoaded() { + const emojis = defaultStore.state.reactions; + const containerWidth = containerEl.offsetWidth; + for (let i = 0; i < 32; i++) { + easterEggEmojis.push({ + id: i.toString(), + top: -(128 + (Math.random() * 256)), + left: (Math.random() * containerWidth), + emoji: emojis[Math.floor(Math.random() * emojis.length)], + }); + } - beforeUnmount() { - if (this.easterEggEngine) { - this.easterEggEngine.stop(); - } - }, + nextTick(() => { + easterEggReady = true; + }); +} - 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)], - }); - } +function gravity() { + if (!easterEggReady) return; + easterEggReady = false; + easterEggEngine = physics(containerEl); +} - this.$nextTick(() => { - this.easterEggReady = true; - }); - }, +function iLoveMisskey() { + os.post({ + initialText: 'I $[jelly ❤] #Misskey', + }); +} - gravity() { - if (!this.easterEggReady) return; - this.easterEggReady = false; - this.easterEggEngine = physics(this.$refs.about); - } +onBeforeUnmount(() => { + if (easterEggEngine) { + easterEggEngine.stop(); } }); + +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.locale.aboutMisskey, + icon: null, + bg: 'var(--bg)', + }, +}); </script> <style lang="scss" scoped> diff --git a/packages/client/src/pages/about.vue b/packages/client/src/pages/about.vue index 04f68b7201..a5984c548d 100644 --- a/packages/client/src/pages/about.vue +++ b/packages/client/src/pages/about.vue @@ -24,7 +24,7 @@ </FormSection> <FormSection> - <div class="_inputSplit _formBlock"> + <FormSplit> <MkKeyValue class="_formBlock"> <template #key>{{ $ts.administrator }}</template> <template #value>{{ $instance.maintainerName }}</template> @@ -33,14 +33,14 @@ <template #key>{{ $ts.contact }}</template> <template #value>{{ $instance.maintainerEmail }}</template> </MkKeyValue> - </div> + </FormSplit> <FormLink v-if="$instance.tosUrl" :to="$instance.tosUrl" class="_formBlock" external>{{ $ts.tos }}</FormLink> </FormSection> <FormSuspense :p="initStats"> <FormSection> <template #label>{{ $ts.statistics }}</template> - <div class="_inputSplit"> + <FormSplit> <MkKeyValue class="_formBlock"> <template #key>{{ $ts.users }}</template> <template #value>{{ number(stats.originalUsersCount) }}</template> @@ -49,7 +49,7 @@ <template #key>{{ $ts.notes }}</template> <template #value>{{ number(stats.originalNotesCount) }}</template> </MkKeyValue> - </div> + </FormSplit> </FormSection> </FormSuspense> @@ -67,46 +67,33 @@ </MkSpacer> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { ref } from 'vue'; import { version, instanceName } from '@/config'; import FormLink from '@/components/form/link.vue'; import FormSection from '@/components/form/section.vue'; import FormSuspense from '@/components/form/suspense.vue'; +import FormSplit from '@/components/form/split.vue'; import MkKeyValue from '@/components/key-value.vue'; import * as os from '@/os'; import number from '@/filters/number'; import * as symbols from '@/symbols'; import { host } from '@/config'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - MkKeyValue, - FormSection, - FormLink, - FormSuspense, - }, +const stats = ref(null); - 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; - }) - } - }, +const initStats = () => os.api('stats', { +}).then((res) => { + stats.value = res; +}); - methods: { - number - } +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.locale.instanceInfo, + icon: 'fas fa-info-circle', + bg: 'var(--bg)', + }, }); </script> diff --git a/packages/client/src/pages/admin/abuses.vue b/packages/client/src/pages/admin/abuses.vue index 8df20097b3..92f93797ce 100644 --- a/packages/client/src/pages/admin/abuses.vue +++ b/packages/client/src/pages/admin/abuses.vue @@ -34,27 +34,7 @@ --> <MkPagination v-slot="{items}" ref="reports" :pagination="pagination" style="margin-top: var(--margin);"> - <div v-for="report in items" :key="report.id" class="bcekxzvu _card _gap"> - <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 v-if="!report.resolved" primary @click="resolve(report)">{{ $ts.abuseMarkAsResolved }}</MkButton> - </div> - </div> + <XAbuseReport v-for="report in items" :key="report.id" :report="report" @resolved="resolved"/> </MkPagination> </div> </div> @@ -62,22 +42,21 @@ </template> <script lang="ts"> -import { defineComponent } from 'vue'; +import { computed, defineComponent } from 'vue'; -import MkButton from '@/components/ui/button.vue'; import MkInput from '@/components/form/input.vue'; import MkSelect from '@/components/form/select.vue'; import MkPagination from '@/components/ui/pagination.vue'; -import { acct } from '@/filters/user'; +import XAbuseReport from '@/components/abuse-report.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; export default defineComponent({ components: { - MkButton, MkInput, MkSelect, MkPagination, + XAbuseReport, }, emits: ['info'], @@ -95,44 +74,20 @@ export default defineComponent({ reporterOrigin: 'combined', targetUserOrigin: 'combined', pagination: { - endpoint: 'admin/abuse-user-reports', + endpoint: 'admin/abuse-user-reports' as const, limit: 10, - params: () => ({ + params: computed(() => ({ 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); - }); + resolved(reportId) { + this.$refs.reports.removeItem(item => item.id === reportId); }, } }); @@ -142,29 +97,4 @@ export default defineComponent({ .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/packages/client/src/pages/admin/ads.vue b/packages/client/src/pages/admin/ads.vue index d12ed8563e..8f164caa99 100644 --- a/packages/client/src/pages/admin/ads.vue +++ b/packages/client/src/pages/admin/ads.vue @@ -23,14 +23,14 @@ <MkRadio v-model="ad.priority" value="low">{{ $ts.low }}</MkRadio> </div> --> - <div class="_inputSplit"> + <FormSplit> <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> - </div> + </FormSplit> <MkTextarea v-model="ad.memo" class="_formBlock"> <template #label>{{ $ts.memo }}</template> </MkTextarea> @@ -49,6 +49,7 @@ import MkButton from '@/components/ui/button.vue'; import MkInput from '@/components/form/input.vue'; import MkTextarea from '@/components/form/textarea.vue'; import FormRadios from '@/components/form/radios.vue'; +import FormSplit from '@/components/form/split.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; @@ -58,6 +59,7 @@ export default defineComponent({ MkInput, MkTextarea, FormRadios, + FormSplit, }, emits: ['info'], @@ -85,10 +87,6 @@ export default defineComponent({ }); }, - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { add() { this.ads.unshift({ diff --git a/packages/client/src/pages/admin/announcements.vue b/packages/client/src/pages/admin/announcements.vue index 3614cb1441..a0d720bb29 100644 --- a/packages/client/src/pages/admin/announcements.vue +++ b/packages/client/src/pages/admin/announcements.vue @@ -61,10 +61,6 @@ export default defineComponent({ }); }, - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { add() { this.announcements.unshift({ diff --git a/packages/client/src/pages/admin/bot-protection.vue b/packages/client/src/pages/admin/bot-protection.vue index 5a97083841..82ab155317 100644 --- a/packages/client/src/pages/admin/bot-protection.vue +++ b/packages/client/src/pages/admin/bot-protection.vue @@ -1,70 +1,55 @@ <template> -<FormBase> +<div> <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> + <div class="_formRoot"> + <FormRadios v-model="provider" class="_formBlock"> + <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 v-sticky-container class="_debobigegoItem _debobigegoNoConcat"> - <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 v-sticky-container class="_debobigegoItem _debobigegoNoConcat"> - <div class="_debobigegoLabel">{{ $ts.preview }}</div> - <div class="_debobigegoPanel" style="padding: var(--debobigegoContentHMargin);"> + <template v-if="provider === 'hcaptcha'"> + <FormInput v-model="hcaptchaSiteKey" class="_formBlock"> + <template #prefix><i class="fas fa-key"></i></template> + <template #label>{{ $ts.hcaptchaSiteKey }}</template> + </FormInput> + <FormInput v-model="hcaptchaSecretKey" class="_formBlock"> + <template #prefix><i class="fas fa-key"></i></template> + <template #label>{{ $ts.hcaptchaSecretKey }}</template> + </FormInput> + <FormSlot class="_formBlock"> + <template #label>{{ $ts.preview }}</template> <MkCaptcha provider="hcaptcha" :sitekey="hcaptchaSiteKey || '10000000-ffff-ffff-ffff-000000000001'"/> - </div> - </div> - </template> - <template v-else-if="provider === 'recaptcha'"> - <div v-sticky-container class="_debobigegoItem _debobigegoNoConcat"> - <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" v-sticky-container class="_debobigegoItem _debobigegoNoConcat"> - <div class="_debobigegoLabel">{{ $ts.preview }}</div> - <div class="_debobigegoPanel" style="padding: var(--debobigegoContentHMargin);"> + </FormSlot> + </template> + <template v-else-if="provider === 'recaptcha'"> + <FormInput v-model="recaptchaSiteKey" class="_formBlock"> + <template #prefix><i class="fas fa-key"></i></template> + <template #label>{{ $ts.recaptchaSiteKey }}</template> + </FormInput> + <FormInput v-model="recaptchaSecretKey" class="_formBlock"> + <template #prefix><i class="fas fa-key"></i></template> + <template #label>{{ $ts.recaptchaSecretKey }}</template> + </FormInput> + <FormSlot v-if="recaptchaSiteKey" class="_formBlock"> + <template #label>{{ $ts.preview }}</template> <MkCaptcha provider="recaptcha" :sitekey="recaptchaSiteKey"/> - </div> - </div> - </template> + </FormSlot> + </template> - <FormButton primary @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton> + <FormButton primary @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton> + </div> </FormSuspense> -</FormBase> +</div> </template> <script lang="ts"> import { defineAsyncComponent, defineComponent } from 'vue'; -import FormRadios from '@/components/debobigego/radios.vue'; -import FormInput from '@/components/debobigego/input.vue'; -import FormButton from '@/components/debobigego/button.vue'; -import FormBase from '@/components/debobigego/base.vue'; -import FormGroup from '@/components/debobigego/group.vue'; -import FormInfo from '@/components/debobigego/info.vue'; -import FormSuspense from '@/components/debobigego/suspense.vue'; +import FormRadios from '@/components/form/radios.vue'; +import FormInput from '@/components/form/input.vue'; +import FormButton from '@/components/ui/button.vue'; +import FormSuspense from '@/components/form/suspense.vue'; +import FormSlot from '@/components/form/slot.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; import { fetchInstance } from '@/instance'; @@ -73,11 +58,9 @@ export default defineComponent({ components: { FormRadios, FormInput, - FormBase, - FormGroup, FormButton, - FormInfo, FormSuspense, + FormSlot, MkCaptcha: defineAsyncComponent(() => import('@/components/captcha.vue')), }, @@ -99,10 +82,6 @@ export default defineComponent({ } }, - async mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { async init() { const meta = await os.api('meta', { detail: true }); diff --git a/packages/client/src/pages/admin/database.vue b/packages/client/src/pages/admin/database.vue index b09f1ad867..3a835eeafa 100644 --- a/packages/client/src/pages/admin/database.vue +++ b/packages/client/src/pages/admin/database.vue @@ -1,28 +1,18 @@ <template> -<FormBase> +<MkSpacer :content-max="800" :margin-min="16" :margin-max="32"> <FormSuspense v-slot="{ result: database }" :p="databasePromiseFactory"> - <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> + <MkKeyValue v-for="table in database" :key="table[0]" oneline style="margin: 1em 0;"> + <template #key>{{ table[0] }}</template> + <template #value>{{ bytes(table[1].size) }} ({{ number(table[1].count) }} recs)</template> + </MkKeyValue> </FormSuspense> -</FormBase> +</MkSpacer> </template> <script lang="ts"> import { defineComponent } from 'vue'; -import FormSuspense from '@/components/debobigego/suspense.vue'; -import FormKeyValueView from '@/components/debobigego/key-value-view.vue'; -import FormLink from '@/components/debobigego/link.vue'; -import FormBase from '@/components/debobigego/base.vue'; -import FormGroup from '@/components/debobigego/group.vue'; +import FormSuspense from '@/components/form/suspense.vue'; +import MkKeyValue from '@/components/key-value.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; import bytes from '@/filters/bytes'; @@ -31,10 +21,7 @@ import number from '@/filters/number'; export default defineComponent({ components: { FormSuspense, - FormKeyValueView, - FormBase, - FormGroup, - FormLink, + MkKeyValue, }, emits: ['info'], @@ -50,10 +37,6 @@ export default defineComponent({ } }, - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { bytes, number, } diff --git a/packages/client/src/pages/admin/email-settings.vue b/packages/client/src/pages/admin/email-settings.vue index 873a853918..6491a453ab 100644 --- a/packages/client/src/pages/admin/email-settings.vue +++ b/packages/client/src/pages/admin/email-settings.vue @@ -1,50 +1,55 @@ <template> -<FormBase> +<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> <FormSuspense :p="init"> - <FormSwitch v-model="enableEmail">{{ $ts.enableEmail }}<template #desc>{{ $ts.emailConfigInfo }}</template></FormSwitch> + <div class="_formRoot"> + <FormSwitch v-model="enableEmail" class="_formBlock"> + <template #label>{{ $ts.enableEmail }}</template> + <template #caption>{{ $ts.emailConfigInfo }}</template> + </FormSwitch> - <template v-if="enableEmail"> - <FormInput v-model="email" type="email"> - <span>{{ $ts.emailAddress }}</span> - </FormInput> + <template v-if="enableEmail"> + <FormInput v-model="email" type="email" class="_formBlock"> + <template #label>{{ $ts.emailAddress }}</template> + </FormInput> - <div v-sticky-container class="_debobigegoItem _debobigegoNoConcat"> - <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 primary @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton> + <FormSection> + <template #label>{{ $ts.smtpConfig }}</template> + <FormSplit :min-width="280"> + <FormInput v-model="smtpHost" class="_formBlock"> + <template #label>{{ $ts.smtpHost }}</template> + </FormInput> + <FormInput v-model="smtpPort" type="number" class="_formBlock"> + <template #label>{{ $ts.smtpPort }}</template> + </FormInput> + </FormSplit> + <FormSplit :min-width="280"> + <FormInput v-model="smtpUser" class="_formBlock"> + <template #label>{{ $ts.smtpUser }}</template> + </FormInput> + <FormInput v-model="smtpPass" type="password" class="_formBlock"> + <template #label>{{ $ts.smtpPass }}</template> + </FormInput> + </FormSplit> + <FormInfo class="_formBlock">{{ $ts.emptyToDisableSmtpAuth }}</FormInfo> + <FormSwitch v-model="smtpSecure" class="_formBlock"> + <template #label>{{ $ts.smtpSecure }}</template> + <template #caption>{{ $ts.smtpSecureInfo }}</template> + </FormSwitch> + </FormSection> + </template> + </div> </FormSuspense> -</FormBase> +</MkSpacer> </template> <script lang="ts"> import { defineComponent } from 'vue'; -import FormSwitch from '@/components/debobigego/switch.vue'; -import FormInput from '@/components/debobigego/input.vue'; -import FormButton from '@/components/debobigego/button.vue'; -import FormBase from '@/components/debobigego/base.vue'; -import FormGroup from '@/components/debobigego/group.vue'; -import FormInfo from '@/components/debobigego/info.vue'; -import FormSuspense from '@/components/debobigego/suspense.vue'; +import FormSwitch from '@/components/form/switch.vue'; +import FormInput from '@/components/form/input.vue'; +import FormInfo from '@/components/ui/info.vue'; +import FormSuspense from '@/components/form/suspense.vue'; +import FormSplit from '@/components/form/split.vue'; +import FormSection from '@/components/form/section.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; import { fetchInstance } from '@/instance'; @@ -53,9 +58,8 @@ export default defineComponent({ components: { FormSwitch, FormInput, - FormBase, - FormGroup, - FormButton, + FormSplit, + FormSection, FormInfo, FormSuspense, }, @@ -68,6 +72,16 @@ export default defineComponent({ title: this.$ts.emailServer, icon: 'fas fa-envelope', bg: 'var(--bg)', + actions: [{ + asFullButton: true, + text: this.$ts.testEmail, + handler: this.testEmail, + }, { + asFullButton: true, + icon: 'fas fa-check', + text: this.$ts.save, + handler: this.save, + }], }, enableEmail: false, email: null, @@ -79,10 +93,6 @@ export default defineComponent({ } }, - async mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { async init() { const meta = await os.api('meta', { detail: true }); diff --git a/packages/client/src/pages/admin/emoji-edit-dialog.vue b/packages/client/src/pages/admin/emoji-edit-dialog.vue index a45d92fa16..2e3903426e 100644 --- a/packages/client/src/pages/admin/emoji-edit-dialog.vue +++ b/packages/client/src/pages/admin/emoji-edit-dialog.vue @@ -95,7 +95,7 @@ export default defineComponent({ }); if (canceled) return; - os.api('admin/emoji/remove', { + os.api('admin/emoji/delete', { id: this.emoji.id }).then(() => { this.$emit('done', { diff --git a/packages/client/src/pages/admin/emojis.vue b/packages/client/src/pages/admin/emojis.vue index 49277325a0..5b1dfe565a 100644 --- a/packages/client/src/pages/admin/emojis.vue +++ b/packages/client/src/pages/admin/emojis.vue @@ -6,11 +6,22 @@ <template #prefix><i class="fas fa-search"></i></template> <template #label>{{ $ts.search }}</template> </MkInput> - <MkPagination ref="emojis" :pagination="pagination"> + <MkSwitch v-model="selectMode" style="margin: 8px 0;"> + <template #label>Select mode</template> + </MkSwitch> + <div v-if="selectMode" style="display: flex; gap: var(--margin); flex-wrap: wrap;"> + <MkButton inline @click="selectAll">Select all</MkButton> + <MkButton inline @click="setCategoryBulk">Set category</MkButton> + <MkButton inline @click="addTagBulk">Add tag</MkButton> + <MkButton inline @click="removeTagBulk">Remove tag</MkButton> + <MkButton inline @click="setTagBulk">Set tag</MkButton> + <MkButton inline danger @click="delBulk">Delete</MkButton> + </div> + <MkPagination ref="emojisPaginationComponent" :pagination="pagination"> <template #empty><span>{{ $ts.noCustomEmojis }}</span></template> <template v-slot="{items}"> <div class="ldhfsamy"> - <button v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" @click="edit(emoji)"> + <button v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" :class="{ selected: selectedEmojis.includes(emoji.id) }" @click="selectMode ? toggleSelect(emoji) : edit(emoji)"> <img :src="emoji.url" class="img" :alt="emoji.name"/> <div class="body"> <div class="name _monospace">{{ emoji.name }}</div> @@ -23,7 +34,7 @@ </div> <div v-else-if="tab === 'remote'" class="remote"> - <div class="_inputSplit"> + <FormSplit> <MkInput v-model="queryRemote" :debounce="true" type="search"> <template #prefix><i class="fas fa-search"></i></template> <template #label>{{ $ts.search }}</template> @@ -31,8 +42,8 @@ <MkInput v-model="host" :debounce="true"> <template #label>{{ $ts.host }}</template> </MkInput> - </div> - <MkPagination ref="remoteEmojis" :pagination="remotePagination"> + </FormSplit> + <MkPagination :pagination="remotePagination"> <template #empty><span>{{ $ts.noCustomEmojis }}</span></template> <template v-slot="{items}"> <div class="ldhfsamy"> @@ -51,146 +62,233 @@ </MkSpacer> </template> -<script lang="ts"> -import { computed, defineComponent, toRef } from 'vue'; +<script lang="ts" setup> +import { computed, defineComponent, ref, toRef } from 'vue'; import MkButton from '@/components/ui/button.vue'; import MkInput from '@/components/form/input.vue'; import MkPagination from '@/components/ui/pagination.vue'; import MkTab from '@/components/tab.vue'; -import { selectFiles } from '@/scripts/select-file'; +import MkSwitch from '@/components/form/switch.vue'; +import FormSplit from '@/components/form/split.vue'; +import { selectFile, selectFiles } from '@/scripts/select-file'; import * as os from '@/os'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - MkTab, - MkButton, - MkInput, - MkPagination, - }, +const emojisPaginationComponent = ref<InstanceType<typeof MkPagination>>(); - emits: ['info'], +const tab = ref('local'); +const query = ref(null); +const queryRemote = ref(null); +const host = ref(null); +const selectMode = ref(false); +const selectedEmojis = ref<string[]>([]); - 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, - }, { - icon: 'fas fa-ellipsis-h', - handler: this.menu, - }], - 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 - })) - }, - } - }, +const pagination = { + endpoint: 'admin/emoji/list' as const, + limit: 30, + params: computed(() => ({ + query: (query.value && query.value !== '') ? query.value : null, + })), +}; - async mounted() { - this.$emit('info', toRef(this, symbols.PAGE_INFO)); - }, +const remotePagination = { + endpoint: 'admin/emoji/list-remote' as const, + limit: 30, + params: computed(() => ({ + query: (queryRemote.value && queryRemote.value !== '') ? queryRemote.value : null, + host: (host.value && host.value !== '') ? host.value : null, + })), +}; - methods: { - async add(e) { - const files = await selectFiles(e.currentTarget || e.target, null); +const selectAll = () => { + if (selectedEmojis.value.length > 0) { + selectedEmojis.value = []; + } else { + selectedEmojis.value = emojisPaginationComponent.value.items.map(item => item.id); + } +}; - const promise = Promise.all(files.map(file => os.api('admin/emoji/add', { - fileId: file.id, - }))); - promise.then(() => { - this.$refs.emojis.reload(); - }); - os.promiseDialog(promise); - }, +const toggleSelect = (emoji) => { + if (selectedEmojis.value.includes(emoji.id)) { + selectedEmojis.value = selectedEmojis.value.filter(x => x !== emoji.id); + } else { + selectedEmojis.value.push(emoji.id); + } +}; - 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'); - }, +const add = async (ev: MouseEvent) => { + const files = await selectFiles(ev.currentTarget || ev.target, null); - im(emoji) { - os.apiWithDialog('admin/emoji/copy', { - emojiId: emoji.id, - }); - }, + const promise = Promise.all(files.map(file => os.api('admin/emoji/add', { + fileId: file.id, + }))); + promise.then(() => { + emojisPaginationComponent.value.reload(); + }); + os.promiseDialog(promise); +}; - 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); +const edit = (emoji) => { + os.popup(import('./emoji-edit-dialog.vue'), { + emoji: emoji + }, { + done: result => { + if (result.updated) { + emojisPaginationComponent.value.replaceItem(item => item.id === emoji.id, { + ...emoji, + ...result.updated + }); + } else if (result.deleted) { + emojisPaginationComponent.value.removeItem(item => item.id === emoji.id); + } }, + }, 'closed'); +}; - menu(ev) { - os.popupMenu([{ - icon: 'fas fa-download', - text: this.$ts.export, - action: async () => { - os.api('export-custom-emojis', { - }) - .then(() => { - os.alert({ - type: 'info', - text: this.$ts.exportRequested, - }); - }).catch((e) => { - os.alert({ - type: 'error', - text: e.message, - }); - }); - } - }], ev.currentTarget || ev.target); +const im = (emoji) => { + os.apiWithDialog('admin/emoji/copy', { + emojiId: emoji.id, + }); +}; + +const remoteMenu = (emoji, ev: MouseEvent) => { + os.popupMenu([{ + type: 'label', + text: ':' + emoji.name + ':', + }, { + text: i18n.locale.import, + icon: 'fas fa-plus', + action: () => { im(emoji) } + }], ev.currentTarget || ev.target); +}; + +const menu = (ev: MouseEvent) => { + os.popupMenu([{ + icon: 'fas fa-download', + text: i18n.locale.export, + action: async () => { + os.api('export-custom-emojis', { + }) + .then(() => { + os.alert({ + type: 'info', + text: i18n.locale.exportRequested, + }); + }).catch((e) => { + os.alert({ + type: 'error', + text: e.message, + }); + }); } - } + }, { + icon: 'fas fa-upload', + text: i18n.locale.import, + action: async () => { + const file = await selectFile(ev.currentTarget || ev.target); + os.api('admin/emoji/import-zip', { + fileId: file.id, + }) + .then(() => { + os.alert({ + type: 'info', + text: i18n.locale.importRequested, + }); + }).catch((e) => { + os.alert({ + type: 'error', + text: e.message, + }); + }); + } + }], ev.currentTarget || ev.target); +}; + +const setCategoryBulk = async () => { + const { canceled, result } = await os.inputText({ + title: 'Category', + }); + if (canceled) return; + await os.apiWithDialog('admin/emoji/set-category-bulk', { + ids: selectedEmojis.value, + category: result, + }); + emojisPaginationComponent.value.reload(); +}; + +const addTagBulk = async () => { + const { canceled, result } = await os.inputText({ + title: 'Tag', + }); + if (canceled) return; + await os.apiWithDialog('admin/emoji/add-aliases-bulk', { + ids: selectedEmojis.value, + aliases: result.split(' '), + }); + emojisPaginationComponent.value.reload(); +}; + +const removeTagBulk = async () => { + const { canceled, result } = await os.inputText({ + title: 'Tag', + }); + if (canceled) return; + await os.apiWithDialog('admin/emoji/remove-aliases-bulk', { + ids: selectedEmojis.value, + aliases: result.split(' '), + }); + emojisPaginationComponent.value.reload(); +}; + +const setTagBulk = async () => { + const { canceled, result } = await os.inputText({ + title: 'Tag', + }); + if (canceled) return; + await os.apiWithDialog('admin/emoji/set-aliases-bulk', { + ids: selectedEmojis.value, + aliases: result.split(' '), + }); + emojisPaginationComponent.value.reload(); +}; + +const delBulk = async () => { + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.locale.deleteConfirm, + }); + if (canceled) return; + await os.apiWithDialog('admin/emoji/delete-bulk', { + ids: selectedEmojis.value, + }); + emojisPaginationComponent.value.reload(); +}; + +defineExpose({ + [symbols.PAGE_INFO]: computed(() => ({ + title: i18n.locale.customEmojis, + icon: 'fas fa-laugh', + bg: 'var(--bg)', + actions: [{ + asFullButton: true, + icon: 'fas fa-plus', + text: i18n.locale.addEmoji, + handler: add, + }, { + icon: 'fas fa-ellipsis-h', + handler: menu, + }], + tabs: [{ + active: tab.value === 'local', + title: i18n.locale.local, + onClick: () => { tab.value = 'local'; }, + }, { + active: tab.value === 'remote', + title: i18n.locale.remote, + onClick: () => { tab.value = 'remote'; }, + },] + })), }); </script> @@ -210,11 +308,16 @@ export default defineComponent({ > .emoji { display: flex; align-items: center; - padding: 12px; + padding: 11px; text-align: left; + border: solid 1px var(--panel); &:hover { - color: var(--accent); + border-color: var(--inputBorderHover); + } + + &.selected { + border-color: var(--accent); } > .img { diff --git a/packages/client/src/pages/admin/files-settings.vue b/packages/client/src/pages/admin/files-settings.vue deleted file mode 100644 index df25bd0fb2..0000000000 --- a/packages/client/src/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 primary @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton> - </FormSuspense> -</FormBase> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import FormSwitch from '@/components/debobigego/switch.vue'; -import FormInput from '@/components/debobigego/input.vue'; -import FormButton from '@/components/debobigego/button.vue'; -import FormBase from '@/components/debobigego/base.vue'; -import FormGroup from '@/components/debobigego/group.vue'; -import FormSuspense from '@/components/debobigego/suspense.vue'; -import * as os from '@/os'; -import * as symbols from '@/symbols'; -import { fetchInstance } from '@/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/packages/client/src/pages/admin/files.vue b/packages/client/src/pages/admin/files.vue index 032e394a66..87dd12f489 100644 --- a/packages/client/src/pages/admin/files.vue +++ b/packages/client/src/pages/admin/files.vue @@ -19,7 +19,7 @@ <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'"> + <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> @@ -55,7 +55,7 @@ </template> <script lang="ts"> -import { defineComponent } from 'vue'; +import { computed, defineComponent } from 'vue'; import MkButton from '@/components/ui/button.vue'; import MkInput from '@/components/form/input.vue'; import MkSelect from '@/components/form/select.vue'; @@ -95,33 +95,17 @@ export default defineComponent({ type: null, searchHost: '', pagination: { - endpoint: 'admin/drive/files', + endpoint: 'admin/drive/files' as const, limit: 10, - params: () => ({ + params: computed(() => ({ type: (this.type && this.type !== '') ? this.type : null, origin: this.origin, - hostname: (this.hostname && this.hostname !== '') ? this.hostname : null, - }), + hostname: (this.searchHost && this.searchHost !== '') ? this.searchHost : 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.confirm({ diff --git a/packages/client/src/pages/admin/index.vue b/packages/client/src/pages/admin/index.vue index e363d1bd03..350e7defc6 100644 --- a/packages/client/src/pages/admin/index.vue +++ b/packages/client/src/pages/admin/index.vue @@ -3,14 +3,14 @@ <div v-if="!narrow || page == null" class="nav"> <MkHeader :info="header"></MkHeader> - <MkSpacer :content-max="700"> + <MkSpacer :content-max="700" :margin-min="16"> <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> + <MkInfo v-if="noBotProtection" warn class="info">{{ $ts.noBotProtectionWarning }} <MkA to="/admin/security" class="_link">{{ $ts.configure }}</MkA></MkInfo> <MkSuperMenu :def="menuDef" :grid="page == null"></MkSuperMenu> </div> @@ -19,7 +19,7 @@ <div class="main"> <MkStickyContainer> <template #header><MkHeader v-if="childInfo && !childInfo.hideHeader" :info="childInfo"/></template> - <component :is="component" :key="page" v-bind="pageProps" @info="onInfo"/> + <component :is="component" :ref="el => pageChanged(el)" :key="page" v-bind="pageProps"/> </MkStickyContainer> </div> </div> @@ -29,9 +29,6 @@ import { computed, defineAsyncComponent, defineComponent, isRef, nextTick, onMounted, reactive, ref, watch } from 'vue'; import { i18n } from '@/i18n'; import MkSuperMenu from '@/components/ui/super-menu.vue'; -import FormGroup from '@/components/debobigego/group.vue'; -import FormBase from '@/components/debobigego/base.vue'; -import FormButton from '@/components/debobigego/button.vue'; import MkInfo from '@/components/ui/info.vue'; import { scroll } from '@/scripts/scroll'; import { instance } from '@/instance'; @@ -41,10 +38,7 @@ import { lookupUser } from '@/scripts/lookup-user'; export default defineComponent({ components: { - FormBase, MkSuperMenu, - FormGroup, - FormButton, MkInfo, }, @@ -72,7 +66,9 @@ export default defineComponent({ const narrow = ref(false); const view = ref(null); const el = ref(null); - const onInfo = (viewInfo) => { + const pageChanged = (page) => { + if (page == null) return; + const viewInfo = page[symbols.PAGE_INFO]; if (isRef(viewInfo)) { watch(viewInfo, () => { childInfo.value = viewInfo.value; @@ -163,11 +159,6 @@ export default defineComponent({ 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', @@ -183,11 +174,6 @@ export default defineComponent({ 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', @@ -236,17 +222,11 @@ export default defineComponent({ 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')); @@ -333,7 +313,7 @@ export default defineComponent({ narrow, view, el, - onInfo, + pageChanged, childInfo, pageProps, component, diff --git a/packages/client/src/pages/admin/instance-block.vue b/packages/client/src/pages/admin/instance-block.vue index 2e899de687..6cadc7df39 100644 --- a/packages/client/src/pages/admin/instance-block.vue +++ b/packages/client/src/pages/admin/instance-block.vue @@ -1,39 +1,29 @@ <template> -<FormBase> +<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> <FormSuspense :p="init"> - <FormTextarea v-model="blockedHosts"> + <FormTextarea v-model="blockedHosts" class="_formBlock"> <span>{{ $ts.blockedInstances }}</span> - <template #desc>{{ $ts.blockedInstancesDescription }}</template> + <template #caption>{{ $ts.blockedInstancesDescription }}</template> </FormTextarea> - <FormButton primary @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton> + <FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton> </FormSuspense> -</FormBase> +</MkSpacer> </template> <script lang="ts"> import { defineComponent } from 'vue'; -import FormSwitch from '@/components/debobigego/switch.vue'; -import FormInput from '@/components/debobigego/input.vue'; -import FormButton from '@/components/debobigego/button.vue'; -import FormBase from '@/components/debobigego/base.vue'; -import FormGroup from '@/components/debobigego/group.vue'; -import FormTextarea from '@/components/debobigego/textarea.vue'; -import FormInfo from '@/components/debobigego/info.vue'; -import FormSuspense from '@/components/debobigego/suspense.vue'; +import FormButton from '@/components/ui/button.vue'; +import FormTextarea from '@/components/form/textarea.vue'; +import FormSuspense from '@/components/form/suspense.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; import { fetchInstance } from '@/instance'; export default defineComponent({ components: { - FormSwitch, - FormInput, - FormBase, - FormGroup, FormButton, FormTextarea, - FormInfo, FormSuspense, }, @@ -50,10 +40,6 @@ export default defineComponent({ } }, - async mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { async init() { const meta = await os.api('meta', { detail: true }); diff --git a/packages/client/src/pages/admin/instance.vue b/packages/client/src/pages/admin/instance.vue deleted file mode 100644 index 51fcb8675a..0000000000 --- a/packages/client/src/pages/admin/instance.vue +++ /dev/null @@ -1,291 +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 style="margin: 0.5em 0 0.5em 0;" @click="deleteAllFiles()"><i class="fas fa-trash-alt"></i> {{ $ts.deleteAllFiles }}</MkButton> - </details> - <details> - <summary>{{ $ts.removeAllFollowing }}</summary> - <MkButton style="margin: 0.5em 0 0.5em 0;" @click="removeAllFollowing()"><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 '@/components/ui/modal-window.vue'; -import MkSelect from '@/components/form/select.vue'; -import MkButton from '@/components/ui/button.vue'; -import MkSwitch from '@/components/form/switch.vue'; -import MkInfo from '@/components/ui/info.vue'; -import MkChart from '@/components/chart.vue'; -import bytes from '@/filters/bytes'; -import number from '@/filters/number'; -import * as os from '@/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() { - // TODO: ページ遷移 - }, - - showFollowers() { - // TODO: ページ遷移 - }, - - showUsers() { - // TODO: ページ遷移 - }, - - 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/packages/client/src/pages/admin/integrations-discord.vue b/packages/client/src/pages/admin/integrations.discord.vue index 383031f3d1..8fc340150a 100644 --- a/packages/client/src/pages/admin/integrations-discord.vue +++ b/packages/client/src/pages/admin/integrations.discord.vue @@ -1,37 +1,36 @@ <template> -<FormBase> - <FormSuspense :p="init"> - <FormSwitch v-model="enableDiscordIntegration"> - {{ $ts.enable }} +<FormSuspense :p="init"> + <div class="_formRoot"> + <FormSwitch v-model="enableDiscordIntegration" class="_formBlock"> + <template #label>{{ $ts.enable }}</template> </FormSwitch> <template v-if="enableDiscordIntegration"> - <FormInfo>Callback URL: {{ `${uri}/api/dc/cb` }}</FormInfo> + <FormInfo class="_formBlock">Callback URL: {{ `${uri}/api/dc/cb` }}</FormInfo> - <FormInput v-model="discordClientId"> + <FormInput v-model="discordClientId" class="_formBlock"> <template #prefix><i class="fas fa-key"></i></template> - Client ID + <template #label>Client ID</template> </FormInput> - <FormInput v-model="discordClientSecret"> + <FormInput v-model="discordClientSecret" class="_formBlock"> <template #prefix><i class="fas fa-key"></i></template> - Client Secret + <template #label>Client Secret</template> </FormInput> </template> - <FormButton primary @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton> - </FormSuspense> -</FormBase> + <FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton> + </div> +</FormSuspense> </template> <script lang="ts"> import { defineComponent } from 'vue'; -import FormSwitch from '@/components/debobigego/switch.vue'; -import FormInput from '@/components/debobigego/input.vue'; -import FormButton from '@/components/debobigego/button.vue'; -import FormBase from '@/components/debobigego/base.vue'; -import FormInfo from '@/components/debobigego/info.vue'; -import FormSuspense from '@/components/debobigego/suspense.vue'; +import FormSwitch from '@/components/form/switch.vue'; +import FormInput from '@/components/form/input.vue'; +import FormButton from '@/components/ui/button.vue'; +import FormInfo from '@/components/ui/info.vue'; +import FormSuspense from '@/components/form/suspense.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; import { fetchInstance } from '@/instance'; @@ -40,7 +39,6 @@ export default defineComponent({ components: { FormSwitch, FormInput, - FormBase, FormInfo, FormButton, FormSuspense, @@ -60,10 +58,6 @@ export default defineComponent({ } }, - async mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { async init() { const meta = await os.api('meta', { detail: true }); diff --git a/packages/client/src/pages/admin/integrations-github.vue b/packages/client/src/pages/admin/integrations.github.vue index ecb2fd67fa..d9db9c00f1 100644 --- a/packages/client/src/pages/admin/integrations-github.vue +++ b/packages/client/src/pages/admin/integrations.github.vue @@ -1,37 +1,36 @@ <template> -<FormBase> - <FormSuspense :p="init"> - <FormSwitch v-model="enableGithubIntegration"> - {{ $ts.enable }} +<FormSuspense :p="init"> + <div class="_formRoot"> + <FormSwitch v-model="enableGithubIntegration" class="_formBlock"> + <template #label>{{ $ts.enable }}</template> </FormSwitch> <template v-if="enableGithubIntegration"> - <FormInfo>Callback URL: {{ `${uri}/api/gh/cb` }}</FormInfo> + <FormInfo class="_formBlock">Callback URL: {{ `${uri}/api/gh/cb` }}</FormInfo> - <FormInput v-model="githubClientId"> + <FormInput v-model="githubClientId" class="_formBlock"> <template #prefix><i class="fas fa-key"></i></template> - Client ID + <template #label>Client ID</template> </FormInput> - <FormInput v-model="githubClientSecret"> + <FormInput v-model="githubClientSecret" class="_formBlock"> <template #prefix><i class="fas fa-key"></i></template> - Client Secret + <template #label>Client Secret</template> </FormInput> </template> - <FormButton primary @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton> - </FormSuspense> -</FormBase> + <FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton> + </div> +</FormSuspense> </template> <script lang="ts"> import { defineComponent } from 'vue'; -import FormSwitch from '@/components/debobigego/switch.vue'; -import FormInput from '@/components/debobigego/input.vue'; -import FormButton from '@/components/debobigego/button.vue'; -import FormBase from '@/components/debobigego/base.vue'; -import FormInfo from '@/components/debobigego/info.vue'; -import FormSuspense from '@/components/debobigego/suspense.vue'; +import FormSwitch from '@/components/form/switch.vue'; +import FormInput from '@/components/form/input.vue'; +import FormButton from '@/components/ui/button.vue'; +import FormInfo from '@/components/ui/info.vue'; +import FormSuspense from '@/components/form/suspense.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; import { fetchInstance } from '@/instance'; @@ -40,7 +39,6 @@ export default defineComponent({ components: { FormSwitch, FormInput, - FormBase, FormInfo, FormButton, FormSuspense, @@ -60,10 +58,6 @@ export default defineComponent({ } }, - async mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { async init() { const meta = await os.api('meta', { detail: true }); diff --git a/packages/client/src/pages/admin/integrations-twitter.vue b/packages/client/src/pages/admin/integrations.twitter.vue index 1404102c57..1f8074535a 100644 --- a/packages/client/src/pages/admin/integrations-twitter.vue +++ b/packages/client/src/pages/admin/integrations.twitter.vue @@ -1,37 +1,36 @@ <template> -<FormBase> - <FormSuspense :p="init"> - <FormSwitch v-model="enableTwitterIntegration"> - {{ $ts.enable }} +<FormSuspense :p="init"> + <div class="_formRoot"> + <FormSwitch v-model="enableTwitterIntegration" class="_formBlock"> + <template #label>{{ $ts.enable }}</template> </FormSwitch> <template v-if="enableTwitterIntegration"> - <FormInfo>Callback URL: {{ `${uri}/api/tw/cb` }}</FormInfo> + <FormInfo class="_formBlock">Callback URL: {{ `${uri}/api/tw/cb` }}</FormInfo> - <FormInput v-model="twitterConsumerKey"> + <FormInput v-model="twitterConsumerKey" class="_formBlock"> <template #prefix><i class="fas fa-key"></i></template> - Consumer Key + <template #label>Consumer Key</template> </FormInput> - <FormInput v-model="twitterConsumerSecret"> + <FormInput v-model="twitterConsumerSecret" class="_formBlock"> <template #prefix><i class="fas fa-key"></i></template> - Consumer Secret + <template #label>Consumer Secret</template> </FormInput> </template> - <FormButton primary @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton> - </FormSuspense> -</FormBase> + <FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton> + </div> +</FormSuspense> </template> <script lang="ts"> import { defineComponent } from 'vue'; -import FormSwitch from '@/components/debobigego/switch.vue'; -import FormInput from '@/components/debobigego/input.vue'; -import FormButton from '@/components/debobigego/button.vue'; -import FormBase from '@/components/debobigego/base.vue'; -import FormInfo from '@/components/debobigego/info.vue'; -import FormSuspense from '@/components/debobigego/suspense.vue'; +import FormSwitch from '@/components/form/switch.vue'; +import FormInput from '@/components/form/input.vue'; +import FormButton from '@/components/ui/button.vue'; +import FormInfo from '@/components/ui/info.vue'; +import FormSuspense from '@/components/form/suspense.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; import { fetchInstance } from '@/instance'; @@ -40,7 +39,6 @@ export default defineComponent({ components: { FormSwitch, FormInput, - FormBase, FormInfo, FormButton, FormSuspense, @@ -60,10 +58,6 @@ export default defineComponent({ } }, - async mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { async init() { const meta = await os.api('meta', { detail: true }); diff --git a/packages/client/src/pages/admin/integrations.vue b/packages/client/src/pages/admin/integrations.vue index c21eebc1c6..91d03fef31 100644 --- a/packages/client/src/pages/admin/integrations.vue +++ b/packages/client/src/pages/admin/integrations.vue @@ -1,46 +1,48 @@ <template> -<FormBase> +<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> <FormSuspense :p="init"> - <FormLink to="/admin/integrations/twitter"> - <i class="fab fa-twitter"></i> Twitter + <FormFolder class="_formBlock"> + <template #icon><i class="fab fa-twitter"></i></template> + <template #label>Twitter</template> <template #suffix>{{ enableTwitterIntegration ? $ts.enabled : $ts.disabled }}</template> - </FormLink> - <FormLink to="/admin/integrations/github"> - <i class="fab fa-github"></i> GitHub + <XTwitter/> + </FormFolder> + <FormFolder to="/admin/integrations/github" class="_formBlock"> + <template #icon><i class="fab fa-github"></i></template> + <template #label>GitHub</template> <template #suffix>{{ enableGithubIntegration ? $ts.enabled : $ts.disabled }}</template> - </FormLink> - <FormLink to="/admin/integrations/discord"> - <i class="fab fa-discord"></i> Discord + <XGithub/> + </FormFolder> + <FormFolder to="/admin/integrations/discord" class="_formBlock"> + <template #icon><i class="fab fa-discord"></i></template> + <template #label>Discord</template> <template #suffix>{{ enableDiscordIntegration ? $ts.enabled : $ts.disabled }}</template> - </FormLink> + <XDiscord/> + </FormFolder> </FormSuspense> -</FormBase> +</MkSpacer> </template> <script lang="ts"> import { defineComponent } from 'vue'; -import FormLink from '@/components/debobigego/link.vue'; -import FormInput from '@/components/debobigego/input.vue'; -import FormButton from '@/components/debobigego/button.vue'; -import FormBase from '@/components/debobigego/base.vue'; -import FormGroup from '@/components/debobigego/group.vue'; -import FormTextarea from '@/components/debobigego/textarea.vue'; -import FormInfo from '@/components/debobigego/info.vue'; -import FormSuspense from '@/components/debobigego/suspense.vue'; +import FormFolder from '@/components/form/folder.vue'; +import FormSecion from '@/components/form/section.vue'; +import FormSuspense from '@/components/form/suspense.vue'; +import XTwitter from './integrations.twitter.vue'; +import XGithub from './integrations.github.vue'; +import XDiscord from './integrations.discord.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; import { fetchInstance } from '@/instance'; export default defineComponent({ components: { - FormLink, - FormInput, - FormBase, - FormGroup, - FormButton, - FormTextarea, - FormInfo, + FormFolder, + FormSecion, FormSuspense, + XTwitter, + XGithub, + XDiscord, }, emits: ['info'], @@ -58,10 +60,6 @@ export default defineComponent({ } }, - async mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { async init() { const meta = await os.api('meta', { detail: true }); diff --git a/packages/client/src/pages/admin/metrics.vue b/packages/client/src/pages/admin/metrics.vue index 05b64b235c..1de297fd93 100644 --- a/packages/client/src/pages/admin/metrics.vue +++ b/packages/client/src/pages/admin/metrics.vue @@ -76,7 +76,6 @@ import MkwFederation from '../../widgets/federation.vue'; import { version, url } from '@/config'; import bytes from '@/filters/bytes'; import number from '@/filters/number'; -import MkInstanceInfo from './instance.vue'; Chart.register( ArcElement, @@ -101,6 +100,7 @@ const alpha = (hex, a) => { return `rgba(${r}, ${g}, ${b}, ${a})`; }; import * as os from '@/os'; +import { stream } from '@/stream'; export default defineComponent({ components: { @@ -119,7 +119,7 @@ export default defineComponent({ stats: null, serverInfo: null, connection: null, - queueConnection: markRaw(os.stream.useChannel('queueStats')), + queueConnection: markRaw(stream.useChannel('queueStats')), memUsage: 0, chartCpuMem: null, chartNet: null, @@ -150,7 +150,7 @@ export default defineComponent({ os.api('admin/server-info', {}).then(res => { this.serverInfo = res; - this.connection = markRaw(os.stream.useChannel('serverStats')); + this.connection = markRaw(stream.useChannel('serverStats')); this.connection.on('stats', this.onStats); this.connection.on('statsLog', this.onStatsLog); this.connection.send('requestLog', { diff --git a/packages/client/src/pages/admin/object-storage.vue b/packages/client/src/pages/admin/object-storage.vue index 8984686b5e..6c5be220f8 100644 --- a/packages/client/src/pages/admin/object-storage.vue +++ b/packages/client/src/pages/admin/object-storage.vue @@ -1,76 +1,78 @@ <template> -<FormBase> +<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> <FormSuspense :p="init"> - <FormSwitch v-model="useObjectStorage">{{ $ts.useObjectStorage }}</FormSwitch> + <div class="_formRoot"> + <FormSwitch v-model="useObjectStorage" class="_formBlock">{{ $ts.useObjectStorage }}</FormSwitch> - <template v-if="useObjectStorage"> - <FormInput v-model="objectStorageBaseUrl"> - <span>{{ $ts.objectStorageBaseUrl }}</span> - <template #desc>{{ $ts.objectStorageBaseUrlDesc }}</template> - </FormInput> + <template v-if="useObjectStorage"> + <FormInput v-model="objectStorageBaseUrl" class="_formBlock"> + <template #label>{{ $ts.objectStorageBaseUrl }}</template> + <template #caption>{{ $ts.objectStorageBaseUrlDesc }}</template> + </FormInput> - <FormInput v-model="objectStorageBucket"> - <span>{{ $ts.objectStorageBucket }}</span> - <template #desc>{{ $ts.objectStorageBucketDesc }}</template> - </FormInput> + <FormInput v-model="objectStorageBucket" class="_formBlock"> + <template #label>{{ $ts.objectStorageBucket }}</template> + <template #caption>{{ $ts.objectStorageBucketDesc }}</template> + </FormInput> - <FormInput v-model="objectStoragePrefix"> - <span>{{ $ts.objectStoragePrefix }}</span> - <template #desc>{{ $ts.objectStoragePrefixDesc }}</template> - </FormInput> + <FormInput v-model="objectStoragePrefix" class="_formBlock"> + <template #label>{{ $ts.objectStoragePrefix }}</template> + <template #caption>{{ $ts.objectStoragePrefixDesc }}</template> + </FormInput> - <FormInput v-model="objectStorageEndpoint"> - <span>{{ $ts.objectStorageEndpoint }}</span> - <template #desc>{{ $ts.objectStorageEndpointDesc }}</template> - </FormInput> + <FormInput v-model="objectStorageEndpoint" class="_formBlock"> + <template #label>{{ $ts.objectStorageEndpoint }}</template> + <template #caption>{{ $ts.objectStorageEndpointDesc }}</template> + </FormInput> - <FormInput v-model="objectStorageRegion"> - <span>{{ $ts.objectStorageRegion }}</span> - <template #desc>{{ $ts.objectStorageRegionDesc }}</template> - </FormInput> + <FormInput v-model="objectStorageRegion" class="_formBlock"> + <template #label>{{ $ts.objectStorageRegion }}</template> + <template #caption>{{ $ts.objectStorageRegionDesc }}</template> + </FormInput> - <FormInput v-model="objectStorageAccessKey"> - <template #prefix><i class="fas fa-key"></i></template> - <span>Access key</span> - </FormInput> + <FormSplit :min-width="280"> + <FormInput v-model="objectStorageAccessKey" class="_formBlock"> + <template #prefix><i class="fas fa-key"></i></template> + <template #label>Access key</template> + </FormInput> - <FormInput v-model="objectStorageSecretKey"> - <template #prefix><i class="fas fa-key"></i></template> - <span>Secret key</span> - </FormInput> + <FormInput v-model="objectStorageSecretKey" class="_formBlock"> + <template #prefix><i class="fas fa-key"></i></template> + <template #label>Secret key</template> + </FormInput> + </FormSplit> - <FormSwitch v-model="objectStorageUseSSL"> - {{ $ts.objectStorageUseSSL }} - <template #desc>{{ $ts.objectStorageUseSSLDesc }}</template> - </FormSwitch> + <FormSwitch v-model="objectStorageUseSSL" class="_formBlock"> + <template #label>{{ $ts.objectStorageUseSSL }}</template> + <template #caption>{{ $ts.objectStorageUseSSLDesc }}</template> + </FormSwitch> - <FormSwitch v-model="objectStorageUseProxy"> - {{ $ts.objectStorageUseProxy }} - <template #desc>{{ $ts.objectStorageUseProxyDesc }}</template> - </FormSwitch> + <FormSwitch v-model="objectStorageUseProxy" class="_formBlock"> + <template #label>{{ $ts.objectStorageUseProxy }}</template> + <template #caption>{{ $ts.objectStorageUseProxyDesc }}</template> + </FormSwitch> - <FormSwitch v-model="objectStorageSetPublicRead"> - {{ $ts.objectStorageSetPublicRead }} - </FormSwitch> + <FormSwitch v-model="objectStorageSetPublicRead" class="_formBlock"> + <template #label>{{ $ts.objectStorageSetPublicRead }}</template> + </FormSwitch> - <FormSwitch v-model="objectStorageS3ForcePathStyle"> - s3ForcePathStyle - </FormSwitch> - </template> - - <FormButton primary @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton> + <FormSwitch v-model="objectStorageS3ForcePathStyle" class="_formBlock"> + <template #label>s3ForcePathStyle</template> + </FormSwitch> + </template> + </div> </FormSuspense> -</FormBase> +</MkSpacer> </template> <script lang="ts"> import { defineComponent } from 'vue'; -import FormSwitch from '@/components/debobigego/switch.vue'; -import FormInput from '@/components/debobigego/input.vue'; -import FormButton from '@/components/debobigego/button.vue'; -import FormBase from '@/components/debobigego/base.vue'; -import FormGroup from '@/components/debobigego/group.vue'; -import FormSuspense from '@/components/debobigego/suspense.vue'; +import FormSwitch from '@/components/form/switch.vue'; +import FormInput from '@/components/form/input.vue'; +import FormGroup from '@/components/form/group.vue'; +import FormSuspense from '@/components/form/suspense.vue'; +import FormSplit from '@/components/form/split.vue'; +import FormSection from '@/components/form/section.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; import { fetchInstance } from '@/instance'; @@ -79,10 +81,10 @@ export default defineComponent({ components: { FormSwitch, FormInput, - FormBase, FormGroup, - FormButton, FormSuspense, + FormSplit, + FormSection, }, emits: ['info'], @@ -93,6 +95,12 @@ export default defineComponent({ title: this.$ts.objectStorage, icon: 'fas fa-cloud', bg: 'var(--bg)', + actions: [{ + asFullButton: true, + icon: 'fas fa-check', + text: this.$ts.save, + handler: this.save, + }], }, useObjectStorage: false, objectStorageBaseUrl: null, @@ -110,10 +118,6 @@ export default defineComponent({ } }, - async mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { async init() { const meta = await os.api('meta', { detail: true }); diff --git a/packages/client/src/pages/admin/other-settings.vue b/packages/client/src/pages/admin/other-settings.vue index eb214a21c8..6b588e88aa 100644 --- a/packages/client/src/pages/admin/other-settings.vue +++ b/packages/client/src/pages/admin/other-settings.vue @@ -1,34 +1,17 @@ <template> -<FormBase> +<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> <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 primary @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton> + none </FormSuspense> -</FormBase> +</MkSpacer> </template> <script lang="ts"> import { defineComponent } from 'vue'; -import FormSwitch from '@/components/debobigego/switch.vue'; -import FormInput from '@/components/debobigego/input.vue'; -import FormButton from '@/components/debobigego/button.vue'; -import FormBase from '@/components/debobigego/base.vue'; -import FormGroup from '@/components/debobigego/group.vue'; -import FormSuspense from '@/components/debobigego/suspense.vue'; +import FormSwitch from '@/components/form/switch.vue'; +import FormInput from '@/components/form/input.vue'; +import FormSection from '@/components/form/section.vue'; +import FormSuspense from '@/components/form/suspense.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; import { fetchInstance } from '@/instance'; @@ -37,9 +20,7 @@ export default defineComponent({ components: { FormSwitch, FormInput, - FormBase, - FormGroup, - FormButton, + FormSection, FormSuspense, }, @@ -51,29 +32,22 @@ export default defineComponent({ title: this.$ts.other, icon: 'fas fa-cogs', bg: 'var(--bg)', + actions: [{ + asFullButton: true, + icon: 'fas fa-check', + text: this.$ts.save, + handler: this.save, + }], }, - 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(); }); diff --git a/packages/client/src/pages/admin/overview.vue b/packages/client/src/pages/admin/overview.vue index da5fc0ba6d..b8ae8ad9e1 100644 --- a/packages/client/src/pages/admin/overview.vue +++ b/packages/client/src/pages/admin/overview.vue @@ -19,7 +19,7 @@ <MkContainer :foldable="true" class="charts"> <template #header><i class="fas fa-chart-bar"></i>{{ $ts.charts }}</template> - <div style="padding-top: 12px;"> + <div style="padding: 12px;"> <MkInstanceStats :chart-limit="500" :detailed="true"/> </div> </MkContainer> @@ -67,7 +67,6 @@ <script lang="ts"> import { computed, defineComponent, markRaw, version as vueVersion } from 'vue'; -import FormKeyValueView from '@/components/debobigego/key-value-view.vue'; import MkInstanceStats from '@/components/instance-stats.vue'; import MkButton from '@/components/ui/button.vue'; import MkSelect from '@/components/form/select.vue'; @@ -78,15 +77,14 @@ import MkQueueChart from '@/components/queue-chart.vue'; import { version, url } from '@/config'; import bytes from '@/filters/bytes'; import number from '@/filters/number'; -import MkInstanceInfo from './instance.vue'; import XMetrics from './metrics.vue'; import * as os from '@/os'; +import { stream } from '@/stream'; import * as symbols from '@/symbols'; export default defineComponent({ components: { MkNumberDiff, - FormKeyValueView, MkInstanceStats, MkContainer, MkFolder, @@ -113,13 +111,11 @@ export default defineComponent({ notesComparedToThePrevDay: null, fetchJobs: () => os.api('admin/queue/deliver-delayed', {}), fetchModLogs: () => os.api('admin/show-moderation-logs', {}), - queueStatsConnection: markRaw(os.stream.useChannel('queueStats')), + queueStatsConnection: markRaw(stream.useChannel('queueStats')), } }, async mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - os.api('meta', { detail: true }).then(meta => { this.meta = meta; }); @@ -160,9 +156,7 @@ export default defineComponent({ host: q }); } - os.popup(MkInstanceInfo, { - instance: instance - }, {}, 'closed'); + // TODO }, bytes, diff --git a/packages/client/src/pages/admin/proxy-account.vue b/packages/client/src/pages/admin/proxy-account.vue index 14ef92a747..5c4fbffa0c 100644 --- a/packages/client/src/pages/admin/proxy-account.vue +++ b/packages/client/src/pages/admin/proxy-account.vue @@ -1,42 +1,32 @@ <template> -<FormBase> +<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> <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> + <MkInfo class="_formBlock">{{ $ts.proxyAccountDescription }}</MkInfo> + <MkKeyValue class="_formBlock"> + <template #key>{{ $ts.proxyAccount }}</template> + <template #value>{{ proxyAccount ? `@${proxyAccount.username}` : $ts.none }}</template> + </MkKeyValue> - <FormButton primary @click="chooseProxyAccount">{{ $ts.selectAccount }}</FormButton> + <FormButton primary class="_formBlock" @click="chooseProxyAccount">{{ $ts.selectAccount }}</FormButton> </FormSuspense> -</FormBase> +</MkSpacer> </template> <script lang="ts"> import { defineComponent } from 'vue'; -import FormKeyValueView from '@/components/debobigego/key-value-view.vue'; -import FormInput from '@/components/debobigego/input.vue'; -import FormButton from '@/components/debobigego/button.vue'; -import FormBase from '@/components/debobigego/base.vue'; -import FormGroup from '@/components/debobigego/group.vue'; -import FormTextarea from '@/components/debobigego/textarea.vue'; -import FormInfo from '@/components/debobigego/info.vue'; -import FormSuspense from '@/components/debobigego/suspense.vue'; +import MkKeyValue from '@/components/key-value.vue'; +import FormButton from '@/components/ui/button.vue'; +import MkInfo from '@/components/ui/info.vue'; +import FormSuspense from '@/components/form/suspense.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; import { fetchInstance } from '@/instance'; export default defineComponent({ components: { - FormKeyValueView, - FormInput, - FormBase, - FormGroup, + MkKeyValue, FormButton, - FormTextarea, - FormInfo, + MkInfo, FormSuspense, }, @@ -54,10 +44,6 @@ export default defineComponent({ } }, - async mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { async init() { const meta = await os.api('meta', { detail: true }); diff --git a/packages/client/src/pages/admin/queue.vue b/packages/client/src/pages/admin/queue.vue index 37a87089cb..522210d933 100644 --- a/packages/client/src/pages/admin/queue.vue +++ b/packages/client/src/pages/admin/queue.vue @@ -1,28 +1,25 @@ <template> -<FormBase> +<MkSpacer :content-max="800"> <XQueue :connection="connection" domain="inbox"> <template #title>In</template> </XQueue> <XQueue :connection="connection" domain="deliver"> <template #title>Out</template> </XQueue> - <FormButton danger @click="clear()"><i class="fas fa-trash-alt"></i> {{ $ts.clearQueue }}</FormButton> -</FormBase> + <MkButton danger @click="clear()"><i class="fas fa-trash-alt"></i> {{ $ts.clearQueue }}</MkButton> +</MkSpacer> </template> <script lang="ts"> import { defineComponent, markRaw } from 'vue'; import MkButton from '@/components/ui/button.vue'; import XQueue from './queue.chart.vue'; -import FormBase from '@/components/debobigego/base.vue'; -import FormButton from '@/components/debobigego/button.vue'; import * as os from '@/os'; +import { stream } from '@/stream'; import * as symbols from '@/symbols'; export default defineComponent({ components: { - FormBase, - FormButton, MkButton, XQueue, }, @@ -36,13 +33,11 @@ export default defineComponent({ icon: 'fas fa-clipboard-list', bg: 'var(--bg)', }, - connection: markRaw(os.stream.useChannel('queueStats')), + connection: markRaw(stream.useChannel('queueStats')), } }, mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - this.$nextTick(() => { this.connection.send('requestLog', { id: Math.random().toString().substr(2, 8), diff --git a/packages/client/src/pages/admin/relays.vue b/packages/client/src/pages/admin/relays.vue index 3e2f1c6f26..bb840db0a2 100644 --- a/packages/client/src/pages/admin/relays.vue +++ b/packages/client/src/pages/admin/relays.vue @@ -1,32 +1,27 @@ <template> -<FormBase class="relaycxt"> - <FormButton primary @click="addRelay"><i class="fas fa-plus"></i> {{ $ts.addRelay }}</FormButton> - - <div v-for="relay in relays" :key="relay.inbox" class="_debobigegoItem"> - <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> +<MkSpacer :content-max="800"> + <div v-for="relay in relays" :key="relay.inbox" class="relaycxt _panel _block" style="padding: 16px;"> + <div>{{ relay.inbox }}</div> + <div class="status"> + <i v-if="relay.status === 'accepted'" class="fas fa-check icon accepted"></i> + <i v-else-if="relay.status === 'rejected'" class="fas fa-ban icon rejected"></i> + <i v-else class="fas fa-clock icon requesting"></i> + <span>{{ $t(`_relayStatus.${relay.status}`) }}</span> </div> + <MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton> </div> -</FormBase> +</MkSpacer> </template> <script lang="ts"> import { defineComponent } from 'vue'; import MkButton from '@/components/ui/button.vue'; -import MkInput from '@/components/form/input.vue'; -import FormBase from '@/components/debobigego/base.vue'; -import FormButton from '@/components/debobigego/button.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; export default defineComponent({ components: { - FormBase, - FormButton, MkButton, - MkInput, }, emits: ['info'], @@ -37,6 +32,12 @@ export default defineComponent({ title: this.$ts.relays, icon: 'fas fa-globe', bg: 'var(--bg)', + actions: [{ + asFullButton: true, + icon: 'fas fa-plus', + text: this.$ts.addRelay, + handler: this.addRelay, + }], }, relays: [], inbox: '', @@ -47,10 +48,6 @@ export default defineComponent({ this.refresh(); }, - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { async addRelay() { const { canceled, result: inbox } = await os.inputText({ @@ -94,5 +91,22 @@ export default defineComponent({ </script> <style lang="scss" scoped> +.relaycxt { + > .status { + margin: 8px 0; + + > .icon { + width: 1em; + margin-right: 0.75em; + &.accepted { + color: var(--success); + } + + &.rejected { + color: var(--error); + } + } + } +} </style> diff --git a/packages/client/src/pages/admin/security.vue b/packages/client/src/pages/admin/security.vue index adfb2e786c..d069891647 100644 --- a/packages/client/src/pages/admin/security.vue +++ b/packages/client/src/pages/admin/security.vue @@ -1,44 +1,58 @@ <template> -<FormBase> +<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> <FormSuspense :p="init"> - <FormLink to="/admin/bot-protection"> - <i class="fas fa-shield-alt"></i> {{ $ts.botProtection }} - <template v-if="enableHcaptcha" #suffix>hCaptcha</template> - <template v-else-if="enableRecaptcha" #suffix>reCAPTCHA</template> - <template v-else #suffix>{{ $ts.none }} ({{ $ts.notRecommended }})</template> - </FormLink> + <div class="_formRoot"> + <FormFolder class="_formBlock"> + <template #icon><i class="fas fa-shield-alt"></i></template> + <template #label>{{ $ts.botProtection }}</template> + <template v-if="enableHcaptcha" #suffix>hCaptcha</template> + <template v-else-if="enableRecaptcha" #suffix>reCAPTCHA</template> + <template v-else #suffix>{{ $ts.none }} ({{ $ts.notRecommended }})</template> - <FormSwitch v-model="enableRegistration">{{ $ts.enableRegistration }}</FormSwitch> + <XBotProtection/> + </FormFolder> - <FormSwitch v-model="emailRequiredForSignup">{{ $ts.emailRequiredForSignup }}</FormSwitch> + <FormFolder class="_formBlock"> + <template #label>Summaly Proxy</template> - <FormButton primary @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton> + <div class="_formRoot"> + <FormInput v-model="summalyProxy" class="_formBlock"> + <template #prefix><i class="fas fa-link"></i></template> + <template #label>Summaly Proxy URL</template> + </FormInput> + + <FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton> + </div> + </FormFolder> + </div> </FormSuspense> -</FormBase> +</MkSpacer> </template> <script lang="ts"> import { defineAsyncComponent, defineComponent } from 'vue'; -import FormLink from '@/components/debobigego/link.vue'; -import FormSwitch from '@/components/debobigego/switch.vue'; -import FormButton from '@/components/debobigego/button.vue'; -import FormBase from '@/components/debobigego/base.vue'; -import FormGroup from '@/components/debobigego/group.vue'; -import FormInfo from '@/components/debobigego/info.vue'; -import FormSuspense from '@/components/debobigego/suspense.vue'; +import FormFolder from '@/components/form/folder.vue'; +import FormSwitch from '@/components/form/switch.vue'; +import FormInfo from '@/components/ui/info.vue'; +import FormSuspense from '@/components/form/suspense.vue'; +import FormSection from '@/components/form/section.vue'; +import FormInput from '@/components/form/input.vue'; +import FormButton from '@/components/ui/button.vue'; +import XBotProtection from './bot-protection.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; import { fetchInstance } from '@/instance'; export default defineComponent({ components: { - FormLink, + FormFolder, FormSwitch, - FormBase, - FormGroup, - FormButton, FormInfo, + FormSection, FormSuspense, + FormButton, + FormInput, + XBotProtection, }, emits: ['info'], @@ -50,30 +64,23 @@ export default defineComponent({ icon: 'fas fa-lock', bg: 'var(--bg)', }, + summalyProxy: '', 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.summalyProxy = meta.summalyProxy; 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, + summalyProxy: this.summalyProxy, }).then(() => { fetchInstance(); }); diff --git a/packages/client/src/pages/admin/service-worker.vue b/packages/client/src/pages/admin/service-worker.vue deleted file mode 100644 index f34cb03e4e..0000000000 --- a/packages/client/src/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 primary @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton> - </FormSuspense> -</FormBase> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import FormSwitch from '@/components/debobigego/switch.vue'; -import FormInput from '@/components/debobigego/input.vue'; -import FormButton from '@/components/debobigego/button.vue'; -import FormBase from '@/components/debobigego/base.vue'; -import FormGroup from '@/components/debobigego/group.vue'; -import FormSuspense from '@/components/debobigego/suspense.vue'; -import * as os from '@/os'; -import * as symbols from '@/symbols'; -import { fetchInstance } from '@/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/packages/client/src/pages/admin/settings.vue b/packages/client/src/pages/admin/settings.vue index d88445abdb..a4bac93834 100644 --- a/packages/client/src/pages/admin/settings.vue +++ b/packages/client/src/pages/admin/settings.vue @@ -1,72 +1,146 @@ <template> -<FormBase> +<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> <FormSuspense :p="init"> - <FormInput v-model="name"> - <span>{{ $ts.instanceName }}</span> - </FormInput> + <div class="_formRoot"> + <FormInput v-model="name" class="_formBlock"> + <template #label>{{ $ts.instanceName }}</template> + </FormInput> - <FormTextarea v-model="description"> - <span>{{ $ts.instanceDescription }}</span> - </FormTextarea> + <FormTextarea v-model="description" class="_formBlock"> + <template #label>{{ $ts.instanceDescription }}</template> + </FormTextarea> - <FormInput v-model="iconUrl"> - <template #prefix><i class="fas fa-link"></i></template> - <span>{{ $ts.iconUrl }}</span> - </FormInput> + <FormInput v-model="iconUrl" class="_formBlock"> + <template #prefix><i class="fas fa-link"></i></template> + <template #label>{{ $ts.iconUrl }}</template> + </FormInput> - <FormInput v-model="bannerUrl"> - <template #prefix><i class="fas fa-link"></i></template> - <span>{{ $ts.bannerUrl }}</span> - </FormInput> + <FormInput v-model="bannerUrl" class="_formBlock"> + <template #prefix><i class="fas fa-link"></i></template> + <template #label>{{ $ts.bannerUrl }}</template> + </FormInput> - <FormInput v-model="backgroundImageUrl"> - <template #prefix><i class="fas fa-link"></i></template> - <span>{{ $ts.backgroundImageUrl }}</span> - </FormInput> + <FormInput v-model="backgroundImageUrl" class="_formBlock"> + <template #prefix><i class="fas fa-link"></i></template> + <template #label>{{ $ts.backgroundImageUrl }}</template> + </FormInput> - <FormInput v-model="tosUrl"> - <template #prefix><i class="fas fa-link"></i></template> - <span>{{ $ts.tosUrl }}</span> - </FormInput> + <FormInput v-model="tosUrl" class="_formBlock"> + <template #prefix><i class="fas fa-link"></i></template> + <template #label>{{ $ts.tosUrl }}</template> + </FormInput> - <FormInput v-model="maintainerName"> - <span>{{ $ts.maintainerName }}</span> - </FormInput> + <FormSplit :min-width="300"> + <FormInput v-model="maintainerName" class="_formBlock"> + <template #label>{{ $ts.maintainerName }}</template> + </FormInput> - <FormInput v-model="maintainerEmail" type="email"> - <template #prefix><i class="fas fa-envelope"></i></template> - <span>{{ $ts.maintainerEmail }}</span> - </FormInput> + <FormInput v-model="maintainerEmail" type="email" class="_formBlock"> + <template #prefix><i class="fas fa-envelope"></i></template> + <template #label>{{ $ts.maintainerEmail }}</template> + </FormInput> + </FormSplit> - <FormTextarea v-model="pinnedUsers"> - <span>{{ $ts.pinnedUsers }}</span> - <template #desc>{{ $ts.pinnedUsersDescription }}</template> - </FormTextarea> + <FormTextarea v-model="pinnedUsers" class="_formBlock"> + <template #label>{{ $ts.pinnedUsers }}</template> + <template #caption>{{ $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> + <FormInput v-model="maxNoteTextLength" type="number" class="_formBlock"> + <template #prefix><i class="fas fa-pencil-alt"></i></template> + <template #label>{{ $ts.maxNoteTextLength }}</template> + </FormInput> - <FormSwitch v-model="enableLocalTimeline">{{ $ts.enableLocalTimeline }}</FormSwitch> - <FormSwitch v-model="enableGlobalTimeline">{{ $ts.enableGlobalTimeline }}</FormSwitch> - <FormInfo>{{ $ts.disablingTimelinesInfo }}</FormInfo> + <FormSection> + <FormSwitch v-model="enableRegistration" class="_formBlock"> + <template #label>{{ $ts.enableRegistration }}</template> + </FormSwitch> - <FormButton primary @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton> + <FormSwitch v-model="emailRequiredForSignup" class="_formBlock"> + <template #label>{{ $ts.emailRequiredForSignup }}</template> + </FormSwitch> + </FormSection> + + <FormSection> + <FormSwitch v-model="enableLocalTimeline" class="_formBlock">{{ $ts.enableLocalTimeline }}</FormSwitch> + <FormSwitch v-model="enableGlobalTimeline" class="_formBlock">{{ $ts.enableGlobalTimeline }}</FormSwitch> + <FormInfo class="_formBlock">{{ $ts.disablingTimelinesInfo }}</FormInfo> + </FormSection> + + <FormSection> + <template #label>{{ $ts.files }}</template> + + <FormSwitch v-model="cacheRemoteFiles" class="_formBlock"> + <template #label>{{ $ts.cacheRemoteFiles }}</template> + <template #caption>{{ $ts.cacheRemoteFilesDescription }}</template> + </FormSwitch> + + <FormSwitch v-model="proxyRemoteFiles" class="_formBlock"> + <template #label>{{ $ts.proxyRemoteFiles }}</template> + <template #caption>{{ $ts.proxyRemoteFilesDescription }}</template> + </FormSwitch> + + <FormSplit :min-width="280"> + <FormInput v-model="localDriveCapacityMb" type="number" class="_formBlock"> + <template #label>{{ $ts.driveCapacityPerLocalAccount }}</template> + <template #suffix>MB</template> + <template #caption>{{ $ts.inMb }}</template> + </FormInput> + + <FormInput v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles" class="_formBlock"> + <template #label>{{ $ts.driveCapacityPerRemoteAccount }}</template> + <template #suffix>MB</template> + <template #caption>{{ $ts.inMb }}</template> + </FormInput> + </FormSplit> + </FormSection> + + <FormSection> + <template #label>ServiceWorker</template> + + <FormSwitch v-model="enableServiceWorker" class="_formBlock"> + <template #label>{{ $ts.enableServiceworker }}</template> + <template #caption>{{ $ts.serviceworkerInfo }}</template> + </FormSwitch> + + <template v-if="enableServiceWorker"> + <FormInput v-model="swPublicKey" class="_formBlock"> + <template #prefix><i class="fas fa-key"></i></template> + <template #label>Public key</template> + </FormInput> + + <FormInput v-model="swPrivateKey" class="_formBlock"> + <template #prefix><i class="fas fa-key"></i></template> + <template #label>Private key</template> + </FormInput> + </template> + </FormSection> + + <FormSection> + <template #label>DeepL Translation</template> + + <FormInput v-model="deeplAuthKey" class="_formBlock"> + <template #prefix><i class="fas fa-key"></i></template> + <template #label>DeepL Auth Key</template> + </FormInput> + <FormSwitch v-model="deeplIsPro" class="_formBlock"> + <template #label>Pro account</template> + </FormSwitch> + </FormSection> + </div> </FormSuspense> -</FormBase> +</MkSpacer> </template> <script lang="ts"> import { defineComponent } from 'vue'; -import FormSwitch from '@/components/debobigego/switch.vue'; -import FormInput from '@/components/debobigego/input.vue'; -import FormButton from '@/components/debobigego/button.vue'; -import FormBase from '@/components/debobigego/base.vue'; -import FormGroup from '@/components/debobigego/group.vue'; -import FormTextarea from '@/components/debobigego/textarea.vue'; -import FormInfo from '@/components/debobigego/info.vue'; -import FormSuspense from '@/components/debobigego/suspense.vue'; +import FormSwitch from '@/components/form/switch.vue'; +import FormInput from '@/components/form/input.vue'; +import FormTextarea from '@/components/form/textarea.vue'; +import FormInfo from '@/components/ui/info.vue'; +import FormSection from '@/components/form/section.vue'; +import FormSplit from '@/components/form/split.vue'; +import FormSuspense from '@/components/form/suspense.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; import { fetchInstance } from '@/instance'; @@ -75,12 +149,11 @@ export default defineComponent({ components: { FormSwitch, FormInput, - FormBase, - FormGroup, - FormButton, + FormSuspense, FormTextarea, FormInfo, - FormSuspense, + FormSection, + FormSplit, }, emits: ['info'], @@ -91,6 +164,12 @@ export default defineComponent({ title: this.$ts.general, icon: 'fas fa-cog', bg: 'var(--bg)', + actions: [{ + asFullButton: true, + icon: 'fas fa-check', + text: this.$ts.save, + handler: this.save, + }], }, name: null, description: null, @@ -104,13 +183,20 @@ export default defineComponent({ enableLocalTimeline: false, enableGlobalTimeline: false, pinnedUsers: '', + cacheRemoteFiles: false, + proxyRemoteFiles: false, + localDriveCapacityMb: 0, + remoteDriveCapacityMb: 0, + enableRegistration: false, + emailRequiredForSignup: false, + enableServiceWorker: false, + swPublicKey: null, + swPrivateKey: null, + deeplAuthKey: '', + deeplIsPro: false, } }, - async mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { async init() { const meta = await os.api('meta', { detail: true }); @@ -126,6 +212,17 @@ export default defineComponent({ this.enableLocalTimeline = !meta.disableLocalTimeline; this.enableGlobalTimeline = !meta.disableGlobalTimeline; this.pinnedUsers = meta.pinnedUsers.join('\n'); + this.cacheRemoteFiles = meta.cacheRemoteFiles; + this.proxyRemoteFiles = meta.proxyRemoteFiles; + this.localDriveCapacityMb = meta.driveCapacityPerLocalUserMb; + this.remoteDriveCapacityMb = meta.driveCapacityPerRemoteUserMb; + this.enableRegistration = !meta.disableRegistration; + this.emailRequiredForSignup = meta.emailRequiredForSignup; + this.enableServiceWorker = meta.enableServiceWorker; + this.swPublicKey = meta.swPublickey; + this.swPrivateKey = meta.swPrivateKey; + this.deeplAuthKey = meta.deeplAuthKey; + this.deeplIsPro = meta.deeplIsPro; }, save() { @@ -142,6 +239,17 @@ export default defineComponent({ disableLocalTimeline: !this.enableLocalTimeline, disableGlobalTimeline: !this.enableGlobalTimeline, pinnedUsers: this.pinnedUsers.split('\n'), + cacheRemoteFiles: this.cacheRemoteFiles, + proxyRemoteFiles: this.proxyRemoteFiles, + localDriveCapacityMb: parseInt(this.localDriveCapacityMb, 10), + remoteDriveCapacityMb: parseInt(this.remoteDriveCapacityMb, 10), + disableRegistration: !this.enableRegistration, + emailRequiredForSignup: this.emailRequiredForSignup, + enableServiceWorker: this.enableServiceWorker, + swPublicKey: this.swPublicKey, + swPrivateKey: this.swPrivateKey, + deeplAuthKey: this.deeplAuthKey, + deeplIsPro: this.deeplIsPro, }).then(() => { fetchInstance(); }); diff --git a/packages/client/src/pages/admin/users.vue b/packages/client/src/pages/admin/users.vue index e7a3437167..03e155ddcf 100644 --- a/packages/client/src/pages/admin/users.vue +++ b/packages/client/src/pages/admin/users.vue @@ -30,7 +30,7 @@ <template #prefix>@</template> <template #label>{{ $ts.username }}</template> </MkInput> - <MkInput v-model="searchHost" style="flex: 1;" type="text" spellcheck="false" :disabled="pagination.params().origin === 'local'" @update:modelValue="$refs.users.reload()"> + <MkInput v-model="searchHost" style="flex: 1;" type="text" spellcheck="false" :disabled="pagination.params.origin === 'local'" @update:modelValue="$refs.users.reload()"> <template #prefix>@</template> <template #label>{{ $ts.host }}</template> </MkInput> @@ -62,7 +62,7 @@ </template> <script lang="ts"> -import { defineComponent } from 'vue'; +import { computed, defineComponent } from 'vue'; import MkButton from '@/components/ui/button.vue'; import MkInput from '@/components/form/input.vue'; import MkSelect from '@/components/form/select.vue'; @@ -110,36 +110,20 @@ export default defineComponent({ searchUsername: '', searchHost: '', pagination: { - endpoint: 'admin/show-users', + endpoint: 'admin/show-users' as const, limit: 10, - params: () => ({ + params: computed(() => ({ 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, diff --git a/packages/client/src/pages/advanced-theme-editor.vue b/packages/client/src/pages/advanced-theme-editor.vue deleted file mode 100644 index 9c2423131c..0000000000 --- a/packages/client/src/pages/advanced-theme-editor.vue +++ /dev/null @@ -1,349 +0,0 @@ -<template> -<div class="t9makv94"> - <section class="_section"> - <div class="_content"> - <details> - <summary>{{ $ts.import }}</summary> - <MkTextarea v-model="themeToImport"> - {{ $ts._theme.importInfo }} - </MkTextarea> - <MkButton :disabled="!themeToImport.trim()" @click="importTheme">{{ $ts.import }}</MkButton> - </details> - </div> - </section> - <section class="_section"> - <div class="_content _card _gap"> - <div class="_content"> - <MkInput v-model="name" required><span>{{ $ts.name }}</span></MkInput> - <MkInput v-model="author" required><span>{{ $ts.author }}</span></MkInput> - <MkTextarea v-model="description"><span>{{ $ts.description }}</span></MkTextarea> - <div class="_inputs"> - <div v-text="$ts._theme.base" /> - <MkRadio v-model="baseTheme" value="light">{{ $ts.light }}</MkRadio> - <MkRadio v-model="baseTheme" value="dark">{{ $ts.dark }}</MkRadio> - </div> - </div> - </div> - <div class="_content _card _gap"> - <div class="list-view _content"> - <div v-for="([ k, v ], i) in theme" :key="k" class="item"> - <div class="_inputs"> - <div> - {{ k.startsWith('$') ? `${k} (${$ts._theme.constant})` : $t('_theme.keys.' + k) }} - <button v-if="k.startsWith('$')" class="_button _link" @click="del(i)" v-text="$ts.delete" /> - </div> - <div> - <div class="type" @click="chooseType($event, i)"> - {{ getTypeOf(v) }} <i class="fas fa-chevron-down"></i> - </div> - <!-- default --> - <div v-if="v === null" class="default-value" v-text="baseProps[k]" /> - <!-- color --> - <div v-else-if="typeof v === 'string'" class="color"> - <input type="color" :value="v" @input="colorChanged($event.target.value, i)"/> - <MkInput class="select" :value="v" @update:modelValue="colorChanged($event, i)"/> - </div> - <!-- ref const --> - <MkInput v-else-if="v.type === 'refConst'" v-model="v.key"> - <template #prefix>$</template> - <span>{{ $ts.name }}</span> - </MkInput> - <!-- ref props --> - <MkSelect v-else-if="v.type === 'refProp'" v-model="v.key" class="select"> - <option v-for="key in themeProps" :key="key" :value="key">{{ $t('_theme.keys.' + key) }}</option> - </MkSelect> - <!-- func --> - <template v-else-if="v.type === 'func'"> - <MkSelect v-model="v.name" class="select"> - <template #label>{{ $ts._theme.funcKind }}</template> - <option v-for="n in ['alpha', 'darken', 'lighten']" :key="n" :value="n">{{ $t('_theme.' + n) }}</option> - </MkSelect> - <MkInput v-model="v.arg" type="number"><span>{{ $ts._theme.argument }}</span></MkInput> - <MkSelect v-model="v.value" class="select"> - <template #label>{{ $ts._theme.basedProp }}</template> - <option v-for="key in themeProps" :key="key" :value="key">{{ $t('_theme.keys.' + key) }}</option> - </MkSelect> - </template> - <!-- CSS --> - <MkInput v-else-if="v.type === 'css'" v-model="v.value"> - <span>CSS</span> - </MkInput> - </div> - </div> - </div> - <MkButton primary @click="addConst">{{ $ts._theme.addConstant }}</MkButton> - </div> - </div> - </section> - <section class="_section"> - <details class="_content"> - <summary>{{ $ts.sample }}</summary> - <MkSample/> - </details> - </section> - <section class="_section"> - <div class="_content"> - <MkButton inline @click="preview">{{ $ts.preview }}</MkButton> - <MkButton inline primary :disabled="!name || !author" @click="save">{{ $ts.save }}</MkButton> - </div> - </section> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import * as JSON5 from 'json5'; -import { toUnicode } from 'punycode/'; - -import MkRadio from '@/components/form/radio.vue'; -import MkButton from '@/components/ui/button.vue'; -import MkInput from '@/components/form/input.vue'; -import MkTextarea from '@/components/form/textarea.vue'; -import MkSelect from '@/components/form/select.vue'; -import MkSample from '@/components/sample.vue'; - -import { convertToMisskeyTheme, ThemeValue, convertToViewModel, ThemeViewModel } from '@/scripts/theme-editor'; -import { Theme, applyTheme, lightTheme, darkTheme, themeProps, validateTheme } from '@/scripts/theme'; -import { host } from '@/config'; -import * as os from '@/os'; -import { ColdDeviceStorage } from '@/store'; -import { addTheme } from '@/theme-store'; -import * as symbols from '@/symbols'; - -export default defineComponent({ - components: { - MkRadio, - MkButton, - MkInput, - MkTextarea, - MkSelect, - MkSample, - }, - - async beforeRouteLeave(to, from, next) { - if (this.changed && !(await this.confirm())) { - next(false); - } else { - next(); - } - }, - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.themeEditor, - icon: 'fas fa-palette', - }, - theme: [] as ThemeViewModel, - name: '', - description: '', - baseTheme: 'light' as 'dark' | 'light', - author: `@${this.$i.username}@${toUnicode(host)}`, - themeToImport: '', - changed: false, - lightTheme, darkTheme, themeProps, - } - }, - - computed: { - baseProps() { - return this.baseTheme === 'light' ? this.lightTheme.props : this.darkTheme.props; - }, - }, - - beforeUnmount() { - window.removeEventListener('beforeunload', this.beforeunload); - }, - - mounted() { - this.init(); - window.addEventListener('beforeunload', this.beforeunload); - const changed = () => this.changed = true; - this.$watch('name', changed); - this.$watch('description', changed); - this.$watch('baseTheme', changed); - this.$watch('author', changed); - this.$watch('theme', changed); - }, - - methods: { - beforeunload(e: BeforeUnloadEvent) { - if (this.changed) { - e.preventDefault(); - e.returnValue = ''; - } - }, - - async confirm(): Promise<boolean> { - const { canceled } = await os.confirm({ - type: 'warning', - text: this.$ts.leaveConfirm, - }); - return !canceled; - }, - - init() { - const t: ThemeViewModel = []; - for (const key of themeProps) { - t.push([ key, null ]); - } - this.theme = t; - }, - - async del(i: number) { - const { canceled } = await os.confirm({ - type: 'warning', - text: this.$t('_theme.deleteConstantConfirm', { const: this.theme[i][0] }), - }); - if (canceled) return; - Vue.delete(this.theme, i); - }, - - async addConst() { - const { canceled, result } = await os.inputText({ - title: this.$ts._theme.inputConstantName, - }); - if (canceled) return; - this.theme.push([ '$' + result, '#000000']); - }, - - save() { - const theme = convertToMisskeyTheme(this.theme, this.name, this.description, this.author, this.baseTheme); - addTheme(theme); - os.alert({ - type: 'success', - text: this.$t('_theme.installed', { name: theme.name }) - }); - this.changed = false; - }, - - preview() { - const theme = convertToMisskeyTheme(this.theme, this.name, this.description, this.author, this.baseTheme); - try { - applyTheme(theme, false); - } catch (e) { - os.alert({ - type: 'error', - text: e.message - }); - } - }, - - async importTheme() { - if (this.changed && (!await this.confirm())) return; - - try { - const theme = JSON5.parse(this.themeToImport) as Theme; - if (!validateTheme(theme)) throw new Error(this.$ts._theme.invalid); - - this.name = theme.name; - this.description = theme.desc || ''; - this.author = theme.author; - this.baseTheme = theme.base || 'light'; - this.theme = convertToViewModel(theme); - this.themeToImport = ''; - } catch (e) { - os.alert({ - type: 'error', - text: e.message - }); - } - }, - - colorChanged(color: string, i: number) { - this.theme[i] = [this.theme[i][0], color]; - }, - - getTypeOf(v: ThemeValue) { - return v === null - ? this.$ts._theme.defaultValue - : typeof v === 'string' - ? this.$ts._theme.color - : this.$t('_theme.' + v.type); - }, - - async chooseType(e: MouseEvent, i: number) { - const newValue = await this.showTypeMenu(e); - this.theme[i] = [ this.theme[i][0], newValue ]; - }, - - showTypeMenu(e: MouseEvent) { - return new Promise<ThemeValue>((resolve) => { - os.popupMenu([{ - text: this.$ts._theme.defaultValue, - action: () => resolve(null), - }, { - text: this.$ts._theme.color, - action: () => resolve('#000000'), - }, { - text: this.$ts._theme.func, - action: () => resolve({ - type: 'func', name: 'alpha', arg: 1, value: 'accent' - }), - }, { - text: this.$ts._theme.refProp, - action: () => resolve({ - type: 'refProp', key: 'accent', - }), - }, { - text: this.$ts._theme.refConst, - action: () => resolve({ - type: 'refConst', key: '', - }), - }, { - text: 'CSS', - action: () => resolve({ - type: 'css', value: '', - }), - }], e.currentTarget || e.target); - }); - } - } -}); -</script> - -<style lang="scss" scoped> -.t9makv94 { - > ._section { - > ._content { - > .list-view { - > .item { - min-height: 48px; - word-break: break-all; - - &:not(:last-child) { - margin-bottom: 8px; - } - - .select { - margin: 24px 0; - } - - .type { - cursor: pointer; - } - - .default-value { - opacity: 0.6; - pointer-events: none; - user-select: none; - } - - .color { - > input { - display: inline-block; - width: 1.5em; - height: 1.5em; - } - - > div { - margin-left: 8px; - display: inline-block; - } - } - } - } - } - } -} -</style> diff --git a/packages/client/src/pages/announcements.vue b/packages/client/src/pages/announcements.vue index ca94640dda..53727823a4 100644 --- a/packages/client/src/pages/announcements.vue +++ b/packages/client/src/pages/announcements.vue @@ -36,7 +36,7 @@ export default defineComponent({ bg: 'var(--bg)', }, pagination: { - endpoint: 'announcements', + endpoint: 'announcements' as const, limit: 10, }, }; diff --git a/packages/client/src/pages/channel.vue b/packages/client/src/pages/channel.vue index 67ab2d8981..c9a8f36844 100644 --- a/packages/client/src/pages/channel.vue +++ b/packages/client/src/pages/channel.vue @@ -67,11 +67,11 @@ export default defineComponent({ channel: null, showBanner: true, pagination: { - endpoint: 'channels/timeline', + endpoint: 'channels/timeline' as const, limit: 10, - params: () => ({ + params: computed(() => ({ channelId: this.channelId, - }) + })) }, }; }, diff --git a/packages/client/src/pages/channels.vue b/packages/client/src/pages/channels.vue index 48877ab3ec..4e538a6da3 100644 --- a/packages/client/src/pages/channels.vue +++ b/packages/client/src/pages/channels.vue @@ -60,15 +60,15 @@ export default defineComponent({ })), tab: 'featured', featuredPagination: { - endpoint: 'channels/featured', + endpoint: 'channels/featured' as const, noPaging: true, }, followingPagination: { - endpoint: 'channels/followed', + endpoint: 'channels/followed' as const, limit: 5, }, ownedPagination: { - endpoint: 'channels/owned', + endpoint: 'channels/owned' as const, limit: 5, }, }; diff --git a/packages/client/src/pages/clip.vue b/packages/client/src/pages/clip.vue index 077a6ac8b5..6b49221d32 100644 --- a/packages/client/src/pages/clip.vue +++ b/packages/client/src/pages/clip.vue @@ -50,11 +50,11 @@ export default defineComponent({ } : null), clip: null, pagination: { - endpoint: 'clips/notes', + endpoint: 'clips/notes' as const, limit: 10, - params: () => ({ + params: computed(() => ({ clipId: this.clipId, - }) + })) }, }; }, diff --git a/packages/client/src/pages/drive.vue b/packages/client/src/pages/drive.vue index f30000367f..1e17bea0cc 100644 --- a/packages/client/src/pages/drive.vue +++ b/packages/client/src/pages/drive.vue @@ -4,27 +4,21 @@ </div> </template> -<script lang="ts"> -import { computed, defineComponent } from 'vue'; +<script lang="ts" setup> +import { computed } from 'vue'; import XDrive from '@/components/drive.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - XDrive - }, +let folder = $ref(null); - data() { - return { - [symbols.PAGE_INFO]: { - title: computed(() => this.folder ? this.folder.name : this.$ts.drive), - icon: 'fas fa-cloud', - bg: 'var(--bg)', - hideHeader: true, - }, - folder: null, - }; - }, +defineExpose({ + [symbols.PAGE_INFO]: computed(() => ({ + title: folder ? folder.name : i18n.locale.drive, + icon: 'fas fa-cloud', + bg: 'var(--bg)', + hideHeader: true, + })), }); </script> diff --git a/packages/client/src/pages/emojis.emoji.vue b/packages/client/src/pages/emojis.emoji.vue index 5dab72daea..83539ce7a3 100644 --- a/packages/client/src/pages/emojis.emoji.vue +++ b/packages/client/src/pages/emojis.emoji.vue @@ -8,35 +8,29 @@ </button> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; import * as os from '@/os'; import copyToClipboard from '@/scripts/copy-to-clipboard'; +import { i18n } from '@/i18n'; -export default defineComponent({ - props: { - emoji: { - type: Object, - required: true, - } - }, +const props = defineProps<{ + emoji: Record<string, unknown>; // TODO +}>(); - 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); +function menu(ev) { + os.popupMenu([{ + type: 'label', + text: ':' + props.emoji.name + ':', + }, { + text: i18n.locale.copy, + icon: 'fas fa-copy', + action: () => { + copyToClipboard(`:${props.emoji.name}:`); + os.success(); } - } -}); + }], ev.currentTarget || ev.target); +} </script> <style lang="scss" scoped> diff --git a/packages/client/src/pages/emojis.vue b/packages/client/src/pages/emojis.vue index 2adb5345e2..6577f5abd9 100644 --- a/packages/client/src/pages/emojis.vue +++ b/packages/client/src/pages/emojis.vue @@ -4,55 +4,47 @@ </div> </template> -<script lang="ts"> -import { defineComponent, computed } from 'vue'; +<script lang="ts" setup> +import { ref, computed } from 'vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; import XCategory from './emojis.category.vue'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - XCategory, - }, +const tab = ref('category'); - data() { - return { - [symbols.PAGE_INFO]: computed(() => ({ - title: this.$ts.customEmojis, - icon: 'fas fa-laugh', - bg: 'var(--bg)', - actions: [{ - icon: 'fas fa-ellipsis-h', - handler: this.menu - }], - })), - tab: 'category', +function menu(ev) { + os.popupMenu([{ + icon: 'fas fa-download', + text: i18n.locale.export, + action: async () => { + os.api('export-custom-emojis', { + }) + .then(() => { + os.alert({ + type: 'info', + text: i18n.locale.exportRequested, + }); + }).catch((e) => { + os.alert({ + type: 'error', + text: e.message, + }); + }); } - }, + }], ev.currentTarget || ev.target); +} - methods: { - menu(ev) { - os.popupMenu([{ - icon: 'fas fa-download', - text: this.$ts.export, - action: async () => { - os.api('export-custom-emojis', { - }) - .then(() => { - os.alert({ - type: 'info', - text: this.$ts.exportRequested, - }); - }).catch((e) => { - os.alert({ - type: 'error', - text: e.message, - }); - }); - } - }], ev.currentTarget || ev.target); - } - } +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.locale.customEmojis, + icon: 'fas fa-laugh', + bg: 'var(--bg)', + actions: [{ + icon: 'fas fa-ellipsis-h', + handler: menu, + }], + }, }); </script> diff --git a/packages/client/src/pages/explore.vue b/packages/client/src/pages/explore.vue index a3c3b771f2..04cc3662a7 100644 --- a/packages/client/src/pages/explore.vue +++ b/packages/client/src/pages/explore.vue @@ -156,7 +156,7 @@ export default defineComponent({ sort: '+createdAt', } }, searchPagination: { - endpoint: 'users/search', + endpoint: 'users/search' as const, limit: 10, params: computed(() => (this.searchQuery && this.searchQuery !== '') ? { query: this.searchQuery, @@ -178,7 +178,7 @@ export default defineComponent({ }, tagUsers(): any { return { - endpoint: 'hashtags/users', + endpoint: 'hashtags/users' as const, limit: 30, params: { tag: this.tag, diff --git a/packages/client/src/pages/favorites.vue b/packages/client/src/pages/favorites.vue index faab864744..8965b30d60 100644 --- a/packages/client/src/pages/favorites.vue +++ b/packages/client/src/pages/favorites.vue @@ -1,49 +1,49 @@ <template> -<div class="jmelgwjh"> - <div class="body"> - <XNotes class="notes" :pagination="pagination" :detail="true" :prop="'note'"/> - </div> -</div> +<MkSpacer :content-max="800"> + <MkPagination ref="pagingComponent" :pagination="pagination"> + <template #empty> + <div class="_fullinfo"> + <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> + <div>{{ $ts.noNotes }}</div> + </div> + </template> + + <template #default="{ items }"> + <XList v-slot="{ item }" :items="items" :direction="'down'" :no-gap="false" :ad="false"> + <XNote :key="item.id" :note="item.note" :class="$style.note"/> + </XList> + </template> + </MkPagination> +</MkSpacer> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; -import XNotes from '@/components/notes.vue'; -import * as os from '@/os'; +<script lang="ts" setup> +import { ref } from 'vue'; +import MkPagination from '@/components/ui/pagination.vue'; +import XNote from '@/components/note.vue'; +import XList from '@/components/date-separated-list.vue'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - XNotes - }, +const pagination = { + endpoint: 'i/favorites' as const, + limit: 10, +}; - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.favorites, - icon: 'fas fa-star', - bg: 'var(--bg)', - }, - pagination: { - endpoint: 'i/favorites', - limit: 10, - params: () => ({ - }) - }, - }; +const pagingComponent = ref<InstanceType<typeof MkPagination>>(); + +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.locale.favorites, + icon: 'fas fa-star', + bg: 'var(--bg)', }, }); </script> -<style lang="scss" scoped> -.jmelgwjh { - background: var(--bg); - - > .body { - box-sizing: border-box; - max-width: 800px; - margin: 0 auto; - padding: 16px; - } +<style lang="scss" module> +.note { + background: var(--panel); + border-radius: var(--radius); } </style> diff --git a/packages/client/src/pages/featured.vue b/packages/client/src/pages/featured.vue index 0844c0952f..725c70f0f7 100644 --- a/packages/client/src/pages/featured.vue +++ b/packages/client/src/pages/featured.vue @@ -4,29 +4,22 @@ </MkSpacer> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> import XNotes from '@/components/notes.vue'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - XNotes - }, +const pagination = { + endpoint: 'notes/featured' as const, + limit: 10, + offsetMode: true, +}; - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.featured, - icon: 'fas fa-fire-alt', - bg: 'var(--bg)', - }, - pagination: { - endpoint: 'notes/featured', - limit: 10, - offsetMode: true, - }, - }; +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.locale.featured, + icon: 'fas fa-fire-alt', + bg: 'var(--bg)', }, }); </script> diff --git a/packages/client/src/pages/federation.vue b/packages/client/src/pages/federation.vue index 4e5f428ff9..6a4a28b6b4 100644 --- a/packages/client/src/pages/federation.vue +++ b/packages/client/src/pages/federation.vue @@ -6,7 +6,7 @@ <template #prefix><i class="fas fa-search"></i></template> <template #label>{{ $ts.host }}</template> </MkInput> - <div class="_inputSplit" style="margin-top: var(--margin);"> + <FormSplit style="margin-top: var(--margin);"> <MkSelect v-model="state"> <template #label>{{ $ts.state }}</template> <option value="all">{{ $ts.all }}</option> @@ -38,7 +38,7 @@ <option value="+driveFiles">{{ $ts.driveFilesCount }} ({{ $ts.descendingOrder }})</option> <option value="-driveFiles">{{ $ts.driveFilesCount }} ({{ $ts.ascendingOrder }})</option> </MkSelect> - </div> + </FormSplit> </div> <MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination"> @@ -95,75 +95,50 @@ </MkSpacer> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { computed } from 'vue'; import MkButton from '@/components/ui/button.vue'; import MkInput from '@/components/form/input.vue'; import MkSelect from '@/components/form/select.vue'; import MkPagination from '@/components/ui/pagination.vue'; +import FormSplit from '@/components/form/split.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; -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 } : - {}) - }) - }, - } - }, +let host = $ref(''); +let state = $ref('federating'); +let sort = $ref('+pubSub'); +const pagination = { + endpoint: 'federation/instances' as const, + limit: 10, + offsetMode: true, + params: computed(() => ({ + sort: sort, + host: host != '' ? host : null, + ...( + state === 'federating' ? { federating: true } : + state === 'subscribing' ? { subscribing: true } : + state === 'publishing' ? { publishing: true } : + state === 'suspended' ? { suspended: true } : + state === 'blocked' ? { blocked: true } : + state === 'notResponding' ? { notResponding: true } : + {}) + })) +}; - watch: { - host() { - this.$refs.instances.reload(); - }, - state() { - this.$refs.instances.reload(); - } - }, +function getStatus(instance) { + if (instance.isSuspended) return 'suspended'; + if (instance.isNotResponding) return 'error'; + return 'alive'; +}; - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.locale.federation, + icon: 'fas fa-globe', + bg: 'var(--bg)', }, - - methods: { - getStatus(instance) { - if (instance.isSuspended) return 'suspended'; - if (instance.isNotResponding) return 'error'; - return 'alive'; - }, - } }); </script> diff --git a/packages/client/src/pages/follow-requests.vue b/packages/client/src/pages/follow-requests.vue index 54d695091d..764daa0d3e 100644 --- a/packages/client/src/pages/follow-requests.vue +++ b/packages/client/src/pages/follow-requests.vue @@ -1,6 +1,6 @@ <template> <div> - <MkPagination ref="list" :pagination="pagination" class="mk-follow-requests"> + <MkPagination ref="paginationComponent" :pagination="pagination"> <template #empty> <div class="_fullinfo"> <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> @@ -8,19 +8,21 @@ </div> </template> <template v-slot="{items}"> - <div v-for="req in items" :key="req.id" class="user _panel"> - <MkAvatar class="avatar" :user="req.follower" :show-indicator="true"/> - <div class="body"> - <div class="name"> - <MkA v-user-preview="req.follower.id" class="name" :to="userPage(req.follower)"><MkUserName :user="req.follower"/></MkA> - <p class="acct">@{{ acct(req.follower) }}</p> - </div> - <div v-if="req.follower.description" class="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 class="mk-follow-requests"> + <div v-for="req in items" :key="req.id" class="user _panel"> + <MkAvatar class="avatar" :user="req.follower" :show-indicator="true"/> + <div class="body"> + <div class="name"> + <MkA v-user-preview="req.follower.id" class="name" :to="userPage(req.follower)"><MkUserName :user="req.follower"/></MkA> + <p class="acct">@{{ acct(req.follower) }}</p> + </div> + <div v-if="req.follower.description" class="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> </div> @@ -29,45 +31,39 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { ref, computed } from 'vue'; import MkPagination from '@/components/ui/pagination.vue'; import { userPage, acct } from '@/filters/user'; import * as os from '@/os'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - MkPagination - }, +const paginationComponent = ref<InstanceType<typeof MkPagination>>(); - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.followRequests, - icon: 'fas fa-user-clock', - }, - pagination: { - endpoint: 'following/requests/list', - limit: 10, - }, - }; - }, +const pagination = { + endpoint: 'following/requests/list' as const, + 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 - } +function accept(user) { + os.api('following/requests/accept', { userId: user.id }).then(() => { + paginationComponent.value.reload(); + }); +} + +function reject(user) { + os.api('following/requests/reject', { userId: user.id }).then(() => { + paginationComponent.value.reload(); + }); +} + +defineExpose({ + [symbols.PAGE_INFO]: computed(() => ({ + title: i18n.locale.followRequests, + icon: 'fas fa-user-clock', + bg: 'var(--bg)', + })), }); </script> diff --git a/packages/client/src/pages/gallery/edit.vue b/packages/client/src/pages/gallery/edit.vue index caca6aed4b..e3fa1a0fcd 100644 --- a/packages/client/src/pages/gallery/edit.vue +++ b/packages/client/src/pages/gallery/edit.vue @@ -1,16 +1,16 @@ <template> -<FormBase> +<div> <FormSuspense :p="init"> <FormInput v-model="title"> - <span>{{ $ts.title }}</span> + <template #label>{{ $ts.title }}</template> </FormInput> <FormTextarea v-model="description" :max="500"> - <span>{{ $ts.description }}</span> + <template #label>{{ $ts.description }}</template> </FormTextarea> <FormGroup> - <div v-for="file in files" :key="file.id" class="_debobigegoItem _debobigegoPanel wqugxsfx" :style="{ backgroundImage: file ? `url(${ file.thumbnailUrl })` : null }"> + <div v-for="file in files" :key="file.id" class="_formGroup wqugxsfx" :style="{ backgroundImage: file ? `url(${ file.thumbnailUrl })` : null }"> <div class="name">{{ file.name }}</div> <button v-tooltip="$ts.remove" class="remove _button" @click="remove(file)"><i class="fas fa-times"></i></button> </div> @@ -24,19 +24,17 @@ <FormButton v-if="postId" danger @click="del"><i class="fas fa-trash-alt"></i> {{ $ts.delete }}</FormButton> </FormSuspense> -</FormBase> +</div> </template> <script lang="ts"> import { computed, defineComponent } from 'vue'; -import FormButton from '@/components/debobigego/button.vue'; -import FormInput from '@/components/debobigego/input.vue'; -import FormTextarea from '@/components/debobigego/textarea.vue'; -import FormSwitch from '@/components/debobigego/switch.vue'; -import FormTuple from '@/components/debobigego/tuple.vue'; -import FormBase from '@/components/debobigego/base.vue'; -import FormGroup from '@/components/debobigego/group.vue'; -import FormSuspense from '@/components/debobigego/suspense.vue'; +import FormButton from '@/components/ui/button.vue'; +import FormInput from '@/components/form/input.vue'; +import FormTextarea from '@/components/form/textarea.vue'; +import FormSwitch from '@/components/form/switch.vue'; +import FormGroup from '@/components/form/group.vue'; +import FormSuspense from '@/components/form/suspense.vue'; import { selectFiles } from '@/scripts/select-file'; import * as os from '@/os'; import * as symbols from '@/symbols'; @@ -47,7 +45,6 @@ export default defineComponent({ FormInput, FormTextarea, FormSwitch, - FormBase, FormGroup, FormSuspense, }, diff --git a/packages/client/src/pages/gallery/index.vue b/packages/client/src/pages/gallery/index.vue index cd0d2a40e4..a19d69d5c2 100644 --- a/packages/client/src/pages/gallery/index.vue +++ b/packages/client/src/pages/gallery/index.vue @@ -81,19 +81,19 @@ export default defineComponent({ }, tab: 'explore', recentPostsPagination: { - endpoint: 'gallery/posts', + endpoint: 'gallery/posts' as const, limit: 6, }, popularPostsPagination: { - endpoint: 'gallery/featured', + endpoint: 'gallery/featured' as const, limit: 5, }, myPostsPagination: { - endpoint: 'i/gallery/posts', + endpoint: 'i/gallery/posts' as const, limit: 5, }, likedPostsPagination: { - endpoint: 'i/gallery/likes', + endpoint: 'i/gallery/likes' as const, limit: 5, }, tags: [], @@ -106,7 +106,7 @@ export default defineComponent({ }, tagUsers(): any { return { - endpoint: 'hashtags/users', + endpoint: 'hashtags/users' as const, limit: 30, params: { tag: this.tag, diff --git a/packages/client/src/pages/gallery/post.vue b/packages/client/src/pages/gallery/post.vue index 096947e6f8..1755c23286 100644 --- a/packages/client/src/pages/gallery/post.vue +++ b/packages/client/src/pages/gallery/post.vue @@ -1,6 +1,6 @@ <template> <div class="_root"> - <transition name="fade" mode="out-in"> + <transition :name="$store.state.animation ? 'fade' : ''" mode="out-in"> <div v-if="post" class="rkxwuolj"> <div class="files"> <div v-for="file in post.files" :key="file.id" class="file"> @@ -93,11 +93,11 @@ export default defineComponent({ }] } : null), otherPostsPagination: { - endpoint: 'users/gallery/posts', + endpoint: 'users/gallery/posts' as const, limit: 6, - params: () => ({ + params: computed(() => ({ userId: this.post.user.id - }) + })), }, post: null, error: null, diff --git a/packages/client/src/pages/instance-info.vue b/packages/client/src/pages/instance-info.vue index 85096d991a..fa36db0659 100644 --- a/packages/client/src/pages/instance-info.vue +++ b/packages/client/src/pages/instance-info.vue @@ -1,70 +1,71 @@ <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" primary @click="info">{{ $ts.settings }}</FormButton> +<MkSpacer :content-max="600" :margin-min="16" :margin-max="32"> + <div v-if="instance" class="_formRoot"> + <div class="fnfelxur"> + <img :src="instance.iconUrl || instance.faviconUrl" alt="" class="icon"/> + </div> + <MkKeyValue :copy="host" oneline style="margin: 1em 0;"> + <template #key>Host</template> + <template #value><span class="_monospace"><MkLink :url="`https://${host}`">{{ host }}</MkLink></span></template> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> + <template #key>Name</template> + <template #value>{{ instance.name || `(${$ts.unknown})` }}</template> + </MkKeyValue> + <MkKeyValue> + <template #key>{{ $ts.description }}</template> + <template #value>{{ instance.description }}</template> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> + <template #key>{{ $ts.software }}</template> + <template #value><span class="_monospace">{{ instance.softwareName || `(${$ts.unknown})` }} / {{ instance.softwareVersion || `(${$ts.unknown})` }}</span></template> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> + <template #key>{{ $ts.administrator }}</template> + <template #value>{{ instance.maintainerName || `(${$ts.unknown})` }} ({{ instance.maintainerEmail || `(${$ts.unknown})` }})</template> + </MkKeyValue> - <FormTextarea readonly :value="instance.description"> - <span>{{ $ts.description }}</span> - </FormTextarea> + <FormSection v-if="iAmModerator"> + <template #label>Moderation</template> + <FormSwitch v-model="suspended" class="_formBlock" @update:modelValue="toggleSuspend">{{ $ts.stopActivityDelivery }}</FormSwitch> + <FormSwitch v-model="isBlocked" class="_formBlock" @update:modelValue="toggleBlock">{{ $ts.blockThisInstance }}</FormSwitch> + </FormSection> - <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> + <FormSection> + <MkKeyValue oneline style="margin: 1em 0;"> + <template #key>{{ $ts.registeredAt }}</template> + <template #value><MkTime mode="detail" :time="instance.caughtAt"/></template> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> + <template #key>{{ $ts.updatedAt }}</template> + <template #value><MkTime mode="detail" :time="instance.infoUpdatedAt"/></template> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> <template #key>{{ $ts.latestRequestSentAt }}</template> <template #value><MkTime v-if="instance.latestRequestSentAt" :time="instance.latestRequestSentAt"/><span v-else>N/A</span></template> - </FormKeyValueView> - <FormKeyValueView> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> <template #key>{{ $ts.latestStatus }}</template> <template #value>{{ instance.latestStatus ? instance.latestStatus : 'N/A' }}</template> - </FormKeyValueView> - <FormKeyValueView> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> <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> + </MkKeyValue> + </FormSection> + + <FormSection> + <MkKeyValue oneline style="margin: 1em 0;"> <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"> + </MkKeyValue> + </FormSection> + + <FormSection> + <template #label>{{ $ts.statistics }}</template> + <div class="cmhjzshl"> <div class="selects"> - <MkSelect v-model="chartSrc" style="margin: 0; flex: 1;"> + <MkSelect v-model="chartSrc" style="margin: 0 10px 0 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> @@ -83,147 +84,100 @@ </MkSelect> </div> <div class="chart"> - <MkChart :src="chartSrc" :span="chartSpan" :limit="90" :detailed="true"></MkChart> + <MkChart :src="chartSrc" :span="chartSpan" :limit="90" :args="{ host: host }" :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> + </FormSection> + + <MkObjectView tall :value="instance"> + </MkObjectView> + + <FormSection> <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 v-slot="{ result: dns }" :p="dnsPromiseFactory"> - <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> + <FormLink :to="`https://${host}/.well-known/host-meta`" external style="margin-bottom: 8px;">host-meta</FormLink> + <FormLink :to="`https://${host}/.well-known/host-meta.json`" external style="margin-bottom: 8px;">host-meta.json</FormLink> + <FormLink :to="`https://${host}/.well-known/nodeinfo`" external style="margin-bottom: 8px;">nodeinfo</FormLink> + <FormLink :to="`https://${host}/robots.txt`" external style="margin-bottom: 8px;">robots.txt</FormLink> + <FormLink :to="`https://${host}/manifest.json`" external style="margin-bottom: 8px;">manifest.json</FormLink> + </FormSection> + </div> +</MkSpacer> </template> -<script lang="ts"> -import { defineAsyncComponent, defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; +import * as misskey from 'misskey-js'; import MkChart from '@/components/chart.vue'; -import FormObjectView from '@/components/debobigego/object-view.vue'; -import FormTextarea from '@/components/debobigego/textarea.vue'; -import FormLink from '@/components/debobigego/link.vue'; -import FormBase from '@/components/debobigego/base.vue'; -import FormGroup from '@/components/debobigego/group.vue'; -import FormButton from '@/components/debobigego/button.vue'; -import FormKeyValueView from '@/components/debobigego/key-value-view.vue'; -import FormSuspense from '@/components/debobigego/suspense.vue'; +import MkObjectView from '@/components/object-view.vue'; +import FormLink from '@/components/form/link.vue'; +import MkLink from '@/components/link.vue'; +import FormSection from '@/components/form/section.vue'; +import MkKeyValue from '@/components/key-value.vue'; import MkSelect from '@/components/form/select.vue'; +import FormSwitch from '@/components/form/switch.vue'; import * as os from '@/os'; import number from '@/filters/number'; import bytes from '@/filters/bytes'; import * as symbols from '@/symbols'; -import MkInstanceInfo from '@/pages/admin/instance.vue'; +import { iAmModerator } from '@/account'; -export default defineComponent({ - components: { - FormBase, - FormTextarea, - FormObjectView, - FormButton, - FormLink, - FormGroup, - FormKeyValueView, - FormSuspense, - MkSelect, - MkChart, - }, +const props = defineProps<{ + host: string; +}>(); - props: { - host: { - type: String, - required: true - } - }, +let meta = $ref<misskey.entities.DetailedInstanceMetadata | null>(null); +let instance = $ref<misskey.entities.Instance | null>(null); +let suspended = $ref(false); +let isBlocked = $ref(false); +let chartSrc = $ref('instance-requests'); +let chartSpan = $ref('hour'); - 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', - } - }, +async function fetch() { + meta = await os.api('meta', { detail: true }); + instance = await os.api('federation/show-instance', { + host: props.host, + }); + suspended = instance.isSuspended; + isBlocked = meta.blockedHosts.includes(instance.host); +} - mounted() { - this.fetch(); - }, +async function toggleBlock(ev) { + if (meta == null) return; + await os.api('admin/update-meta', { + blockedHosts: isBlocked ? meta.blockedHosts.concat([instance.host]) : meta.blockedHosts.filter(x => x !== instance.host) + }); +} - methods: { - number, - bytes, +async function toggleSuspend(v) { + await os.api('admin/federation/update-instance', { + host: instance.host, + isSuspended: suspended, + }); +} - async fetch() { - this.instance = await os.api('federation/show-instance', { - host: this.host - }); - }, +fetch(); - info() { - os.popup(MkInstanceInfo, { - instance: this.instance - }, {}, 'closed'); - } - } +defineExpose({ + [symbols.PAGE_INFO]: { + title: props.host, + icon: 'fas fa-info-circle', + bg: 'var(--bg)', + actions: [{ + text: `https://${props.host}`, + icon: 'fas fa-external-link-alt', + handler: () => { + window.open(`https://${props.host}`, '_blank'); + } + }], + }, }); </script> <style lang="scss" scoped> .fnfelxur { - padding: 16px; - > .icon { display: block; - margin: auto; + margin: 0; height: 64px; border-radius: 8px; } @@ -232,7 +186,7 @@ export default defineComponent({ .cmhjzshl { > .selects { display: flex; - padding: 16px; + margin: 0 0 16px 0; } } </style> diff --git a/packages/client/src/pages/mentions.vue b/packages/client/src/pages/mentions.vue index 691d3bd9aa..bda56fc729 100644 --- a/packages/client/src/pages/mentions.vue +++ b/packages/client/src/pages/mentions.vue @@ -4,28 +4,21 @@ </MkSpacer> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> import XNotes from '@/components/notes.vue'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - XNotes - }, +const pagination = { + endpoint: 'notes/mentions' as const, + limit: 10, +}; - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.mentions, - icon: 'fas fa-at', - bg: 'var(--bg)', - }, - pagination: { - endpoint: 'notes/mentions', - limit: 10, - }, - }; +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.locale.mentions, + icon: 'fas fa-at', + bg: 'var(--bg)', }, }); </script> diff --git a/packages/client/src/pages/messages.vue b/packages/client/src/pages/messages.vue index 9085af9489..8efdc55586 100644 --- a/packages/client/src/pages/messages.vue +++ b/packages/client/src/pages/messages.vue @@ -4,31 +4,24 @@ </MkSpacer> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> import XNotes from '@/components/notes.vue'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - XNotes - }, +const pagination = { + endpoint: 'notes/mentions' as const, + limit: 10, + params: () => ({ + visibility: 'specified' + }), +}; - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.directNotes, - icon: 'fas fa-envelope', - bg: 'var(--bg)', - }, - pagination: { - endpoint: 'notes/mentions', - limit: 10, - params: () => ({ - visibility: 'specified' - }) - }, - }; +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.locale.directNotes, + icon: 'fas fa-envelope', + bg: 'var(--bg)', }, }); </script> diff --git a/packages/client/src/pages/messaging/index.vue b/packages/client/src/pages/messaging/index.vue index 01f9d4518f..554ebc4b6b 100644 --- a/packages/client/src/pages/messaging/index.vue +++ b/packages/client/src/pages/messaging/index.vue @@ -44,6 +44,7 @@ import * as Acct from 'misskey-js/built/acct'; import MkButton from '@/components/ui/button.vue'; import { acct } from '@/filters/user'; import * as os from '@/os'; +import { stream } from '@/stream'; import * as symbols from '@/symbols'; export default defineComponent({ @@ -66,7 +67,7 @@ export default defineComponent({ }, mounted() { - this.connection = markRaw(os.stream.useChannel('messagingIndex')); + this.connection = markRaw(stream.useChannel('messagingIndex')); this.connection.on('message', this.onMessage); this.connection.on('read', this.onRead); diff --git a/packages/client/src/pages/messaging/messaging-room.form.vue b/packages/client/src/pages/messaging/messaging-room.form.vue index 8d92c430f1..0fc7c8a5df 100644 --- a/packages/client/src/pages/messaging/messaging-room.form.vue +++ b/packages/client/src/pages/messaging/messaging-room.form.vue @@ -7,7 +7,7 @@ ref="text" v-model="text" :placeholder="$ts.inputMessageHere" - @keypress="onKeypress" + @keydown="onKeydown" @compositionupdate="onCompositionUpdate" @paste="onPaste" ></textarea> @@ -28,6 +28,7 @@ import * as autosize from 'autosize'; import { formatTimeString } from '@/scripts/format-time-string'; import { selectFile } from '@/scripts/select-file'; import * as os from '@/os'; +import { stream } from '@/stream'; import { Autocomplete } from '@/scripts/autocomplete'; import { throttle } from 'throttle-debounce'; @@ -48,7 +49,7 @@ export default defineComponent({ file: null, sending: false, typing: throttle(3000, () => { - os.stream.send('typingOnMessaging', this.user ? { partner: this.user.id } : { group: this.group.id }); + stream.send('typingOnMessaging', this.user ? { partner: this.user.id } : { group: this.group.id }); }), }; }, @@ -140,7 +141,7 @@ export default defineComponent({ //#endregion }, - onKeypress(e) { + onKeydown(e) { this.typing(); if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey) && this.canSend) { this.send(); diff --git a/packages/client/src/pages/messaging/messaging-room.vue b/packages/client/src/pages/messaging/messaging-room.vue index ffc7f7bc0d..a715dad6de 100644 --- a/packages/client/src/pages/messaging/messaging-room.vue +++ b/packages/client/src/pages/messaging/messaging-room.vue @@ -24,7 +24,7 @@ </I18n> <MkEllipsis/> </div> - <transition name="fade"> + <transition :name="$store.state.animation ? 'fade' : ''"> <div v-show="showIndicator" class="new-message"> <button class="_buttonPrimary" @click="onIndicatorClick"><i class="fas fa-arrow-circle-down"></i>{{ $ts.newMessageExists }}</button> </div> @@ -43,6 +43,7 @@ import XForm from './messaging-room.form.vue'; import * as Acct from 'misskey-js/built/acct'; import { isBottom, onScrollBottom, scroll } from '@/scripts/scroll'; import * as os from '@/os'; +import { stream } from '@/stream'; import { popout } from '@/scripts/popout'; import * as sound from '@/scripts/sound'; import * as symbols from '@/symbols'; @@ -141,7 +142,7 @@ const Component = defineComponent({ this.group = group; } - this.connection = markRaw(os.stream.useChannel('messaging', { + this.connection = markRaw(stream.useChannel('messaging', { otherparty: this.user ? this.user.id : undefined, group: this.group ? this.group.id : undefined, })); @@ -161,7 +162,7 @@ const Component = defineComponent({ // もっと見るの交差検知を発火させないためにfetchは // スクロールが終わるまでfalseにしておく // scrollendのようなイベントはないのでsetTimeoutで - setTimeout(() => this.fetching = false, 300); + window.setTimeout(() => this.fetching = false, 300); }); }, @@ -299,9 +300,9 @@ const Component = defineComponent({ this.showIndicator = false; }); - if (this.timer) clearTimeout(this.timer); + if (this.timer) window.clearTimeout(this.timer); - this.timer = setTimeout(() => { + this.timer = window.setTimeout(() => { this.showIndicator = false; }, 4000); }, diff --git a/packages/client/src/pages/my-antennas/create.vue b/packages/client/src/pages/my-antennas/create.vue index 173807475a..427c9935c3 100644 --- a/packages/client/src/pages/my-antennas/create.vue +++ b/packages/client/src/pages/my-antennas/create.vue @@ -4,45 +4,37 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; -import MkButton from '@/components/ui/button.vue'; +<script lang="ts" setup> +import { } from 'vue'; import XAntenna from './editor.vue'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; +import { router } from '@/router'; -export default defineComponent({ - components: { - MkButton, - XAntenna, - }, +let draft = $ref({ + name: '', + src: 'all', + userListId: null, + userGroupId: null, + users: [], + keywords: [], + excludeKeywords: [], + withReplies: false, + caseSensitive: false, + withFile: false, + notify: false +}); - 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 - }, - }; - }, +function onAntennaCreated() { + router.push('/my/antennas'); +} - methods: { - onAntennaCreated() { - this.$router.push('/my/antennas'); - }, - } +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.locale.manageAntennas, + icon: 'fas fa-satellite', + bg: 'var(--bg)', + }, }); </script> diff --git a/packages/client/src/pages/my-antennas/index.vue b/packages/client/src/pages/my-antennas/index.vue index d185e796c3..7138d269a9 100644 --- a/packages/client/src/pages/my-antennas/index.vue +++ b/packages/client/src/pages/my-antennas/index.vue @@ -38,7 +38,7 @@ export default defineComponent({ } }, pagination: { - endpoint: 'antennas/list', + endpoint: 'antennas/list' as const, limit: 10, }, }; diff --git a/packages/client/src/pages/my-clips/index.vue b/packages/client/src/pages/my-clips/index.vue index a5bbc3fd2d..97b563f6f8 100644 --- a/packages/client/src/pages/my-clips/index.vue +++ b/packages/client/src/pages/my-clips/index.vue @@ -3,7 +3,7 @@ <div class="qtcaoidl"> <MkButton primary class="add" @click="create"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton> - <MkPagination v-slot="{items}" ref="list" :pagination="pagination" class="list"> + <MkPagination v-slot="{items}" ref="pagingComponent" :pagination="pagination" 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> @@ -13,71 +13,64 @@ </MkSpacer> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; import MkPagination from '@/components/ui/pagination.vue'; import MkButton from '@/components/ui/button.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; +import i18n from '@/components/global/i18n'; -export default defineComponent({ - components: { - MkPagination, - MkButton, - }, - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.clip, - icon: 'fas fa-paperclip', - bg: 'var(--bg)', - action: { - icon: 'fas fa-plus', - handler: this.create - } - }, - pagination: { - endpoint: 'clips/list', - limit: 10, - }, - draft: null, - }; - }, +const pagination = { + endpoint: 'clips/list' as const, + limit: 10, +}; - 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; +const pagingComponent = $ref<InstanceType<typeof MkPagination>>(); - os.apiWithDialog('clips/create', result); +async function create() { + const { canceled, result } = await os.form(i18n.locale.createNewClip, { + name: { + type: 'string', + label: i18n.locale.name, }, - - onClipCreated() { - this.$refs.list.reload(); - this.draft = null; + description: { + type: 'string', + required: false, + multiline: true, + label: i18n.locale.description, }, + isPublic: { + type: 'boolean', + label: i18n.locale.public, + default: false, + }, + }); + if (canceled) return; + + os.apiWithDialog('clips/create', result); + + pagingComponent.reload(); +} + +function onClipCreated() { + pagingComponent.reload(); +} - onClipDeleted() { - this.$refs.list.reload(); +function onClipDeleted() { + pagingComponent.reload(); +} + +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.locale.clip, + icon: 'fas fa-paperclip', + bg: 'var(--bg)', + action: { + icon: 'fas fa-plus', + handler: create }, - } + }, }); </script> diff --git a/packages/client/src/pages/my-groups/group.vue b/packages/client/src/pages/my-groups/group.vue index c307f037a6..92c0483af9 100644 --- a/packages/client/src/pages/my-groups/group.vue +++ b/packages/client/src/pages/my-groups/group.vue @@ -1,6 +1,6 @@ <template> <div class="mk-group-page"> - <transition name="zoom" mode="out-in"> + <transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in"> <div v-if="group" class="_section"> <div class="_content" style="display: flex; gap: var(--margin); flex-wrap: wrap;"> <MkButton inline @click="invite()">{{ $ts.invite }}</MkButton> @@ -11,7 +11,7 @@ </div> </transition> - <transition name="zoom" mode="out-in"> + <transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in"> <div v-if="group" class="_section members _gap"> <div class="_title">{{ $ts.members }}</div> <div class="_content"> diff --git a/packages/client/src/pages/my-groups/index.vue b/packages/client/src/pages/my-groups/index.vue index db5ccde466..4b2b2963a8 100644 --- a/packages/client/src/pages/my-groups/index.vue +++ b/packages/client/src/pages/my-groups/index.vue @@ -87,15 +87,15 @@ export default defineComponent({ })), tab: 'owned', ownedPagination: { - endpoint: 'users/groups/owned', + endpoint: 'users/groups/owned' as const, limit: 10, }, joinedPagination: { - endpoint: 'users/groups/joined', + endpoint: 'users/groups/joined' as const, limit: 10, }, invitationPagination: { - endpoint: 'i/user-group-invites', + endpoint: 'i/user-group-invites' as const, limit: 10, }, }; diff --git a/packages/client/src/pages/my-lists/index.vue b/packages/client/src/pages/my-lists/index.vue index 94a869b9ff..e6fcba1b34 100644 --- a/packages/client/src/pages/my-lists/index.vue +++ b/packages/client/src/pages/my-lists/index.vue @@ -3,7 +3,7 @@ <div class="qkcjvfiv"> <MkButton primary class="add" @click="create"><i class="fas fa-plus"></i> {{ $ts.createList }}</MkButton> - <MkPagination v-slot="{items}" ref="list" :pagination="pagination" class="lists _content"> + <MkPagination v-slot="{items}" ref="pagingComponent" :pagination="pagination" class="lists _content"> <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"/> @@ -13,50 +13,41 @@ </MkSpacer> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; import MkPagination from '@/components/ui/pagination.vue'; import MkButton from '@/components/ui/button.vue'; import MkAvatars from '@/components/avatars.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - MkPagination, - MkButton, - MkAvatars, - }, +const pagingComponent = $ref<InstanceType<typeof MkPagination>>(); - 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, - }, - }; - }, +const pagination = { + endpoint: 'users/lists/list' as const, + limit: 10, +}; + +async function create() { + const { canceled, result: name } = await os.inputText({ + title: i18n.locale.enterListName, + }); + if (canceled) return; + await os.apiWithDialog('users/lists/create', { name: name }); + pagingComponent.reload(); +} - methods: { - async create() { - const { canceled, result: name } = await os.inputText({ - title: this.$ts.enterListName, - }); - if (canceled) return; - await os.api('users/lists/create', { name: name }); - this.$refs.list.reload(); - os.success(); +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.locale.manageLists, + icon: 'fas fa-list-ul', + bg: 'var(--bg)', + action: { + icon: 'fas fa-plus', + handler: create, }, - } + }, }); </script> diff --git a/packages/client/src/pages/my-lists/list.vue b/packages/client/src/pages/my-lists/list.vue index a25522f933..bc24f58431 100644 --- a/packages/client/src/pages/my-lists/list.vue +++ b/packages/client/src/pages/my-lists/list.vue @@ -1,7 +1,7 @@ <template> <MkSpacer :content-max="700"> <div class="mk-list-page"> - <transition name="zoom" mode="out-in"> + <transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in"> <div v-if="list" class="_section"> <div class="_content"> <MkButton inline @click="addUser()">{{ $ts.addUser }}</MkButton> @@ -11,7 +11,7 @@ </div> </transition> - <transition name="zoom" mode="out-in"> + <transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in"> <div v-if="list" class="_section members _gap"> <div class="_title">{{ $ts.members }}</div> <div class="_content"> diff --git a/packages/client/src/pages/not-found.vue b/packages/client/src/pages/not-found.vue index 92d3f399f7..914fdb9297 100644 --- a/packages/client/src/pages/not-found.vue +++ b/packages/client/src/pages/not-found.vue @@ -7,19 +7,15 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; -import * as os from '@/os'; +<script lang="ts" setup> import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; -export default defineComponent({ - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.notFound, - icon: 'fas fa-exclamation-triangle' - }, - } +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.locale.notFound, + icon: 'fas fa-exclamation-triangle', + bg: 'var(--bg)', }, }); </script> diff --git a/packages/client/src/pages/note.vue b/packages/client/src/pages/note.vue index d40082381c..efeea345dc 100644 --- a/packages/client/src/pages/note.vue +++ b/packages/client/src/pages/note.vue @@ -1,7 +1,7 @@ <template> <MkSpacer :content-max="800"> <div class="fcuexfpr"> - <transition name="fade" mode="out-in"> + <transition :name="$store.state.animation ? 'fade' : ''" mode="out-in"> <div v-if="note" class="note"> <div v-if="showNext" class="_gap"> <XNotes class="_content" :pagination="next" :no-gap="true"/> @@ -82,21 +82,21 @@ export default defineComponent({ showNext: false, error: null, prev: { - endpoint: 'users/notes', + endpoint: 'users/notes' as const, limit: 10, - params: init => ({ + params: computed(() => ({ userId: this.note.userId, untilId: this.note.id, - }) + })), }, next: { reversed: true, - endpoint: 'users/notes', + endpoint: 'users/notes' as const, limit: 10, - params: init => ({ + params: computed(() => ({ userId: this.note.userId, sinceId: this.note.id, - }) + })), }, }; }, diff --git a/packages/client/src/pages/notifications.vue b/packages/client/src/pages/notifications.vue index 695c54a535..090e80f99a 100644 --- a/packages/client/src/pages/notifications.vue +++ b/packages/client/src/pages/notifications.vue @@ -6,70 +6,62 @@ </MkSpacer> </template> -<script lang="ts"> -import { computed, defineComponent } from 'vue'; +<script lang="ts" setup> +import { computed } from 'vue'; import XNotifications from '@/components/notifications.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; import { notificationTypes } from 'misskey-js'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - XNotifications - }, +let tab = $ref('all'); +let includeTypes = $ref<string[] | null>(null); - 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: { - 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); +function setFilter(ev) { + const typeItems = notificationTypes.map(t => ({ + text: i18n.t(`_notification._types.${t}`), + active: includeTypes && includeTypes.includes(t), + action: () => { + includeTypes = [t]; + } + })); + const items = includeTypes != null ? [{ + icon: 'fas fa-times', + text: i18n.locale.clear, + action: () => { + includeTypes = null; } - } + }, null, ...typeItems] : typeItems; + os.popupMenu(items, ev.currentTarget || ev.target); +} + +defineExpose({ + [symbols.PAGE_INFO]: computed(() => ({ + title: i18n.locale.notifications, + icon: 'fas fa-bell', + bg: 'var(--bg)', + actions: [{ + text: i18n.locale.filter, + icon: 'fas fa-filter', + highlighted: includeTypes != null, + handler: setFilter, + }, { + text: i18n.locale.markAllAsRead, + icon: 'fas fa-check', + handler: () => { + os.apiWithDialog('notifications/mark-all-as-read'); + }, + }], + tabs: [{ + active: tab === 'all', + title: i18n.locale.all, + onClick: () => { tab = 'all'; }, + }, { + active: tab === 'unread', + title: i18n.locale.unread, + onClick: () => { tab = 'unread'; }, + },] + })), }); </script> diff --git a/packages/client/src/pages/page.vue b/packages/client/src/pages/page.vue index 3a4803c3a3..b2c039a269 100644 --- a/packages/client/src/pages/page.vue +++ b/packages/client/src/pages/page.vue @@ -1,6 +1,6 @@ <template> <MkSpacer :content-max="700"> - <transition name="fade" mode="out-in"> + <transition :name="$store.state.animation ? 'fade' : ''" mode="out-in"> <div v-if="page" :key="page.id" v-size="{ max: [450] }" class="xcukqgmh"> <div class="_block main"> <!-- @@ -106,11 +106,11 @@ export default defineComponent({ page: null, error: null, otherPostsPagination: { - endpoint: 'users/pages', + endpoint: 'users/pages' as const, limit: 6, - params: () => ({ + params: computed(() => ({ userId: this.page.user.id - }) + })), }, }; }, diff --git a/packages/client/src/pages/pages.vue b/packages/client/src/pages/pages.vue index f1dd64f119..dcccf7f7c4 100644 --- a/packages/client/src/pages/pages.vue +++ b/packages/client/src/pages/pages.vue @@ -62,15 +62,15 @@ export default defineComponent({ })), tab: 'featured', featuredPagesPagination: { - endpoint: 'pages/featured', + endpoint: 'pages/featured' as const, noPaging: true, }, myPagesPagination: { - endpoint: 'i/pages', + endpoint: 'i/pages' as const, limit: 5, }, likedPagesPagination: { - endpoint: 'i/page-likes', + endpoint: 'i/page-likes' as const, limit: 5, }, }; diff --git a/packages/client/src/pages/preview.vue b/packages/client/src/pages/preview.vue index 9d1ebb74ed..8eb4549516 100644 --- a/packages/client/src/pages/preview.vue +++ b/packages/client/src/pages/preview.vue @@ -4,24 +4,18 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { computed } from 'vue'; import MkSample from '@/components/sample.vue'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - MkSample, - }, - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.preview, - icon: 'fas fa-eye', - }, - } - }, +defineExpose({ + [symbols.PAGE_INFO]: computed(() => ({ + title: i18n.locale.preview, + icon: 'fas fa-eye', + bg: 'var(--bg)', + })), }); </script> diff --git a/packages/client/src/pages/reset-password.vue b/packages/client/src/pages/reset-password.vue index f9a2500840..8ef73858f6 100644 --- a/packages/client/src/pages/reset-password.vue +++ b/packages/client/src/pages/reset-password.vue @@ -1,67 +1,53 @@ <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> +<MkSpacer v-if="token" :content-max="700" :margin-min="16" :margin-max="32"> + <div class="_formRoot"> + <FormInput v-model="password" type="password" class="_formBlock"> + <template #prefix><i class="fas fa-lock"></i></template> + <template #label>{{ i18n.locale.newPassword }}</template> + </FormInput> + + <FormButton primary class="_formBlock" @click="save">{{ i18n.locale.save }}</FormButton> + </div> +</MkSpacer> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; -import FormLink from '@/components/debobigego/link.vue'; -import FormBase from '@/components/debobigego/base.vue'; -import FormGroup from '@/components/debobigego/group.vue'; -import FormInput from '@/components/debobigego/input.vue'; -import FormButton from '@/components/debobigego/button.vue'; +<script lang="ts" setup> +import { onMounted } from 'vue'; +import FormInput from '@/components/form/input.vue'; +import FormButton from '@/components/ui/button.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; +import { router } from '@/router'; -export default defineComponent({ - components: { - FormBase, - FormGroup, - FormLink, - FormInput, - FormButton, - }, - - props: { - token: { - type: String, - required: false - } - }, +const props = defineProps<{ + token?: string; +}>(); - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.resetPassword, - icon: 'fas fa-lock' - }, - password: '', - } - }, +let password = $ref(''); - mounted() { - if (this.token == null) { - os.popup(import('@/components/forgot-password.vue'), {}, {}, 'closed'); - this.$router.push('/'); - } - }, +async function save() { + await os.apiWithDialog('reset-password', { + token: props.token, + password: password, + }); + router.push('/'); +} - methods: { - async save() { - await os.apiWithDialog('reset-password', { - token: this.token, - password: this.password, - }); - this.$router.push('/'); - } +onMounted(() => { + if (props.token == null) { + os.popup(import('@/components/forgot-password.vue'), {}, {}, 'closed'); + router.push('/'); } }); + +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.locale.resetPassword, + icon: 'fas fa-lock', + bg: 'var(--bg)', + }, +}); </script> <style lang="scss" scoped> diff --git a/packages/client/src/pages/reversi/game.board.vue b/packages/client/src/pages/reversi/game.board.vue deleted file mode 100644 index eb6fef2799..0000000000 --- a/packages/client/src/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 v-if="!iAmPlayer && !game.isEnded" class="turn"> - <Mfm :key="'turn:' + turnUser().name" :text="$t('_reversi.turnOf', { name: turnUser().name })" :plain="true" :custom-emojis="turnUser().emojis"/> - <MkEllipsis/> - </p> - <p v-if="logPos != logs.length" class="turn"> - <Mfm :key="'past-turn-of:' + turnUser().name" :text="$t('_reversi.pastTurnOf', { name: turnUser().name })" :plain="true" :custom-emojis="turnUser().emojis"/> - </p> - <p v-if="iAmPlayer && !game.isEnded && !isMyTurn()" class="turn1">{{ $ts._reversi.opponentTurn }}<MkEllipsis/></p> - <p v-if="iAmPlayer && !game.isEnded && isMyTurn()" class="turn2" style="animation: tada 1s linear infinite both;">{{ $ts._reversi.myTurn }}</p> - <p v-if="game.isEnded && logPos == logs.length" class="result"> - <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 v-if="$store.state.gamesReversiShowBoardLabels" class="labels-x"> - <span v-for="i in game.map[0].length">{{ String.fromCharCode(64 + i) }}</span> - </div> - <div class="flex"> - <div v-if="$store.state.gamesReversiShowBoardLabels" class="labels-y"> - <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 }" - :title="`${String.fromCharCode(65 + o.transformPosToXy(i)[0])}${o.transformPosToXy(i)[1] + 1}`" - @click="set(i)" - > - <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 v-if="$store.state.gamesReversiShowBoardLabels" class="labels-y"> - <div v-for="i in game.map.length">{{ i }}</div> - </div> - </div> - <div v-if="$store.state.gamesReversiShowBoardLabels" class="labels-x"> - <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 v-if="!game.isEnded && iAmPlayer" class="actions"> - <MkButton inline @click="surrender">{{ $ts._reversi.surrender }}</MkButton> - </div> - - <div v-if="game.isEnded" class="player"> - <span>{{ logPos }} / {{ logs.length }}</span> - <div v-if="!autoplaying" class="buttons"> - <MkButton inline :disabled="logPos == 0" @click="logPos = 0"><i class="fas fa-angle-double-left"></i></MkButton> - <MkButton inline :disabled="logPos == 0" @click="logPos--"><i class="fas fa-angle-left"></i></MkButton> - <MkButton inline :disabled="logPos == logs.length" @click="logPos++"><i class="fas fa-angle-right"></i></MkButton> - <MkButton inline :disabled="logPos == logs.length" @click="logPos = logs.length"><i class="fas fa-angle-double-right"></i></MkButton> - </div> - <MkButton :disabled="autoplaying" style="margin: var(--margin) auto 0 auto;" @click="autoplay()"><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 '@/scripts/games/reversi/core'; -import { url } from '@/config'; -import MkButton from '@/components/ui/button.vue'; -import { userPage } from '@/filters/user'; -import * as os from '@/os'; -import * as sound from '@/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/packages/client/src/pages/reversi/game.setting.vue b/packages/client/src/pages/reversi/game.setting.vue deleted file mode 100644 index 28bc598cfd..0000000000 --- a/packages/client/src/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 v-if="mapName == '-Custom-'" label="-Custom-" :value="mapName"/> - <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 v-if="game.map == null" class="random"><i class="fas fa-dice"></i></div> - <div v-else class="board" :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 v-if="form" class="card form _panel"> - <header> - <span>{{ $ts._reversi.botSettings }}</span> - </header> - - <div> - <template v-for="item in form"> - <MkSwitch v-if="item.type == 'switch'" :key="item.id" v-model="item.value" @change="onChangeForm(item)">{{ item.label || item.desc || '' }}</MkSwitch> - - <div v-if="item.type == 'radio'" :key="item.id" class="card"> - <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 v-if="item.type == 'slider'" :key="item.id" class="card"> - <header> - <span>{{ item.label }}</span> - </header> - - <div> - <input v-model="item.value" type="range" :min="item.min" :max="item.max" :step="item.step || 1" @change="onChangeForm(item)"/> - </div> - </div> - - <div v-if="item.type == 'textbox'" :key="item.id" class="card"> - <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 v-if="!isAccepted" inline primary @click="accept">{{ $ts._reversi.ready }}</MkButton> - <MkButton v-if="isAccepted" inline primary @click="cancel">{{ $ts._reversi.cancelReady }}</MkButton> - </div> - </footer> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import * as maps from '@/scripts/games/reversi/maps'; -import MkButton from '@/components/ui/button.vue'; -import MkSwitch from '@/components/form/switch.vue'; -import MkRadio from '@/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/packages/client/src/pages/reversi/game.vue b/packages/client/src/pages/reversi/game.vue deleted file mode 100644 index b1ed632904..0000000000 --- a/packages/client/src/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 '@/os'; -import * as symbols from '@/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/packages/client/src/pages/reversi/index.vue b/packages/client/src/pages/reversi/index.vue deleted file mode 100644 index 0b118531fc..0000000000 --- a/packages/client/src/pages/reversi/index.vue +++ /dev/null @@ -1,279 +0,0 @@ -<template> -<div v-if="!matching" class="bgvwxkhb"> - <h1>Misskey {{ $ts._reversi.reversi }}</h1> - - <div class="play"> - <MkButton primary round style="margin: var(--margin) auto 0 auto;" @click="match">{{ $ts.invite }}</MkButton> - </div> - - <div class="_section"> - <MkFolder v-if="invitations.length > 0"> - <template #header>{{ $ts.invitations }}</template> - <div class="nfcacttm"> - <button v-for="invitation in invitations" class="invitation _panel _button" 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 v-for="g in myGames" :key="g.id" class="game _panel" tabindex="-1" :to="`/games/reversi/${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 v-for="g in games" :key="g.id" class="game _panel" tabindex="-1" :to="`/games/reversi/${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 v-else class="sazhgisb"> - <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 '@/os'; -import MkButton from '@/components/ui/button.vue'; -import MkFolder from '@/components/ui/folder.vue'; -import * as symbols from '@/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/packages/client/src/pages/room/preview.vue b/packages/client/src/pages/room/preview.vue deleted file mode 100644 index b0e600d4fb..0000000000 --- a/packages/client/src/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 '@/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/packages/client/src/pages/room/room.vue b/packages/client/src/pages/room/room.vue deleted file mode 100644 index eb85d39dc4..0000000000 --- a/packages/client/src/pages/room/room.vue +++ /dev/null @@ -1,279 +0,0 @@ -<template> -<div class="hveuntkp"> - <div v-if="objectSelected" class="controller _section"> - <div class="_content"> - <p class="name">{{ selectedFurnitureName }}</p> - <XPreview ref="preview"/> - <template v-if="selectedFurnitureInfo.props"> - <div v-for="k in Object.keys(selectedFurnitureInfo.props)" :key="k"> - <p>{{ k }}</p> - <template v-if="selectedFurnitureInfo.props[k] === 'image'"> - <MkButton @click="chooseImage(k, $event)">{{ $ts._rooms.chooseImage }}</MkButton> - </template> - <template v-else-if="selectedFurnitureInfo.props[k] === 'color'"> - <input type="color" :value="selectedFurnitureProps ? selectedFurnitureProps[k] : null" @change="updateColor(k, $event)"/> - </template> - </div> - </template> - </div> - <div class="_content"> - <MkButton inline :primary="isTranslateMode" @click="translate()"><i class="fas fa-arrows-alt"></i> {{ $ts._rooms.translate }}</MkButton> - <MkButton inline :primary="isRotateMode" @click="rotate()"><i class="fas fa-undo"></i> {{ $ts._rooms.rotate }}</MkButton> - <MkButton v-if="isTranslateMode || isRotateMode" inline @click="exit()"><i class="fas fa-ban"></i> {{ $ts._rooms.exit }}</MkButton> - </div> - <div class="_content"> - <MkButton @click="remove()"><i class="fas fa-trash-alt"></i> {{ $ts._rooms.remove }}</MkButton> - </div> - </div> - - <div v-if="isMyRoom" class="menu _section"> - <div class="_content"> - <MkButton @click="add()"><i class="fas fa-box-open"></i> {{ $ts._rooms.addFurniture }}</MkButton> - </div> - <div class="_content"> - <MkSelect :model-value="roomType" @update:modelValue="updateRoomType($event)"> - <template #label>{{ $ts._rooms.roomType }}</template> - <option value="default">{{ $ts._rooms._roomType.default }}</option> - <option value="washitsu">{{ $ts._rooms._roomType.washitsu }}</option> - </MkSelect> - <label v-if="roomType === 'default'"> - <span>{{ $ts._rooms.carpetColor }}</span> - <input type="color" :value="carpetColor" @change="updateCarpetColor($event)"/> - </label> - </div> - <div class="_content"> - <MkButton inline :disabled="!changed" primary @click="save()"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton> - <MkButton inline @click="clear()"><i class="fas fa-broom"></i> {{ $ts._rooms.clear }}</MkButton> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import { computed, defineComponent } from 'vue'; -import { Room } from '@/scripts/room/room'; -import * as Acct from 'misskey-js/built/acct'; -import XPreview from './preview.vue'; -const storeItems = require('@/scripts/room/furnitures.json5'); -import { query as urlQuery } from '@/scripts/url'; -import MkButton from '@/components/ui/button.vue'; -import MkSelect from '@/components/form/select.vue'; -import { selectFile } from '@/scripts/select-file'; -import * as os from '@/os'; -import { ColdDeviceStorage } from '@/store'; -import * as symbols from '@/symbols'; - -let room: Room; - -export default defineComponent({ - components: { - XPreview, - MkButton, - MkSelect, - }, - - beforeRouteLeave(to, from, next) { - if (this.changed) { - os.confirm({ - type: 'warning', - text: this.$ts.leaveConfirm, - }).then(({ canceled }) => { - if (canceled) { - next(false); - } else { - next(); - } - }); - } else { - next(); - } - }, - - props: { - acct: { - type: String, - required: true - }, - }, - - data() { - return { - [symbols.PAGE_INFO]: computed(() => this.user ? { - title: this.$ts.room, - avatar: this.user, - } : null), - user: null, - objectSelected: false, - selectedFurnitureName: null, - selectedFurnitureInfo: null, - selectedFurnitureProps: null, - roomType: null, - carpetColor: null, - isTranslateMode: false, - isRotateMode: false, - isMyRoom: false, - changed: false, - }; - }, - - async mounted() { - window.addEventListener('beforeunload', this.beforeunload); - - this.user = await os.api('users/show', { - ...Acct.parse(this.acct) - }); - - this.isMyRoom = this.$i && (this.$i.id === this.user.id); - - const roomInfo = await os.api('room/show', { - userId: this.user.id - }); - - this.roomType = roomInfo.roomType; - this.carpetColor = roomInfo.carpetColor; - - room = new Room(this.user, this.isMyRoom, roomInfo, this.$el, { - graphicsQuality: ColdDeviceStorage.get('roomGraphicsQuality'), - onChangeSelect: obj => { - this.objectSelected = obj != null; - if (obj) { - const f = room.findFurnitureById(obj.name); - this.selectedFurnitureName = this.$t('_rooms._furnitures.' + f.type); - this.selectedFurnitureInfo = storeItems.find(x => x.id === f.type); - this.selectedFurnitureProps = f.props - ? JSON.parse(JSON.stringify(f.props)) // Disable reactivity - : null; - this.$nextTick(() => { - this.$refs.preview.selected(obj); - }); - } - }, - useOrthographicCamera: ColdDeviceStorage.get('roomUseOrthographicCamera'), - }); - }, - - beforeUnmount() { - room.destroy(); - window.removeEventListener('beforeunload', this.beforeunload); - }, - - methods: { - beforeunload(e: BeforeUnloadEvent) { - if (this.changed) { - e.preventDefault(); - e.returnValue = ''; - } - }, - - async add() { - const { canceled, result: id } = await os.select({ - title: this.$ts._rooms.addFurniture, - items: storeItems.map(item => ({ - value: item.id, text: this.$t('_rooms._furnitures.' + item.id) - })) - }); - if (canceled) return; - room.addFurniture(id); - this.changed = true; - }, - - remove() { - this.isTranslateMode = false; - this.isRotateMode = false; - room.removeFurniture(); - this.changed = true; - }, - - save() { - os.api('room/update', { - room: room.getRoomInfo() - }).then(() => { - this.changed = false; - os.success(); - }).catch((e: any) => { - os.alert({ - type: 'error', - text: e.message - }); - }); - }, - - clear() { - os.confirm({ - type: 'warning', - text: this.$ts._rooms.clearConfirm, - }).then(({ canceled }) => { - if (canceled) return; - room.removeAllFurnitures(); - this.changed = true; - }); - }, - - chooseImage(key, e) { - selectFile(e.currentTarget || e.target, null).then(file => { - room.updateProp(key, `/proxy/?${urlQuery({ url: file.thumbnailUrl })}`); - this.$refs.preview.selected(room.getSelectedObject()); - this.changed = true; - }); - }, - - updateColor(key, ev) { - room.updateProp(key, ev.target.value); - this.$refs.preview.selected(room.getSelectedObject()); - this.changed = true; - }, - - updateCarpetColor(ev) { - room.updateCarpetColor(ev.target.value); - this.carpetColor = ev.target.value; - this.changed = true; - }, - - updateRoomType(type) { - room.changeRoomType(type); - this.roomType = type; - this.changed = true; - }, - - translate() { - if (this.isTranslateMode) { - this.exit(); - } else { - this.isRotateMode = false; - this.isTranslateMode = true; - room.enterTransformMode('translate'); - } - this.changed = true; - }, - - rotate() { - if (this.isRotateMode) { - this.exit(); - } else { - this.isTranslateMode = false; - this.isRotateMode = true; - room.enterTransformMode('rotate'); - } - this.changed = true; - }, - - exit() { - this.isTranslateMode = false; - this.isRotateMode = false; - room.exitTransformMode(); - this.changed = true; - } - } -}); -</script> - -<style lang="scss" scoped> -.hveuntkp { - position: relative; - min-height: 500px; - - > ::v-deep(canvas) { - display: block; - } -} -</style> diff --git a/packages/client/src/pages/search.vue b/packages/client/src/pages/search.vue index 85d19bb255..ce2b7035da 100644 --- a/packages/client/src/pages/search.vue +++ b/packages/client/src/pages/search.vue @@ -6,37 +6,31 @@ </div> </template> -<script lang="ts"> -import { computed, defineComponent } from 'vue'; +<script lang="ts" setup> +import { computed } from 'vue'; import XNotes from '@/components/notes.vue'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - XNotes - }, +const props = defineProps<{ + query: string; + channel?: string; +}>(); - 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, - }) - }, - }; - }, +const pagination = { + endpoint: 'notes/search' as const, + limit: 10, + params: computed(() => ({ + query: props.query, + channelId: props.channel, + })) +}; - watch: { - $route() { - (this.$refs.notes as any).reload(); - } - }, +defineExpose({ + [symbols.PAGE_INFO]: computed(() => ({ + title: i18n.t('searchWith', { q: props.query }), + icon: 'fas fa-search', + bg: 'var(--bg)', + })), }); </script> diff --git a/packages/client/src/pages/settings/2fa.vue b/packages/client/src/pages/settings/2fa.vue index cffd10a0ee..10599d99ff 100644 --- a/packages/client/src/pages/settings/2fa.vue +++ b/packages/client/src/pages/settings/2fa.vue @@ -71,9 +71,6 @@ import MkButton from '@/components/ui/button.vue'; import MkInfo from '@/components/ui/info.vue'; import MkInput from '@/components/form/input.vue'; import MkSwitch from '@/components/form/switch.vue'; -import FormBase from '@/components/debobigego/base.vue'; -import FormGroup from '@/components/debobigego/group.vue'; -import FormButton from '@/components/debobigego/button.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; diff --git a/packages/client/src/pages/settings/account-info.vue b/packages/client/src/pages/settings/account-info.vue index f3d5e2f2c3..c98ad056f6 100644 --- a/packages/client/src/pages/settings/account-info.vue +++ b/packages/client/src/pages/settings/account-info.vue @@ -1,144 +1,135 @@ <template> -<FormBase> - <FormKeyValueView> +<div class="_formRoot"> + <MkKeyValue> <template #key>ID</template> <template #value><span class="_monospace">{{ $i.id }}</span></template> - </FormKeyValueView> + </MkKeyValue> - <FormGroup> - <FormKeyValueView> + <FormSection> + <MkKeyValue> <template #key>{{ $ts.registeredDate }}</template> <template #value><MkTime :time="$i.createdAt" mode="detail"/></template> - </FormKeyValueView> - </FormGroup> + </MkKeyValue> + </FormSection> - <FormGroup v-if="stats"> + <FormSection v-if="stats"> <template #label>{{ $ts.statistics }}</template> - <FormKeyValueView> + <MkKeyValue oneline style="margin: 1em 0;"> <template #key>{{ $ts.notesCount }}</template> <template #value>{{ number(stats.notesCount) }}</template> - </FormKeyValueView> - <FormKeyValueView> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> <template #key>{{ $ts.repliesCount }}</template> <template #value>{{ number(stats.repliesCount) }}</template> - </FormKeyValueView> - <FormKeyValueView> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> <template #key>{{ $ts.renotesCount }}</template> <template #value>{{ number(stats.renotesCount) }}</template> - </FormKeyValueView> - <FormKeyValueView> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> <template #key>{{ $ts.repliedCount }}</template> <template #value>{{ number(stats.repliedCount) }}</template> - </FormKeyValueView> - <FormKeyValueView> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> <template #key>{{ $ts.renotedCount }}</template> <template #value>{{ number(stats.renotedCount) }}</template> - </FormKeyValueView> - <FormKeyValueView> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> <template #key>{{ $ts.pollVotesCount }}</template> <template #value>{{ number(stats.pollVotesCount) }}</template> - </FormKeyValueView> - <FormKeyValueView> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> <template #key>{{ $ts.pollVotedCount }}</template> <template #value>{{ number(stats.pollVotedCount) }}</template> - </FormKeyValueView> - <FormKeyValueView> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> <template #key>{{ $ts.sentReactionsCount }}</template> <template #value>{{ number(stats.sentReactionsCount) }}</template> - </FormKeyValueView> - <FormKeyValueView> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> <template #key>{{ $ts.receivedReactionsCount }}</template> <template #value>{{ number(stats.receivedReactionsCount) }}</template> - </FormKeyValueView> - <FormKeyValueView> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> <template #key>{{ $ts.noteFavoritesCount }}</template> <template #value>{{ number(stats.noteFavoritesCount) }}</template> - </FormKeyValueView> - <FormKeyValueView> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> <template #key>{{ $ts.followingCount }}</template> <template #value>{{ number(stats.followingCount) }}</template> - </FormKeyValueView> - <FormKeyValueView> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> <template #key>{{ $ts.followingCount }} ({{ $ts.local }})</template> <template #value>{{ number(stats.localFollowingCount) }}</template> - </FormKeyValueView> - <FormKeyValueView> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> <template #key>{{ $ts.followingCount }} ({{ $ts.remote }})</template> <template #value>{{ number(stats.remoteFollowingCount) }}</template> - </FormKeyValueView> - <FormKeyValueView> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> <template #key>{{ $ts.followersCount }}</template> <template #value>{{ number(stats.followersCount) }}</template> - </FormKeyValueView> - <FormKeyValueView> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> <template #key>{{ $ts.followersCount }} ({{ $ts.local }})</template> <template #value>{{ number(stats.localFollowersCount) }}</template> - </FormKeyValueView> - <FormKeyValueView> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> <template #key>{{ $ts.followersCount }} ({{ $ts.remote }})</template> <template #value>{{ number(stats.remoteFollowersCount) }}</template> - </FormKeyValueView> - <FormKeyValueView> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> <template #key>{{ $ts.pageLikesCount }}</template> <template #value>{{ number(stats.pageLikesCount) }}</template> - </FormKeyValueView> - <FormKeyValueView> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> <template #key>{{ $ts.pageLikedCount }}</template> <template #value>{{ number(stats.pageLikedCount) }}</template> - </FormKeyValueView> - <FormKeyValueView> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> <template #key>{{ $ts.driveFilesCount }}</template> <template #value>{{ number(stats.driveFilesCount) }}</template> - </FormKeyValueView> - <FormKeyValueView> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> <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> + </MkKeyValue> + </FormSection> - <FormGroup> + <FormSection> <template #label>{{ $ts.other }}</template> - <FormKeyValueView> + <MkKeyValue oneline style="margin: 1em 0;"> <template #key>emailVerified</template> <template #value>{{ $i.emailVerified ? $ts.yes : $ts.no }}</template> - </FormKeyValueView> - <FormKeyValueView> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> <template #key>twoFactorEnabled</template> <template #value>{{ $i.twoFactorEnabled ? $ts.yes : $ts.no }}</template> - </FormKeyValueView> - <FormKeyValueView> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> <template #key>securityKeys</template> <template #value>{{ $i.securityKeys ? $ts.yes : $ts.no }}</template> - </FormKeyValueView> - <FormKeyValueView> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> <template #key>usePasswordLessLogin</template> <template #value>{{ $i.usePasswordLessLogin ? $ts.yes : $ts.no }}</template> - </FormKeyValueView> - <FormKeyValueView> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> <template #key>isModerator</template> <template #value>{{ $i.isModerator ? $ts.yes : $ts.no }}</template> - </FormKeyValueView> - <FormKeyValueView> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> <template #key>isAdmin</template> <template #value>{{ $i.isAdmin ? $ts.yes : $ts.no }}</template> - </FormKeyValueView> - </FormGroup> -</FormBase> + </MkKeyValue> + </FormSection> +</div> </template> <script lang="ts"> import { defineAsyncComponent, defineComponent } from 'vue'; -import FormSwitch from '@/components/form/switch.vue'; -import FormSelect from '@/components/form/select.vue'; -import FormLink from '@/components/debobigego/link.vue'; -import FormBase from '@/components/debobigego/base.vue'; -import FormGroup from '@/components/debobigego/group.vue'; -import FormButton from '@/components/debobigego/button.vue'; -import FormKeyValueView from '@/components/debobigego/key-value-view.vue'; +import FormSection from '@/components/form/section.vue'; +import MkKeyValue from '@/components/key-value.vue'; import * as os from '@/os'; import number from '@/filters/number'; import bytes from '@/filters/bytes'; @@ -146,13 +137,8 @@ import * as symbols from '@/symbols'; export default defineComponent({ components: { - FormBase, - FormSelect, - FormSwitch, - FormButton, - FormLink, - FormGroup, - FormKeyValueView, + FormSection, + MkKeyValue, }, emits: ['info'], @@ -168,8 +154,6 @@ export default defineComponent({ }, mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - os.api('users/stats', { userId: this.$i.id }).then(stats => { diff --git a/packages/client/src/pages/settings/accounts.vue b/packages/client/src/pages/settings/accounts.vue index 2d1e0eff4e..c795ede8ac 100644 --- a/packages/client/src/pages/settings/accounts.vue +++ b/packages/client/src/pages/settings/accounts.vue @@ -1,41 +1,35 @@ <template> -<FormBase> +<div class="_formRoot"> <FormSuspense :p="init"> <FormButton primary @click="addAccount"><i class="fas fa-plus"></i> {{ $ts.addAccount }}</FormButton> - <div v-for="account in accounts" :key="account.id" class="_debobigegoItem _button" @click="menu(account, $event)"> - <div class="_debobigegoPanel lcjjdxlm"> - <div class="avatar"> - <MkAvatar :user="account" class="avatar"/> + <div v-for="account in accounts" :key="account.id" class="_panel _button lcjjdxlm" @click="menu(account, $event)"> + <div class="avatar"> + <MkAvatar :user="account" class="avatar"/> + </div> + <div class="body"> + <div class="name"> + <MkUserName :user="account"/> </div> - <div class="body"> - <div class="name"> - <MkUserName :user="account"/> - </div> - <div class="acct"> - <MkAcct :user="account"/> - </div> + <div class="acct"> + <MkAcct :user="account"/> </div> </div> </div> </FormSuspense> -</FormBase> +</div> </template> <script lang="ts"> import { defineComponent } from 'vue'; -import FormSuspense from '@/components/debobigego/suspense.vue'; -import FormLink from '@/components/debobigego/link.vue'; -import FormBase from '@/components/debobigego/base.vue'; -import FormGroup from '@/components/debobigego/group.vue'; -import FormButton from '@/components/debobigego/button.vue'; +import FormSuspense from '@/components/form/suspense.vue'; +import FormButton from '@/components/ui/button.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; import { getAccounts, addAccount, login } from '@/account'; export default defineComponent({ components: { - FormBase, FormSuspense, FormButton, }, @@ -59,10 +53,6 @@ export default defineComponent({ }; }, - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { menu(account, ev) { os.popupMenu([{ diff --git a/packages/client/src/pages/settings/api.vue b/packages/client/src/pages/settings/api.vue index 30a4902a15..20ff2a8d96 100644 --- a/packages/client/src/pages/settings/api.vue +++ b/packages/client/src/pages/settings/api.vue @@ -1,25 +1,20 @@ <template> -<FormBase> - <FormButton primary @click="generateToken">{{ $ts.generateAccessToken }}</FormButton> - <FormLink to="/settings/apps">{{ $ts.manageAccessTokens }}</FormLink> - <FormLink to="/api-console" :behavior="isDesktop ? 'window' : null">API console</FormLink> -</FormBase> +<div class="_formRoot"> + <FormButton primary class="_formBlock" @click="generateToken">{{ $ts.generateAccessToken }}</FormButton> + <FormLink to="/settings/apps" class="_formBlock">{{ $ts.manageAccessTokens }}</FormLink> + <FormLink to="/api-console" :behavior="isDesktop ? 'window' : null" class="_formBlock">API console</FormLink> +</div> </template> <script lang="ts"> import { defineComponent } from 'vue'; -import FormSwitch from '@/components/form/switch.vue'; -import FormSelect from '@/components/form/select.vue'; -import FormLink from '@/components/debobigego/link.vue'; -import FormBase from '@/components/debobigego/base.vue'; -import FormGroup from '@/components/debobigego/group.vue'; -import FormButton from '@/components/debobigego/button.vue'; +import FormLink from '@/components/form/link.vue'; +import FormButton from '@/components/ui/button.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; export default defineComponent({ components: { - FormBase, FormButton, FormLink, }, @@ -37,10 +32,6 @@ export default defineComponent({ }; }, - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { generateToken() { os.popup(import('@/components/token-generate-window.vue'), {}, { diff --git a/packages/client/src/pages/settings/apps.vue b/packages/client/src/pages/settings/apps.vue index b5fe4e0aed..9c0fa8a54d 100644 --- a/packages/client/src/pages/settings/apps.vue +++ b/packages/client/src/pages/settings/apps.vue @@ -1,5 +1,5 @@ <template> -<FormBase> +<div class="_formRoot"> <FormPagination ref="list" :pagination="pagination"> <template #empty> <div class="_fullinfo"> @@ -8,7 +8,7 @@ </div> </template> <template v-slot="{items}"> - <div v-for="token in items" :key="token.id" class="_debobigegoPanel bfomjevm"> + <div v-for="token in items" :key="token.id" class="_panel bfomjevm"> <img v-if="token.iconUrl" class="icon" :src="token.iconUrl" alt=""/> <div class="body"> <div class="name">{{ token.name }}</div> @@ -34,23 +34,17 @@ </div> </template> </FormPagination> -</FormBase> +</div> </template> <script lang="ts"> import { defineComponent } from 'vue'; -import FormPagination from '@/components/debobigego/pagination.vue'; -import FormSelect from '@/components/form/select.vue'; -import FormLink from '@/components/debobigego/link.vue'; -import FormBase from '@/components/debobigego/base.vue'; -import FormGroup from '@/components/debobigego/group.vue'; -import FormButton from '@/components/debobigego/button.vue'; +import FormPagination from '@/components/ui/pagination.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; export default defineComponent({ components: { - FormBase, FormPagination, }, @@ -64,7 +58,7 @@ export default defineComponent({ bg: 'var(--bg)', }, pagination: { - endpoint: 'i/apps', + endpoint: 'i/apps' as const, limit: 100, params: { sort: '+lastUsedAt' @@ -73,10 +67,6 @@ export default defineComponent({ }; }, - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { revoke(token) { os.api('i/revoke-token', { tokenId: token.id }).then(() => { diff --git a/packages/client/src/pages/settings/custom-css.vue b/packages/client/src/pages/settings/custom-css.vue index 155956923c..556ee30c1d 100644 --- a/packages/client/src/pages/settings/custom-css.vue +++ b/packages/client/src/pages/settings/custom-css.vue @@ -1,25 +1,18 @@ <template> -<FormBase> - <FormInfo warn>{{ $ts.customCssWarn }}</FormInfo> +<div class="_formRoot"> + <FormInfo warn class="_formBlock">{{ $ts.customCssWarn }}</FormInfo> - <FormTextarea v-model="localCustomCss" manual-save tall class="_monospace" style="tab-size: 2;"> - <span>{{ $ts.local }}</span> + <FormTextarea v-model="localCustomCss" manual-save tall class="_monospace _formBlock" style="tab-size: 2;"> + <template #label>CSS</template> </FormTextarea> -</FormBase> +</div> </template> <script lang="ts"> import { defineComponent } from 'vue'; import FormTextarea from '@/components/form/textarea.vue'; -import FormSelect from '@/components/form/select.vue'; -import FormRadios from '@/components/form/radios.vue'; -import FormBase from '@/components/debobigego/base.vue'; -import FormGroup from '@/components/debobigego/group.vue'; -import FormLink from '@/components/debobigego/link.vue'; -import FormButton from '@/components/debobigego/button.vue'; -import FormInfo from '@/components/debobigego/info.vue'; +import FormInfo from '@/components/ui/info.vue'; import * as os from '@/os'; -import { ColdDeviceStorage } from '@/store'; import { unisonReload } from '@/scripts/unison-reload'; import * as symbols from '@/symbols'; import { defaultStore } from '@/store'; @@ -27,12 +20,6 @@ import { defaultStore } from '@/store'; export default defineComponent({ components: { FormTextarea, - FormSelect, - FormRadios, - FormBase, - FormGroup, - FormLink, - FormButton, FormInfo, }, @@ -50,8 +37,6 @@ export default defineComponent({ }, mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - this.$watch('localCustomCss', this.apply); }, diff --git a/packages/client/src/pages/settings/deck.vue b/packages/client/src/pages/settings/deck.vue index bc82b0ca84..46b90d3d1a 100644 --- a/packages/client/src/pages/settings/deck.vue +++ b/packages/client/src/pages/settings/deck.vue @@ -1,42 +1,41 @@ <template> -<FormBase> +<div class="_formRoot"> <FormGroup> <template #label>{{ $ts.defaultNavigationBehaviour }}</template> <FormSwitch v-model="navWindow">{{ $ts.openInWindow }}</FormSwitch> </FormGroup> - <FormSwitch v-model="alwaysShowMainColumn">{{ $ts._deck.alwaysShowMainColumn }}</FormSwitch> + <FormSwitch v-model="alwaysShowMainColumn" class="_formBlock">{{ $ts._deck.alwaysShowMainColumn }}</FormSwitch> - <FormRadios v-model="columnAlign"> - <template #desc>{{ $ts._deck.columnAlign }}</template> + <FormRadios v-model="columnAlign" class="_formBlock"> + <template #label>{{ $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> + <FormRadios v-model="columnHeaderHeight" class="_formBlock"> + <template #label>{{ $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> + <FormInput v-model="columnMargin" type="number" class="_formBlock"> + <template #label>{{ $ts._deck.columnMargin }}</template> <template #suffix>px</template> </FormInput> - <FormLink @click="setProfile">{{ $ts._deck.profile }}<template #suffix>{{ profile }}</template></FormLink> -</FormBase> + <FormLink class="_formBlock" @click="setProfile">{{ $ts._deck.profile }}<template #suffix>{{ profile }}</template></FormLink> +</div> </template> <script lang="ts"> import { defineComponent } from 'vue'; -import FormSwitch from '@/components/debobigego/switch.vue'; -import FormLink from '@/components/debobigego/link.vue'; -import FormRadios from '@/components/debobigego/radios.vue'; -import FormInput from '@/components/debobigego/input.vue'; -import FormBase from '@/components/debobigego/base.vue'; -import FormGroup from '@/components/debobigego/group.vue'; +import FormSwitch from '@/components/form/switch.vue'; +import FormLink from '@/components/form/link.vue'; +import FormRadios from '@/components/form/radios.vue'; +import FormInput from '@/components/form/input.vue'; +import FormGroup from '@/components/form/group.vue'; import { deckStore } from '@/ui/deck/deck-store'; import * as os from '@/os'; import { unisonReload } from '@/scripts/unison-reload'; @@ -48,7 +47,6 @@ export default defineComponent({ FormLink, FormInput, FormRadios, - FormBase, FormGroup, }, @@ -85,10 +83,6 @@ export default defineComponent({ } }, - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { async setProfile() { const { canceled, result: name } = await os.inputText({ diff --git a/packages/client/src/pages/settings/delete-account.vue b/packages/client/src/pages/settings/delete-account.vue index 6ce8d6509c..7edc81a309 100644 --- a/packages/client/src/pages/settings/delete-account.vue +++ b/packages/client/src/pages/settings/delete-account.vue @@ -1,28 +1,23 @@ <template> -<FormBase> - <FormInfo warn>{{ $ts._accountDelete.mayTakeTime }}</FormInfo> - <FormInfo>{{ $ts._accountDelete.sendEmail }}</FormInfo> - <FormButton v-if="!$i.isDeleted" danger @click="deleteAccount">{{ $ts._accountDelete.requestAccountDelete }}</FormButton> +<div class="_formRoot"> + <FormInfo warn class="_formBlock">{{ $ts._accountDelete.mayTakeTime }}</FormInfo> + <FormInfo class="_formBlock">{{ $ts._accountDelete.sendEmail }}</FormInfo> + <FormButton v-if="!$i.isDeleted" danger class="_formBlock" @click="deleteAccount">{{ $ts._accountDelete.requestAccountDelete }}</FormButton> <FormButton v-else disabled>{{ $ts._accountDelete.inProgress }}</FormButton> -</FormBase> +</div> </template> <script lang="ts"> import { defineAsyncComponent, defineComponent } from 'vue'; -import FormInfo from '@/components/debobigego/info.vue'; -import FormBase from '@/components/debobigego/base.vue'; -import FormGroup from '@/components/debobigego/group.vue'; -import FormButton from '@/components/debobigego/button.vue'; +import FormInfo from '@/components/ui/info.vue'; +import FormButton from '@/components/ui/button.vue'; import * as os from '@/os'; -import { debug } from '@/config'; import { signout } from '@/account'; import * as symbols from '@/symbols'; export default defineComponent({ components: { - FormBase, FormButton, - FormGroup, FormInfo, }, @@ -35,14 +30,9 @@ export default defineComponent({ icon: 'fas fa-exclamation-triangle', bg: 'var(--bg)', }, - debug, } }, - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { async deleteAccount() { { diff --git a/packages/client/src/pages/settings/drive.vue b/packages/client/src/pages/settings/drive.vue index 9ab99c6efe..f1016ebd84 100644 --- a/packages/client/src/pages/settings/drive.vue +++ b/packages/client/src/pages/settings/drive.vue @@ -5,7 +5,7 @@ <div class="_formBlock uawsfosz"> <div class="meter"><div :style="meterStyle"></div></div> </div> - <div class="_inputSplit _formBlock"> + <FormSplit> <MkKeyValue class="_formBlock"> <template #key>{{ $ts.capacity }}</template> <template #value>{{ bytes(capacity, 1) }}</template> @@ -14,7 +14,7 @@ <template #key>{{ $ts.inUse }}</template> <template #value>{{ bytes(usage, 1) }}</template> </MkKeyValue> - </div> + </FormSplit> </FormSection> <FormSection> @@ -38,6 +38,7 @@ import * as tinycolor from 'tinycolor2'; import FormLink from '@/components/form/link.vue'; import FormSection from '@/components/form/section.vue'; import MkKeyValue from '@/components/key-value.vue'; +import FormSplit from '@/components/form/split.vue'; import * as os from '@/os'; import bytes from '@/filters/bytes'; import * as symbols from '@/symbols'; @@ -49,6 +50,7 @@ export default defineComponent({ FormLink, FormSection, MkKeyValue, + FormSplit, }, emits: ['info'], @@ -97,10 +99,6 @@ export default defineComponent({ } }, - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { chooseUploadFolder() { os.selectDriveFolder(false).then(async folder => { diff --git a/packages/client/src/pages/settings/email.vue b/packages/client/src/pages/settings/email.vue index b04295cce0..54557f8773 100644 --- a/packages/client/src/pages/settings/email.vue +++ b/packages/client/src/pages/settings/email.vue @@ -41,8 +41,6 @@ <script lang="ts"> import { defineComponent, onMounted, ref, watch } from 'vue'; -import FormButton from '@/components/debobigego/button.vue'; -import FormLink from '@/components/debobigego/link.vue'; import FormSection from '@/components/form/section.vue'; import FormInput from '@/components/form/input.vue'; import FormSwitch from '@/components/form/switch.vue'; @@ -54,8 +52,6 @@ import { i18n } from '@/i18n'; export default defineComponent({ components: { FormSection, - FormLink, - FormButton, FormSwitch, FormInput, }, @@ -115,8 +111,6 @@ export default defineComponent({ }); onMounted(() => { - context.emit('info', INFO); - watch(emailAddress, () => { saveEmailAddress(); }); diff --git a/packages/client/src/pages/settings/experimental-features.vue b/packages/client/src/pages/settings/experimental-features.vue deleted file mode 100644 index 5a7bcb3b41..0000000000 --- a/packages/client/src/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 '@/components/form/switch.vue'; -import FormSelect from '@/components/form/select.vue'; -import FormLink from '@/components/debobigego/link.vue'; -import FormBase from '@/components/debobigego/base.vue'; -import FormGroup from '@/components/debobigego/group.vue'; -import FormButton from '@/components/debobigego/button.vue'; -import FormKeyValueView from '@/components/debobigego/key-value-view.vue'; -import * as os from '@/os'; -import * as symbols from '@/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/packages/client/src/pages/settings/general.vue b/packages/client/src/pages/settings/general.vue index 734bc78442..2e159e56a9 100644 --- a/packages/client/src/pages/settings/general.vue +++ b/packages/client/src/pages/settings/general.vue @@ -195,10 +195,6 @@ export default defineComponent({ }, }, - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { async reloadAsk() { const { canceled } = await os.confirm({ diff --git a/packages/client/src/pages/settings/import-export.vue b/packages/client/src/pages/settings/import-export.vue index a1dd6a1539..21031c559e 100644 --- a/packages/client/src/pages/settings/import-export.vue +++ b/packages/client/src/pages/settings/import-export.vue @@ -133,10 +133,6 @@ export default defineComponent({ os.api('i/import-blocking', { fileId: file.id }).then(onImportSuccess).catch(onError); }; - onMounted(() => { - context.emit('info', INFO); - }); - return { [symbols.PAGE_INFO]: INFO, excludeMutingUsers, diff --git a/packages/client/src/pages/settings/index.vue b/packages/client/src/pages/settings/index.vue index 8ffff86705..66c8b147bb 100644 --- a/packages/client/src/pages/settings/index.vue +++ b/packages/client/src/pages/settings/index.vue @@ -14,7 +14,7 @@ </div> <div class="main"> <div class="bkzroven"> - <component :is="component" :key="page" v-bind="pageProps" @info="onInfo"/> + <component :is="component" :ref="el => pageChanged(el)" :key="page" v-bind="pageProps"/> </div> </div> </div> @@ -215,19 +215,9 @@ export default defineComponent({ 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')); } return null; }); @@ -235,17 +225,6 @@ export default defineComponent({ 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 }); }); @@ -271,8 +250,9 @@ export default defineComponent({ const emailNotConfigured = computed(() => instance.enableEmail && ($i.email == null || !$i.emailVerified)); - const onInfo = (info) => { - childInfo.value = info; + const pageChanged = (page) => { + if (page == null) return; + childInfo.value = page[symbols.PAGE_INFO]; }; return { @@ -285,7 +265,7 @@ export default defineComponent({ pageProps, component, emailNotConfigured, - onInfo, + pageChanged, childInfo, }; }, diff --git a/packages/client/src/pages/settings/instance-mute.vue b/packages/client/src/pages/settings/instance-mute.vue index 584a21e4bd..f84a209b60 100644 --- a/packages/client/src/pages/settings/instance-mute.vue +++ b/packages/client/src/pages/settings/instance-mute.vue @@ -47,11 +47,6 @@ export default defineComponent({ }, }, - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - async created() { this.instanceMutes = this.$i.mutedInstances.join('\n'); }, diff --git a/packages/client/src/pages/settings/integration.vue b/packages/client/src/pages/settings/integration.vue index 3d8aaf8a6f..ca36c91665 100644 --- a/packages/client/src/pages/settings/integration.vue +++ b/packages/client/src/pages/settings/integration.vue @@ -1,45 +1,39 @@ <template> -<FormBase> - <div v-if="enableTwitterIntegration" class="_debobigegoItem"> - <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" danger @click="disconnectTwitter">{{ $ts.disconnectService }}</MkButton> - <MkButton v-else primary @click="connectTwitter">{{ $ts.connectService }}</MkButton> - </div> - </div> +<div class="_formRoot"> + <FormSection v-if="enableTwitterIntegration"> + <template #label><i class="fab fa-twitter"></i> Twitter</template> + <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" danger @click="disconnectTwitter">{{ $ts.disconnectService }}</MkButton> + <MkButton v-else primary @click="connectTwitter">{{ $ts.connectService }}</MkButton> + </FormSection> - <div v-if="enableDiscordIntegration" class="_debobigegoItem"> - <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" danger @click="disconnectDiscord">{{ $ts.disconnectService }}</MkButton> - <MkButton v-else primary @click="connectDiscord">{{ $ts.connectService }}</MkButton> - </div> - </div> + <FormSection v-if="enableDiscordIntegration"> + <template #label><i class="fab fa-discord"></i> Discord</template> + <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" danger @click="disconnectDiscord">{{ $ts.disconnectService }}</MkButton> + <MkButton v-else primary @click="connectDiscord">{{ $ts.connectService }}</MkButton> + </FormSection> - <div v-if="enableGithubIntegration" class="_debobigegoItem"> - <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" danger @click="disconnectGithub">{{ $ts.disconnectService }}</MkButton> - <MkButton v-else primary @click="connectGithub">{{ $ts.connectService }}</MkButton> - </div> - </div> -</FormBase> + <FormSection v-if="enableGithubIntegration"> + <template #label><i class="fab fa-github"></i> GitHub</template> + <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" danger @click="disconnectGithub">{{ $ts.disconnectService }}</MkButton> + <MkButton v-else primary @click="connectGithub">{{ $ts.connectService }}</MkButton> + </FormSection> +</div> </template> <script lang="ts"> import { defineComponent } from 'vue'; import { apiUrl } from '@/config'; -import FormBase from '@/components/debobigego/base.vue'; +import FormSection from '@/components/form/section.vue'; import MkButton from '@/components/ui/button.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; export default defineComponent({ components: { - FormBase, + FormSection, MkButton }, @@ -79,8 +73,6 @@ export default defineComponent({ }, mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - document.cookie = `igi=${this.$i.token}; path=/;` + ` max-age=31536000;` + (document.location.protocol.startsWith('https') ? ' secure' : ''); diff --git a/packages/client/src/pages/settings/menu.vue b/packages/client/src/pages/settings/menu.vue index 19d26be89a..6e38cd5dfe 100644 --- a/packages/client/src/pages/settings/menu.vue +++ b/packages/client/src/pages/settings/menu.vue @@ -21,7 +21,6 @@ import { defineComponent } from 'vue'; import FormTextarea from '@/components/form/textarea.vue'; import FormRadios from '@/components/form/radios.vue'; -import FormBase from '@/components/debobigego/base.vue'; import FormButton from '@/components/ui/button.vue'; import * as os from '@/os'; import { menuDef } from '@/menu'; @@ -31,7 +30,6 @@ import { unisonReload } from '@/scripts/unison-reload'; export default defineComponent({ components: { - FormBase, FormButton, FormTextarea, FormRadios, @@ -69,10 +67,6 @@ export default defineComponent({ }, }, - 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)); diff --git a/packages/client/src/pages/settings/mute-block.vue b/packages/client/src/pages/settings/mute-block.vue index 4f42d5e429..f4f9ebf8dd 100644 --- a/packages/client/src/pages/settings/mute-block.vue +++ b/packages/client/src/pages/settings/mute-block.vue @@ -1,5 +1,5 @@ <template> -<FormBase> +<div class="_formRoot"> <MkTab v-model="tab" style="margin-bottom: var(--margin);"> <option value="mute">{{ $ts.mutedUsers }}</option> <option value="block">{{ $ts.blockedUsers }}</option> @@ -8,11 +8,9 @@ <MkPagination :pagination="mutingPagination" class="muting"> <template #empty><FormInfo>{{ $ts.noUsers }}</FormInfo></template> <template v-slot="{items}"> - <FormGroup> - <FormLink v-for="mute in items" :key="mute.id" :to="userPage(mute.mutee)"> - <MkAcct :user="mute.mutee"/> - </FormLink> - </FormGroup> + <FormLink v-for="mute in items" :key="mute.id" :to="userPage(mute.mutee)"> + <MkAcct :user="mute.mutee"/> + </FormLink> </template> </MkPagination> </div> @@ -20,66 +18,43 @@ <MkPagination :pagination="blockingPagination" class="blocking"> <template #empty><FormInfo>{{ $ts.noUsers }}</FormInfo></template> <template v-slot="{items}"> - <FormGroup> - <FormLink v-for="block in items" :key="block.id" :to="userPage(block.blockee)"> - <MkAcct :user="block.blockee"/> - </FormLink> - </FormGroup> + <FormLink v-for="block in items" :key="block.id" :to="userPage(block.blockee)"> + <MkAcct :user="block.blockee"/> + </FormLink> </template> </MkPagination> </div> -</FormBase> +</div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; import MkPagination from '@/components/ui/pagination.vue'; import MkTab from '@/components/tab.vue'; -import FormInfo from '@/components/debobigego/info.vue'; -import FormLink from '@/components/debobigego/link.vue'; -import FormBase from '@/components/debobigego/base.vue'; -import FormGroup from '@/components/debobigego/group.vue'; +import FormInfo from '@/components/ui/info.vue'; +import FormLink from '@/components/form/link.vue'; import { userPage } from '@/filters/user'; import * as os from '@/os'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - MkPagination, - MkTab, - FormInfo, - FormBase, - FormGroup, - FormLink, - }, +let tab = $ref('mute'); - emits: ['info'], +const mutingPagination = { + endpoint: 'mute/list' as const, + limit: 10, +}; - 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, - }, - } - }, +const blockingPagination = { + endpoint: 'blocking/list' as const, + limit: 10, +}; - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.locale.muteAndBlock, + icon: 'fas fa-ban', + bg: 'var(--bg)', }, - - methods: { - userPage - } }); </script> diff --git a/packages/client/src/pages/settings/notifications.vue b/packages/client/src/pages/settings/notifications.vue index d3ada0d7ef..12171530bb 100644 --- a/packages/client/src/pages/settings/notifications.vue +++ b/packages/client/src/pages/settings/notifications.vue @@ -13,7 +13,6 @@ import { defineComponent } from 'vue'; import FormButton from '@/components/ui/button.vue'; import FormLink from '@/components/form/link.vue'; -import FormBase from '@/components/debobigego/base.vue'; import FormSection from '@/components/form/section.vue'; import { notificationTypes } from 'misskey-js'; import * as os from '@/os'; @@ -21,7 +20,6 @@ import * as symbols from '@/symbols'; export default defineComponent({ components: { - FormBase, FormLink, FormButton, FormSection, @@ -39,10 +37,6 @@ export default defineComponent({ } }, - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { readAllUnreadNotes() { os.api('i/read-all-unread-notes'); diff --git a/packages/client/src/pages/settings/other.vue b/packages/client/src/pages/settings/other.vue index 0d9e60e21d..6e48cb58a6 100644 --- a/packages/client/src/pages/settings/other.vue +++ b/packages/client/src/pages/settings/other.vue @@ -1,30 +1,12 @@ <template> <div class="_formRoot"> - <FormLink to="/settings/update" class="_formBlock">Misskey Update</FormLink> - - <FormSwitch :value="$i.injectFeaturedNote" @update:modelValue="onChangeInjectFeaturedNote" class="_formBlock"> + <FormSwitch :value="$i.injectFeaturedNote" class="_formBlock" @update:modelValue="onChangeInjectFeaturedNote"> {{ $ts.showFeaturedNotesInTimeline }} </FormSwitch> - <FormSwitch v-model="reportError" class="_formBlock">{{ $ts.sendErrorReports }}<template #desc>{{ $ts.sendErrorReportsDescription }}</template></FormSwitch> + <FormSwitch v-model="reportError" class="_formBlock">{{ $ts.sendErrorReports }}<template #caption>{{ $ts.sendErrorReportsDescription }}</template></FormSwitch> <FormLink to="/settings/account-info" class="_formBlock">{{ $ts.accountInfo }}</FormLink> - <FormLink to="/settings/experimental-features" class="_formBlock">{{ $ts.experimentalFeatures }}</FormLink> - - <FormSection> - <template #label>{{ $ts.developer }}</template> - <FormSwitch v-model="debug" @update:modelValue="changeDebug" class="_formBlock"> - DEBUG MODE - </FormSwitch> - <template v-if="debug"> - <FormButton @click="taskmanager">Task Manager</FormButton> - </template> - </FormSection> - - <FormLink to="/settings/registry" class="_formBlock"><template #icon><i class="fas fa-cogs"></i></template>{{ $ts.registry }}</FormLink> - - <FormLink to="/bios" behavior="browser" class="_formBlock"><template #icon><i class="fas fa-door-open"></i></template>BIOS</FormLink> - <FormLink to="/cli" behavior="browser" class="_formBlock"><template #icon><i class="fas fa-door-open"></i></template>CLI</FormLink> <FormLink to="/settings/delete-account" class="_formBlock"><template #icon><i class="fas fa-exclamation-triangle"></i></template>{{ $ts.closeAccount }}</FormLink> </div> @@ -33,10 +15,8 @@ <script lang="ts"> import { defineAsyncComponent, defineComponent } from 'vue'; import FormSwitch from '@/components/form/switch.vue'; -import FormSelect from '@/components/form/select.vue'; import FormSection from '@/components/form/section.vue'; -import FormLink from '@/components/debobigego/link.vue'; -import FormButton from '@/components/debobigego/button.vue'; +import FormLink from '@/components/form/link.vue'; import * as os from '@/os'; import { debug } from '@/config'; import { defaultStore } from '@/store'; @@ -45,10 +25,8 @@ import * as symbols from '@/symbols'; export default defineComponent({ components: { - FormSelect, FormSection, FormSwitch, - FormButton, FormLink, }, @@ -69,10 +47,6 @@ export default defineComponent({ reportError: defaultStore.makeGetterSetter('reportError'), }, - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { changeDebug(v) { console.log(v); @@ -85,11 +59,6 @@ export default defineComponent({ injectFeaturedNote: v }); }, - - taskmanager() { - os.popup(import('@/components/taskmanager.vue'), { - }, {}, 'closed'); - }, } }); </script> diff --git a/packages/client/src/pages/settings/plugin.install.vue b/packages/client/src/pages/settings/plugin.install.vue index af93ef2930..d35d20d17a 100644 --- a/packages/client/src/pages/settings/plugin.install.vue +++ b/packages/client/src/pages/settings/plugin.install.vue @@ -1,15 +1,15 @@ <template> -<FormBase> - <FormInfo warn>{{ $ts._plugin.installWarn }}</FormInfo> +<div class="_formRoot"> + <FormInfo warn class="_formBlock">{{ $ts._plugin.installWarn }}</FormInfo> - <FormGroup> - <FormTextarea v-model="code" tall> - <span>{{ $ts.code }}</span> - </FormTextarea> - </FormGroup> + <FormTextarea v-model="code" tall class="_formBlock"> + <template #label>{{ $ts.code }}</template> + </FormTextarea> - <FormButton :disabled="code == null" primary inline @click="install"><i class="fas fa-check"></i> {{ $ts.install }}</FormButton> -</FormBase> + <div class="_formBlock"> + <FormButton :disabled="code == null" primary inline @click="install"><i class="fas fa-check"></i> {{ $ts.install }}</FormButton> + </div> +</div> </template> <script lang="ts"> @@ -18,13 +18,8 @@ import { AiScript, parse } from '@syuilo/aiscript'; import { serialize } from '@syuilo/aiscript/built/serializer'; import { v4 as uuid } from 'uuid'; import FormTextarea from '@/components/form/textarea.vue'; -import FormSelect from '@/components/form/select.vue'; -import FormRadios from '@/components/form/radios.vue'; -import FormBase from '@/components/debobigego/base.vue'; -import FormGroup from '@/components/debobigego/group.vue'; -import FormLink from '@/components/debobigego/link.vue'; -import FormButton from '@/components/debobigego/button.vue'; -import FormInfo from '@/components/debobigego/info.vue'; +import FormButton from '@/components/ui/button.vue'; +import FormInfo from '@/components/ui/info.vue'; import * as os from '@/os'; import { ColdDeviceStorage } from '@/store'; import { unisonReload } from '@/scripts/unison-reload'; @@ -33,11 +28,6 @@ import * as symbols from '@/symbols'; export default defineComponent({ components: { FormTextarea, - FormSelect, - FormRadios, - FormBase, - FormGroup, - FormLink, FormButton, FormInfo, }, @@ -55,10 +45,6 @@ export default defineComponent({ } }, - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { installPlugin({ id, meta, ast, token }) { ColdDeviceStorage.set('plugins', ColdDeviceStorage.get('plugins').concat({ diff --git a/packages/client/src/pages/settings/plugin.manage.vue b/packages/client/src/pages/settings/plugin.manage.vue deleted file mode 100644 index 8b9021dc3d..0000000000 --- a/packages/client/src/pages/settings/plugin.manage.vue +++ /dev/null @@ -1,116 +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 :modelValue="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 v-if="plugin.config" inline @click="config(plugin)"><i class="fas fa-cog"></i> {{ $ts.settings }}</MkButton> - <MkButton inline danger @click="uninstall(plugin)"><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 '@/components/ui/button.vue'; -import MkTextarea from '@/components/form/textarea.vue'; -import MkSelect from '@/components/form/select.vue'; -import FormSwitch from '@/components/form/switch.vue'; -import FormBase from '@/components/debobigego/base.vue'; -import FormGroup from '@/components/debobigego/group.vue'; -import * as os from '@/os'; -import { ColdDeviceStorage } from '@/store'; -import * as symbols from '@/symbols'; -import { unisonReload } from '@/scripts/unison-reload'; - -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/packages/client/src/pages/settings/plugin.vue b/packages/client/src/pages/settings/plugin.vue index 50e53f459f..7a3ab9d152 100644 --- a/packages/client/src/pages/settings/plugin.vue +++ b/packages/client/src/pages/settings/plugin.vue @@ -1,23 +1,54 @@ <template> -<FormBase> +<div class="_formRoot"> <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> + + <FormSection> + <template #label>{{ $ts.manage }}</template> + <div v-for="plugin in plugins" :key="plugin.id" class="_formBlock _panel" style="padding: 20px;"> + <span style="display: flex;"><b>{{ plugin.name }}</b><span style="margin-left: auto;">v{{ plugin.version }}</span></span> + + <FormSwitch class="_formBlock" :modelValue="plugin.active" @update:modelValue="changeActive(plugin, $event)">{{ $ts.makeActive }}</FormSwitch> + + <MkKeyValue class="_formBlock"> + <template #key>{{ $ts.author }}</template> + <template #value>{{ plugin.author }}</template> + </MkKeyValue> + <MkKeyValue class="_formBlock"> + <template #key>{{ $ts.description }}</template> + <template #value>{{ plugin.description }}</template> + </MkKeyValue> + <MkKeyValue class="_formBlock"> + <template #key>{{ $ts.permission }}</template> + <template #value>{{ plugin.permission }}</template> + </MkKeyValue> + + <div style="display: flex; gap: var(--margin); flex-wrap: wrap;"> + <MkButton v-if="plugin.config" inline @click="config(plugin)"><i class="fas fa-cog"></i> {{ $ts.settings }}</MkButton> + <MkButton inline danger @click="uninstall(plugin)"><i class="fas fa-trash-alt"></i> {{ $ts.uninstall }}</MkButton> + </div> + </div> + </FormSection> +</div> </template> <script lang="ts"> import { defineComponent } from 'vue'; -import FormBase from '@/components/debobigego/base.vue'; -import FormGroup from '@/components/debobigego/group.vue'; -import FormLink from '@/components/debobigego/link.vue'; +import FormLink from '@/components/form/link.vue'; +import FormSwitch from '@/components/form/switch.vue'; +import FormSection from '@/components/form/section.vue'; +import MkButton from '@/components/ui/button.vue'; +import MkKeyValue from '@/components/key-value.vue'; import * as os from '@/os'; import { ColdDeviceStorage } from '@/store'; import * as symbols from '@/symbols'; export default defineComponent({ components: { - FormBase, FormLink, + FormSwitch, + FormSection, + MkButton, + MkKeyValue, }, emits: ['info'], @@ -29,12 +60,47 @@ export default defineComponent({ icon: 'fas fa-plug', bg: 'var(--bg)', }, - plugins: ColdDeviceStorage.get('plugins').length, + 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> diff --git a/packages/client/src/pages/settings/privacy.vue b/packages/client/src/pages/settings/privacy.vue index 78a0ea8b8d..dd13ba4bd0 100644 --- a/packages/client/src/pages/settings/privacy.vue +++ b/packages/client/src/pages/settings/privacy.vue @@ -47,8 +47,8 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; import FormSwitch from '@/components/form/switch.vue'; import FormSelect from '@/components/form/select.vue'; import FormSection from '@/components/form/section.vue'; @@ -56,67 +56,39 @@ import FormGroup from '@/components/form/group.vue'; import * as os from '@/os'; import { defaultStore } from '@/store'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; +import { $i } from '@/account'; -export default defineComponent({ - components: { - FormSelect, - FormSection, - 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, - ffVisibility: 'public', - } - }, +let isLocked = $ref($i.isLocked); +let autoAcceptFollowed = $ref($i.autoAcceptFollowed); +let noCrawle = $ref($i.noCrawle); +let isExplorable = $ref($i.isExplorable); +let hideOnlineStatus = $ref($i.hideOnlineStatus); +let publicReactions = $ref($i.publicReactions); +let ffVisibility = $ref($i.ffVisibility); - computed: { - defaultNoteVisibility: defaultStore.makeGetterSetter('defaultNoteVisibility'), - defaultNoteLocalOnly: defaultStore.makeGetterSetter('defaultNoteLocalOnly'), - rememberNoteVisibility: defaultStore.makeGetterSetter('rememberNoteVisibility'), - keepCw: defaultStore.makeGetterSetter('keepCw'), - }, +let defaultNoteVisibility = $computed(defaultStore.makeGetterSetter('defaultNoteVisibility')); +let defaultNoteLocalOnly = $computed(defaultStore.makeGetterSetter('defaultNoteLocalOnly')); +let rememberNoteVisibility = $computed(defaultStore.makeGetterSetter('rememberNoteVisibility')); +let keepCw = $computed(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; - this.ffVisibility = this.$i.ffVisibility; - }, +function save() { + os.api('i/update', { + isLocked: !!isLocked, + autoAcceptFollowed: !!autoAcceptFollowed, + noCrawle: !!noCrawle, + isExplorable: !!isExplorable, + hideOnlineStatus: !!hideOnlineStatus, + publicReactions: !!publicReactions, + ffVisibility: ffVisibility, + }); +} - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.locale.privacy, + icon: 'fas fa-lock-open', + bg: 'var(--bg)', }, - - methods: { - save() { - os.api('i/update', { - isLocked: !!this.isLocked, - autoAcceptFollowed: !!this.autoAcceptFollowed, - noCrawle: !!this.noCrawle, - isExplorable: !!this.isExplorable, - hideOnlineStatus: !!this.hideOnlineStatus, - publicReactions: !!this.publicReactions, - ffVisibility: this.ffVisibility, - }); - } - } }); </script> diff --git a/packages/client/src/pages/settings/profile.vue b/packages/client/src/pages/settings/profile.vue index 2eaf9a9f83..f875146a2c 100644 --- a/packages/client/src/pages/settings/profile.vue +++ b/packages/client/src/pages/settings/profile.vue @@ -3,50 +3,50 @@ <div class="llvierxe" :style="{ backgroundImage: $i.bannerUrl ? `url(${ $i.bannerUrl })` : null }"> <div class="avatar _acrylic"> <MkAvatar class="avatar" :user="$i" :disable-link="true" @click="changeAvatar"/> - <MkButton primary class="avatarEdit" @click="changeAvatar">{{ $ts._profile.changeAvatar }}</MkButton> + <MkButton primary class="avatarEdit" @click="changeAvatar">{{ i18n.locale._profile.changeAvatar }}</MkButton> </div> - <MkButton primary class="bannerEdit" @click="changeBanner">{{ $ts._profile.changeBanner }}</MkButton> + <MkButton primary class="bannerEdit" @click="changeBanner">{{ i18n.locale._profile.changeBanner }}</MkButton> </div> - <FormInput v-model="name" :max="30" manual-save class="_formBlock"> - <template #label>{{ $ts._profile.name }}</template> + <FormInput v-model="profile.name" :max="30" manual-save class="_formBlock"> + <template #label>{{ i18n.locale._profile.name }}</template> </FormInput> - <FormTextarea v-model="description" :max="500" tall manual-save class="_formBlock"> - <template #label>{{ $ts._profile.description }}</template> - <template #caption>{{ $ts._profile.youCanIncludeHashtags }}</template> + <FormTextarea v-model="profile.description" :max="500" tall manual-save class="_formBlock"> + <template #label>{{ i18n.locale._profile.description }}</template> + <template #caption>{{ i18n.locale._profile.youCanIncludeHashtags }}</template> </FormTextarea> - <FormInput v-model="location" manual-save class="_formBlock"> - <template #label>{{ $ts.location }}</template> + <FormInput v-model="profile.location" manual-save class="_formBlock"> + <template #label>{{ i18n.locale.location }}</template> <template #prefix><i class="fas fa-map-marker-alt"></i></template> </FormInput> - <FormInput v-model="birthday" type="date" manual-save class="_formBlock"> - <template #label>{{ $ts.birthday }}</template> + <FormInput v-model="profile.birthday" type="date" manual-save class="_formBlock"> + <template #label>{{ i18n.locale.birthday }}</template> <template #prefix><i class="fas fa-birthday-cake"></i></template> </FormInput> - <FormSelect v-model="lang" class="_formBlock"> - <template #label>{{ $ts.language }}</template> + <FormSelect v-model="profile.lang" class="_formBlock"> + <template #label>{{ i18n.locale.language }}</template> <option v-for="x in langs" :key="x[0]" :value="x[0]">{{ x[1] }}</option> </FormSelect> <FormSlot> - <MkButton @click="editMetadata">{{ $ts._profile.metadataEdit }}</MkButton> - <template #caption>{{ $ts._profile.metadataDescription }}</template> + <MkButton @click="editMetadata">{{ i18n.locale._profile.metadataEdit }}</MkButton> + <template #caption>{{ i18n.locale._profile.metadataDescription }}</template> </FormSlot> - <FormSwitch v-model="isCat" class="_formBlock">{{ $ts.flagAsCat }}<template #caption>{{ $ts.flagAsCatDescription }}</template></FormSwitch> + <FormSwitch v-model="profile.isCat" class="_formBlock">{{ i18n.locale.flagAsCat }}<template #caption>{{ i18n.locale.flagAsCatDescription }}</template></FormSwitch> - <FormSwitch v-model="isBot" class="_formBlock">{{ $ts.flagAsBot }}<template #caption>{{ $ts.flagAsBotDescription }}</template></FormSwitch> + <FormSwitch v-model="profile.isBot" class="_formBlock">{{ i18n.locale.flagAsBot }}<template #caption>{{ i18n.locale.flagAsBotDescription }}</template></FormSwitch> - <FormSwitch v-model="alwaysMarkNsfw" class="_formBlock">{{ $ts.alwaysMarkSensitive }}</FormSwitch> + <FormSwitch v-model="profile.alwaysMarkNsfw" class="_formBlock">{{ i18n.locale.alwaysMarkSensitive }}</FormSwitch> </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { defineComponent, reactive, watch } from 'vue'; import MkButton from '@/components/ui/button.vue'; import FormInput from '@/components/form/input.vue'; import FormTextarea from '@/components/form/textarea.vue'; @@ -57,198 +57,149 @@ import { host, langs } from '@/config'; import { selectFile } from '@/scripts/select-file'; import * as os from '@/os'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; +import { $i } from '@/account'; -export default defineComponent({ - components: { - MkButton, - FormInput, - FormTextarea, - FormSwitch, - FormSelect, - FormSlot, - }, - - emits: ['info'], +const profile = reactive({ + name: $i.name, + description: $i.description, + location: $i.location, + birthday: $i.birthday, + lang: $i.lang, + isBot: $i.isBot, + isCat: $i.isCat, + alwaysMarkNsfw: $i.alwaysMarkNsfw, +}); - 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, - } - }, +const additionalFields = reactive({ + fieldName0: $i.fields[0] ? $i.fields[0].name : null, + fieldValue0: $i.fields[0] ? $i.fields[0].value : null, + fieldName1: $i.fields[1] ? $i.fields[1].name : null, + fieldValue1: $i.fields[1] ? $i.fields[1].value : null, + fieldName2: $i.fields[2] ? $i.fields[2].name : null, + fieldValue2: $i.fields[2] ? $i.fields[2].value : null, + fieldName3: $i.fields[3] ? $i.fields[3].name : null, + fieldValue3: $i.fields[3] ? $i.fields[3].value : null, +}); - 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; +watch(() => profile, () => { + save(); +}, { + deep: true, +}); - 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; +function save() { + os.apiWithDialog('i/update', { + name: profile.name || null, + description: profile.description || null, + location: profile.location || null, + birthday: profile.birthday || null, + lang: profile.lang || null, + isBot: !!profile.isBot, + isCat: !!profile.isCat, + alwaysMarkNsfw: !!profile.alwaysMarkNsfw, + }); +} - 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); - }, +function changeAvatar(ev) { + selectFile(ev.currentTarget || ev.target, i18n.locale.avatar).then(async (file) => { + const i = await os.apiWithDialog('i/update', { + avatarId: file.id, + }); + $i.avatarId = i.avatarId; + $i.avatarUrl = i.avatarUrl; + }); +} - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, +function changeBanner(ev) { + selectFile(ev.currentTarget || ev.target, i18n.locale.banner).then(async (file) => { + const i = await os.apiWithDialog('i/update', { + bannerId: file.id, + }); + $i.bannerId = i.bannerId; + $i.bannerUrl = i.bannerUrl; + }); +} - methods: { - changeAvatar(e) { - selectFile(e.currentTarget || e.target, this.$ts.avatar).then(file => { - os.api('i/update', { - avatarId: file.id, - }); - }); +async function editMetadata() { + const { canceled, result } = await os.form(i18n.locale._profile.metadata, { + fieldName0: { + type: 'string', + label: i18n.locale._profile.metadataLabel + ' 1', + default: additionalFields.fieldName0, }, - - changeBanner(e) { - selectFile(e.currentTarget || e.target, this.$ts.banner).then(file => { - os.api('i/update', { - bannerId: file.id, - }); - }); + fieldValue0: { + type: 'string', + label: i18n.locale._profile.metadataContent + ' 1', + default: additionalFields.fieldValue0, }, + fieldName1: { + type: 'string', + label: i18n.locale._profile.metadataLabel + ' 2', + default: additionalFields.fieldName1, + }, + fieldValue1: { + type: 'string', + label: i18n.locale._profile.metadataContent + ' 2', + default: additionalFields.fieldValue1, + }, + fieldName2: { + type: 'string', + label: i18n.locale._profile.metadataLabel + ' 3', + default: additionalFields.fieldName2, + }, + fieldValue2: { + type: 'string', + label: i18n.locale._profile.metadataContent + ' 3', + default: additionalFields.fieldValue2, + }, + fieldName3: { + type: 'string', + label: i18n.locale._profile.metadataLabel + ' 4', + default: additionalFields.fieldName3, + }, + fieldValue3: { + type: 'string', + label: i18n.locale._profile.metadataContent + ' 4', + default: additionalFields.fieldValue3, + }, + }); + if (canceled) return; - 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 }, - ]; + additionalFields.fieldName0 = result.fieldName0; + additionalFields.fieldValue0 = result.fieldValue0; + additionalFields.fieldName1 = result.fieldName1; + additionalFields.fieldValue1 = result.fieldValue1; + additionalFields.fieldName2 = result.fieldName2; + additionalFields.fieldValue2 = result.fieldValue2; + additionalFields.fieldName3 = result.fieldName3; + additionalFields.fieldValue3 = result.fieldValue3; - os.api('i/update', { - fields, - }).then(i => { - os.success(); - }).catch(err => { - os.alert({ - type: 'error', - text: err.id - }); - }); - }, + const fields = [ + { name: additionalFields.fieldName0, value: additionalFields.fieldValue0 }, + { name: additionalFields.fieldName1, value: additionalFields.fieldValue1 }, + { name: additionalFields.fieldName2, value: additionalFields.fieldValue2 }, + { name: additionalFields.fieldName3, value: additionalFields.fieldValue3 }, + ]; - save() { - this.saving = true; + os.api('i/update', { + fields, + }).then(i => { + os.success(); + }).catch(err => { + os.alert({ + type: 'error', + text: err.id + }); + }); +} - 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; - }); - }, - } +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.locale.profile, + icon: 'fas fa-user', + bg: 'var(--bg)', + }, }); </script> diff --git a/packages/client/src/pages/settings/reaction.vue b/packages/client/src/pages/settings/reaction.vue index 0d4db46936..e5b1189947 100644 --- a/packages/client/src/pages/settings/reaction.vue +++ b/packages/client/src/pages/settings/reaction.vue @@ -100,10 +100,6 @@ export default defineComponent({ } }, - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { save() { this.$store.set('reactions', this.reactions); diff --git a/packages/client/src/pages/settings/registry.keys.vue b/packages/client/src/pages/settings/registry.keys.vue deleted file mode 100644 index 89953ebea1..0000000000 --- a/packages/client/src/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 primary @click="createKey">{{ $ts._registry.createKey }}</FormButton> -</FormBase> -</template> - -<script lang="ts"> -import { defineAsyncComponent, defineComponent } from 'vue'; -import * as JSON5 from 'json5'; -import FormSwitch from '@/components/form/switch.vue'; -import FormSelect from '@/components/form/select.vue'; -import FormLink from '@/components/debobigego/link.vue'; -import FormBase from '@/components/debobigego/base.vue'; -import FormGroup from '@/components/debobigego/group.vue'; -import FormButton from '@/components/debobigego/button.vue'; -import FormKeyValueView from '@/components/debobigego/key-value-view.vue'; -import * as os from '@/os'; -import * as symbols from '@/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/packages/client/src/pages/settings/registry.value.vue b/packages/client/src/pages/settings/registry.value.vue deleted file mode 100644 index 6acd3f6048..0000000000 --- a/packages/client/src/pages/settings/registry.value.vue +++ /dev/null @@ -1,147 +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 v-model="valueForEditor" tall class="_monospace" style="tab-size: 2;"> - <span>{{ $ts.value }} (JSON)</span> - </FormTextarea> - <FormButton primary @click="save"><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 '@/components/debobigego/info.vue'; -import FormSwitch from '@/components/form/switch.vue'; -import FormSelect from '@/components/form/select.vue'; -import FormTextarea from '@/components/form/textarea.vue'; -import FormBase from '@/components/debobigego/base.vue'; -import FormGroup from '@/components/debobigego/group.vue'; -import FormButton from '@/components/debobigego/button.vue'; -import FormKeyValueView from '@/components/debobigego/key-value-view.vue'; -import * as os from '@/os'; -import * as symbols from '@/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.alert({ - type: 'error', - text: this.$ts.invalidValue - }); - return; - } - - os.confirm({ - type: 'warning', - text: this.$ts.saveConfirm, - }).then(({ canceled }) => { - if (canceled) return; - os.apiWithDialog('i/registry/set', { - scope: this.scope, - key: this.xKey, - value: JSON5.parse(this.valueForEditor) - }); - }); - }, - - del() { - os.confirm({ - type: 'warning', - text: this.$ts.deleteConfirm, - }).then(({ canceled }) => { - if (canceled) return; - os.apiWithDialog('i/registry/remove', { - scope: this.scope, - key: this.xKey - }); - }); - } - } -}); -</script> diff --git a/packages/client/src/pages/settings/registry.vue b/packages/client/src/pages/settings/registry.vue deleted file mode 100644 index 6faff5d2a4..0000000000 --- a/packages/client/src/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 primary @click="createKey">{{ $ts._registry.createKey }}</FormButton> -</FormBase> -</template> - -<script lang="ts"> -import { defineAsyncComponent, defineComponent } from 'vue'; -import * as JSON5 from 'json5'; -import FormSwitch from '@/components/form/switch.vue'; -import FormSelect from '@/components/form/select.vue'; -import FormLink from '@/components/debobigego/link.vue'; -import FormBase from '@/components/debobigego/base.vue'; -import FormGroup from '@/components/debobigego/group.vue'; -import FormButton from '@/components/debobigego/button.vue'; -import FormKeyValueView from '@/components/debobigego/key-value-view.vue'; -import * as os from '@/os'; -import * as symbols from '@/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/packages/client/src/pages/settings/security.vue b/packages/client/src/pages/settings/security.vue index 069f9d964d..6fb3f1c413 100644 --- a/packages/client/src/pages/settings/security.vue +++ b/packages/client/src/pages/settings/security.vue @@ -12,7 +12,7 @@ <FormSection> <template #label>{{ $ts.signinHistory }}</template> - <FormPagination :pagination="pagination"> + <MkPagination :pagination="pagination"> <template v-slot="{items}"> <div> <div v-for="item in items" :key="item.id" v-panel class="timnmucd"> @@ -25,7 +25,7 @@ </div> </div> </template> - </FormPagination> + </MkPagination> </FormSection> <FormSection> @@ -40,10 +40,9 @@ <script lang="ts"> import { defineComponent } from 'vue'; import FormSection from '@/components/form/section.vue'; -import FormLink from '@/components/debobigego/link.vue'; import FormSlot from '@/components/form/slot.vue'; import FormButton from '@/components/ui/button.vue'; -import FormPagination from '@/components/form/pagination.vue'; +import MkPagination from '@/components/ui/pagination.vue'; import X2fa from './2fa.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; @@ -51,9 +50,8 @@ import * as symbols from '@/symbols'; export default defineComponent({ components: { FormSection, - FormLink, FormButton, - FormPagination, + MkPagination, FormSlot, X2fa, }, @@ -68,16 +66,12 @@ export default defineComponent({ bg: 'var(--bg)', }, pagination: { - endpoint: 'i/signin-history', + endpoint: 'i/signin-history' as const, limit: 5, }, } }, - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { async change() { const { canceled: canceled1, result: currentPassword } = await os.inputText({ diff --git a/packages/client/src/pages/settings/sounds.vue b/packages/client/src/pages/settings/sounds.vue index 0977dd8322..490a1b5514 100644 --- a/packages/client/src/pages/settings/sounds.vue +++ b/packages/client/src/pages/settings/sounds.vue @@ -94,12 +94,6 @@ export default defineComponent({ 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: { diff --git a/packages/client/src/pages/settings/theme.install.vue b/packages/client/src/pages/settings/theme.install.vue index c3e531afb2..e2a3f042b9 100644 --- a/packages/client/src/pages/settings/theme.install.vue +++ b/packages/client/src/pages/settings/theme.install.vue @@ -1,105 +1,79 @@ <template> -<FormBase> - <FormGroup> - <FormTextarea v-model="installThemeCode"> - <span>{{ $ts._theme.code }}</span> - </FormTextarea> - <FormButton :disabled="installThemeCode == null" inline @click="() => preview(installThemeCode)"><i class="fas fa-eye"></i> {{ $ts.preview }}</FormButton> - </FormGroup> +<div class="_formRoot"> + <FormTextarea v-model="installThemeCode" class="_formBlock"> + <template #label>{{ i18n.locale._theme.code }}</template> + </FormTextarea> - <FormButton :disabled="installThemeCode == null" primary inline @click="() => install(installThemeCode)"><i class="fas fa-check"></i> {{ $ts.install }}</FormButton> -</FormBase> + <div class="_formBlock" style="display: flex; gap: var(--margin); flex-wrap: wrap;"> + <FormButton :disabled="installThemeCode == null" inline @click="() => preview(installThemeCode)"><i class="fas fa-eye"></i> {{ i18n.locale.preview }}</FormButton> + <FormButton :disabled="installThemeCode == null" primary inline @click="() => install(installThemeCode)"><i class="fas fa-check"></i> {{ i18n.locale.install }}</FormButton> + </div> +</div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; import * as JSON5 from 'json5'; import FormTextarea from '@/components/form/textarea.vue'; -import FormSelect from '@/components/form/select.vue'; -import FormRadios from '@/components/form/radios.vue'; -import FormBase from '@/components/debobigego/base.vue'; -import FormGroup from '@/components/debobigego/group.vue'; -import FormLink from '@/components/debobigego/link.vue'; -import FormButton from '@/components/debobigego/button.vue'; +import FormButton from '@/components/ui/button.vue'; import { applyTheme, validateTheme } from '@/scripts/theme'; import * as os from '@/os'; -import { ColdDeviceStorage } from '@/store'; import { addTheme, getThemes } from '@/theme-store'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - FormTextarea, - FormSelect, - FormRadios, - FormBase, - FormGroup, - FormLink, - FormButton, - }, - - emits: ['info'], +let installThemeCode = $ref(null); - 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]); - }, +function parseThemeCode(code: string) { + let theme; - methods: { - parseThemeCode(code) { - let theme; + try { + theme = JSON5.parse(code); + } catch (e) { + os.alert({ + type: 'error', + text: i18n.locale._theme.invalid + }); + return false; + } + if (!validateTheme(theme)) { + os.alert({ + type: 'error', + text: i18n.locale._theme.invalid + }); + return false; + } + if (getThemes().some(t => t.id === theme.id)) { + os.alert({ + type: 'info', + text: i18n.locale._theme.alreadyInstalled + }); + return false; + } - try { - theme = JSON5.parse(code); - } catch (e) { - os.alert({ - type: 'error', - text: this.$ts._theme.invalid - }); - return false; - } - if (!validateTheme(theme)) { - os.alert({ - type: 'error', - text: this.$ts._theme.invalid - }); - return false; - } - if (getThemes().some(t => t.id === theme.id)) { - os.alert({ - type: 'info', - text: this.$ts._theme.alreadyInstalled - }); - return false; - } + return theme; +} - return theme; - }, +function preview(code: string): void { + const theme = parseThemeCode(code); + if (theme) applyTheme(theme, false); +} - preview(code) { - const theme = this.parseThemeCode(code); - if (theme) applyTheme(theme, false); - }, +async function install(code: string): Promise<void> { + const theme = parseThemeCode(code); + if (!theme) return; + await addTheme(theme); + os.alert({ + type: 'success', + text: i18n.t('_theme.installed', { name: theme.name }) + }); +} - async install(code) { - const theme = this.parseThemeCode(code); - if (!theme) return; - await addTheme(theme); - os.alert({ - type: 'success', - text: this.$t('_theme.installed', { name: theme.name }) - }); - }, - } +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.locale._theme.install, + icon: 'fas fa-download', + bg: 'var(--bg)', + }, }); </script> diff --git a/packages/client/src/pages/settings/theme.manage.vue b/packages/client/src/pages/settings/theme.manage.vue index c605b1eb64..a1e849b540 100644 --- a/packages/client/src/pages/settings/theme.manage.vue +++ b/packages/client/src/pages/settings/theme.manage.vue @@ -30,9 +30,6 @@ import { defineComponent } from 'vue'; import * as JSON5 from 'json5'; import FormTextarea from '@/components/form/textarea.vue'; import FormSelect from '@/components/form/select.vue'; -import FormRadios from '@/components/form/radios.vue'; -import FormBase from '@/components/debobigego/base.vue'; -import FormGroup from '@/components/debobigego/group.vue'; import FormInput from '@/components/form/input.vue'; import FormButton from '@/components/ui/button.vue'; import { Theme, builtinThemes } from '@/scripts/theme'; @@ -46,9 +43,6 @@ export default defineComponent({ components: { FormTextarea, FormSelect, - FormRadios, - FormBase, - FormGroup, FormInput, FormButton, }, @@ -84,10 +78,6 @@ export default defineComponent({ }, }, - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { copyThemeCode() { copyToClipboard(this.selectedThemeCode); diff --git a/packages/client/src/pages/settings/theme.vue b/packages/client/src/pages/settings/theme.vue index 6c88b65699..658e36ec05 100644 --- a/packages/client/src/pages/settings/theme.vue +++ b/packages/client/src/pages/settings/theme.vue @@ -163,10 +163,6 @@ export default defineComponent({ location.reload(); }); - onMounted(() => { - emit('info', INFO); - }); - onActivated(() => { fetchThemes().then(() => { installedThemes.value = getThemes(); diff --git a/packages/client/src/pages/settings/update.vue b/packages/client/src/pages/settings/update.vue deleted file mode 100644 index e0d8f6d15c..0000000000 --- a/packages/client/src/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 v-if="meta" #value>{{ meta.version }}</template> - <template v-else #value><MkEllipsis/></template> - </FormKeyValueView> - </FormGroup> - <FormGroup> - <template #label>Misskey</template> - <FormKeyValueView> - <template #key>{{ $ts.latestVersion }}</template> - <template v-if="releases" #value>{{ releases[0].tag_name }}</template> - <template v-else #value><MkEllipsis/></template> - </FormKeyValueView> - <template v-if="releases" #caption><MkTime :time="releases[0].published_at" mode="detail"/></template> - </FormGroup> -</FormBase> -</template> - -<script lang="ts"> -import { defineAsyncComponent, defineComponent } from 'vue'; -import FormSwitch from '@/components/form/switch.vue'; -import FormSelect from '@/components/form/select.vue'; -import FormLink from '@/components/debobigego/link.vue'; -import FormBase from '@/components/debobigego/base.vue'; -import FormGroup from '@/components/debobigego/group.vue'; -import FormButton from '@/components/debobigego/button.vue'; -import FormKeyValueView from '@/components/debobigego/key-value-view.vue'; -import FormInfo from '@/components/debobigego/info.vue'; -import * as os from '@/os'; -import { version, instanceName } from '@/config'; -import * as symbols from '@/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/packages/client/src/pages/settings/word-mute.vue b/packages/client/src/pages/settings/word-mute.vue index 068f88740a..19980dea14 100644 --- a/packages/client/src/pages/settings/word-mute.vue +++ b/packages/client/src/pages/settings/word-mute.vue @@ -31,7 +31,6 @@ <script lang="ts"> import { defineComponent } from 'vue'; import FormTextarea from '@/components/form/textarea.vue'; -import FormBase from '@/components/debobigego/base.vue'; import MkKeyValue from '@/components/key-value.vue'; import MkButton from '@/components/ui/button.vue'; import MkInfo from '@/components/ui/info.vue'; @@ -42,7 +41,6 @@ import * as symbols from '@/symbols'; export default defineComponent({ components: { - FormBase, MkButton, FormTextarea, MkKeyValue, @@ -89,10 +87,6 @@ export default defineComponent({ 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(' '))); diff --git a/packages/client/src/pages/share.vue b/packages/client/src/pages/share.vue index bdd8500ee4..5df6256fb2 100644 --- a/packages/client/src/pages/share.vue +++ b/packages/client/src/pages/share.vue @@ -169,7 +169,7 @@ export default defineComponent({ window.close(); // 閉じなければ100ms後タイムラインに - setTimeout(() => { + window.setTimeout(() => { this.$router.push('/'); }, 100); } diff --git a/packages/client/src/pages/signup-complete.vue b/packages/client/src/pages/signup-complete.vue index 89375e05d2..a10af1a4cc 100644 --- a/packages/client/src/pages/signup-complete.vue +++ b/packages/client/src/pages/signup-complete.vue @@ -1,50 +1,36 @@ <template> <div> - {{ $ts.processing }} + {{ i18n.locale.processing }} </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { onMounted } from 'vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; import { login } from '@/account'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { +const props = defineProps<{ + code: string; +}>(); - }, - - props: { - code: { - type: String, - required: true - } - }, - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.signup, - icon: 'fas fa-user' - }, - } - }, +onMounted(async () => { + await os.alert({ + type: 'info', + text: i18n.t('clickToFinishEmailVerification', { ok: i18n.locale.gotIt }), + }); + const res = await os.apiWithDialog('signup-pending', { + code: props.code, + }); + login(res.i, '/'); +}); - async mounted() { - await os.alert({ - type: 'info', - text: this.$t('clickToFinishEmailVerification', { ok: this.$ts.gotIt }), - }); - const res = await os.apiWithDialog('signup-pending', { - code: this.code, - }); - login(res.i, '/'); +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.locale.signup, + icon: 'fas fa-user', }, - - methods: { - - } }); </script> diff --git a/packages/client/src/pages/tag.vue b/packages/client/src/pages/tag.vue index a0c8367849..045f1ef259 100644 --- a/packages/client/src/pages/tag.vue +++ b/packages/client/src/pages/tag.vue @@ -1,46 +1,31 @@ <template> <div class="_section"> - <XNotes ref="notes" class="_content" :pagination="pagination"/> + <XNotes class="_content" :pagination="pagination"/> </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { computed } from 'vue'; import XNotes from '@/components/notes.vue'; import * as symbols from '@/symbols'; -export default defineComponent({ - components: { - XNotes - }, +const props = defineProps<{ + tag: string; +}>(); - props: { - tag: { - type: String, - required: true - } - }, +const pagination = { + endpoint: 'notes/search-by-tag' as const, + limit: 10, + params: computed(() => ({ + tag: props.tag, + })), +}; - 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(); - } - }, +defineExpose({ + [symbols.PAGE_INFO]: computed(() => ({ + title: props.tag, + icon: 'fas fa-hashtag', + bg: 'var(--bg)', + })), }); </script> diff --git a/packages/client/src/pages/test.vue b/packages/client/src/pages/test.vue deleted file mode 100644 index d05e00d374..0000000000 --- a/packages/client/src/pages/test.vue +++ /dev/null @@ -1,260 +0,0 @@ -<template> -<div class="_section"> - <div class="_content"> - <div class="_card _gap"> - <div class="_title">Dialog</div> - <div class="_content"> - <MkInput v-model="dialogTitle"> - <template #label>Title</template> - </MkInput> - <MkInput v-model="dialogBody"> - <template #label>Body</template> - </MkInput> - <MkRadio v-model="dialogType" value="info">Info</MkRadio> - <MkRadio v-model="dialogType" value="success">Success</MkRadio> - <MkRadio v-model="dialogType" value="warning">Warn</MkRadio> - <MkRadio v-model="dialogType" value="error">Error</MkRadio> - <MkSwitch v-model="dialogCancel"> - <span>With cancel button</span> - </MkSwitch> - <MkSwitch v-model="dialogCancelByBgClick"> - <span>Can cancel by modal bg click</span> - </MkSwitch> - <MkSwitch v-model="dialogInput"> - <span>With input field</span> - </MkSwitch> - <MkButton @click="showDialog()">Show</MkButton> - </div> - <div class="_content"> - <code>Result: {{ dialogResult }}</code> - </div> - </div> - - <div class="_card _gap"> - <div class="_title">Form</div> - <div class="_content"> - <MkInput v-model="formTitle"> - <template #label>Title</template> - </MkInput> - <MkTextarea v-model="formForm"> - <template #label>Form</template> - </MkTextarea> - <MkButton @click="form()">Show</MkButton> - </div> - <div class="_content"> - <code>Result: {{ formResult }}</code> - </div> - </div> - - <div class="_card _gap"> - <div class="_title">MFM</div> - <div class="_content"> - <MkTextarea v-model="mfm"> - <template #label>MFM</template> - </MkTextarea> - </div> - <div class="_content"> - <Mfm :text="mfm"/> - </div> - </div> - - <div class="_card _gap"> - <div class="_title">selectDriveFile</div> - <div class="_content"> - <MkSwitch v-model="selectDriveFileMultiple"> - <span>Multiple</span> - </MkSwitch> - <MkButton @click="selectDriveFile()">selectDriveFile</MkButton> - </div> - <div class="_content"> - <code>Result: {{ JSON.stringify(selectDriveFileResult) }}</code> - </div> - </div> - - <div class="_card _gap"> - <div class="_title">selectDriveFolder</div> - <div class="_content"> - <MkSwitch v-model="selectDriveFolderMultiple"> - <span>Multiple</span> - </MkSwitch> - <MkButton @click="selectDriveFolder()">selectDriveFolder</MkButton> - </div> - <div class="_content"> - <code>Result: {{ JSON.stringify(selectDriveFolderResult) }}</code> - </div> - </div> - - <div class="_card _gap"> - <div class="_title">selectUser</div> - <div class="_content"> - <MkButton @click="selectUser()">selectUser</MkButton> - </div> - <div class="_content"> - <code>Result: {{ user }}</code> - </div> - </div> - - <div class="_card _gap"> - <div class="_title">Notification</div> - <div class="_content"> - <MkInput v-model="notificationIconUrl"> - <template #label>Icon URL</template> - </MkInput> - <MkInput v-model="notificationHeader"> - <template #label>Header</template> - </MkInput> - <MkTextarea v-model="notificationBody"> - <template #label>Body</template> - </MkTextarea> - <MkButton @click="createNotification()">createNotification</MkButton> - </div> - </div> - - <div class="_card _gap"> - <div class="_title">Waiting dialog</div> - <div class="_content"> - <MkButton inline @click="openWaitingDialog()">icon only</MkButton> - <MkButton inline @click="openWaitingDialog('Doing')">with text</MkButton> - </div> - </div> - - <div class="_card _gap"> - <div class="_title">Messaging window</div> - <div class="_content"> - <MkButton @click="messagingWindowOpen()">open</MkButton> - </div> - </div> - - <MkButton @click="resetTutorial()">Reset tutorial</MkButton> - </div> -</div> -</template> - -<script lang="ts"> -import { defineComponent, defineAsyncComponent } from 'vue'; -import MkButton from '@/components/ui/button.vue'; -import MkInput from '@/components/form/input.vue'; -import MkSwitch from '@/components/form/switch.vue'; -import MkTextarea from '@/components/form/textarea.vue'; -import MkRadio from '@/components/form/radio.vue'; -import * as os from '@/os'; -import * as symbols from '@/symbols'; - -export default defineComponent({ - components: { - MkButton, - MkInput, - MkSwitch, - MkTextarea, - MkRadio, - }, - - data() { - return { - [symbols.PAGE_INFO]: { - title: 'TEST', - icon: 'fas fa-exclamation-triangle' - }, - dialogTitle: 'Hello', - dialogBody: 'World!', - dialogType: 'info', - dialogCancel: false, - dialogCancelByBgClick: true, - dialogInput: false, - dialogResult: null, - formTitle: 'Test form', - formForm: JSON.stringify({ - foo: { - type: 'boolean', - default: true, - label: 'This is a boolean property' - }, - bar: { - type: 'number', - default: 300, - label: 'This is a number property' - }, - baz: { - type: 'string', - default: 'Misskey makes you happy.', - label: 'This is a string property' - }, - qux: { - type: 'string', - multiline: true, - default: 'Misskey makes\nyou happy.', - label: 'Multiline string' - }, - }, null, '\t'), - formResult: null, - mfm: '', - selectDriveFileMultiple: false, - selectDriveFolderMultiple: false, - selectDriveFileResult: null, - selectDriveFolderResult: null, - user: null, - notificationIconUrl: null, - notificationHeader: '', - notificationBody: '', - } - }, - - methods: { - async showDialog() { - this.dialogResult = null; - /* - this.dialogResult = await os.dialog({ - type: this.dialogType, - title: this.dialogTitle, - text: this.dialogBody, - showCancelButton: this.dialogCancel, - cancelableByBgClick: this.dialogCancelByBgClick, - input: this.dialogInput ? {} : null - });*/ - }, - - async form() { - this.formResult = null; - this.formResult = await os.form(this.formTitle, JSON.parse(this.formForm)); - }, - - async selectDriveFile() { - this.selectDriveFileResult = null; - this.selectDriveFileResult = await os.selectDriveFile(this.selectDriveFileMultiple); - }, - - async selectDriveFolder() { - this.selectDriveFolderResult = null; - this.selectDriveFolderResult = await os.selectDriveFolder(this.selectDriveFolderMultiple); - }, - - async selectUser() { - this.user = null; - this.user = await os.selectUser(); - }, - - async createNotification() { - os.api('notifications/create', { - header: this.notificationHeader, - body: this.notificationBody, - icon: this.notificationIconUrl, - }); - }, - - messagingWindowOpen() { - os.pageWindow('/my/messaging'); - }, - - openWaitingDialog(text?) { - const promise = new Promise((resolve, reject) => { - setTimeout(resolve, 2000); - }); - os.promiseDialog(promise, null, null, text); - }, - - resetTutorial() { - this.$store.set('tutorial', 0); - }, - } -}); -</script> diff --git a/packages/client/src/pages/theme-editor.vue b/packages/client/src/pages/theme-editor.vue index f023653425..80b8c7806c 100644 --- a/packages/client/src/pages/theme-editor.vue +++ b/packages/client/src/pages/theme-editor.vue @@ -1,300 +1,274 @@ <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" class="color _button" :class="{ active: theme.props.bg === color.color }" @click="setBgColor(color)"> - <div class="preview" :style="{ background: color.forPreview }"></div> - </button> +<MkSpacer :content-max="800" :margin-min="16" :margin-max="32"> + <div class="cwepdizn _formRoot"> + <FormFolder :default-open="true" class="_formBlock"> + <template #label>{{ i18n.locale.backgroundColor }}</template> + <div class="cwepdizn-colors"> + <div class="row"> + <button v-for="color in bgColors.filter(x => x.kind === 'light')" :key="color.color" class="color _button" :class="{ active: theme.props.bg === color.color }" @click="setBgColor(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" class="color _button" :class="{ active: theme.props.bg === color.color }" @click="setBgColor(color)"> + <div class="preview" :style="{ background: color.forPreview }"></div> + </button> + </div> </div> - <div class="row"> - <button v-for="color in bgColors.filter(x => x.kind === 'dark')" :key="color.color" class="color _button" :class="{ active: theme.props.bg === color.color }" @click="setBgColor(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" class="color rounded _button" :class="{ active: theme.props.accent === color }" @click="setAccentColor(color)"> - <div class="preview" :style="{ background: color }"></div> - </button> + </FormFolder> + + <FormFolder :default-open="true" class="_formBlock"> + <template #label>{{ i18n.locale.accentColor }}</template> + <div class="cwepdizn-colors"> + <div class="row"> + <button v-for="color in accentColors" :key="color" class="color rounded _button" :class="{ active: theme.props.accent === color }" @click="setAccentColor(color)"> + <div class="preview" :style="{ background: color }"></div> + </button> + </div> </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" class="color char _button" :class="{ active: (theme.props.fg === color.forLight) || (theme.props.fg === color.forDark) }" @click="setFgColor(color)"> - <div class="preview" :style="{ color: color.forPreview ? color.forPreview : theme.base === 'light' ? '#5f5f5f' : '#dadada' }">A</div> - </button> + </FormFolder> + + <FormFolder :default-open="true" class="_formBlock"> + <template #label>{{ i18n.locale.textColor }}</template> + <div class="cwepdizn-colors"> + <div class="row"> + <button v-for="color in fgColors" :key="color" class="color char _button" :class="{ active: (theme.props.fg === color.forLight) || (theme.props.fg === color.forDark) }" @click="setFgColor(color)"> + <div class="preview" :style="{ color: color.forPreview ? color.forPreview : theme.base === 'light' ? '#5f5f5f' : '#dadada' }">A</div> + </button> + </div> </div> - </div> - </div> + </FormFolder> - <FormGroup v-if="codeEnabled"> - <FormTextarea v-model="themeCode" tall> - <span>{{ $ts._theme.code }}</span> - </FormTextarea> - <FormButton primary @click="applyThemeCode">{{ $ts.apply }}</FormButton> - </FormGroup> - <FormButton v-else @click="codeEnabled = true"><i class="fas fa-code"></i> {{ $ts.editCode }}</FormButton> + <FormFolder :default-open="false" class="_formBlock"> + <template #icon><i class="fas fa-code"></i></template> + <template #label>{{ i18n.locale.editCode }}</template> - <FormGroup v-if="descriptionEnabled"> - <FormTextarea v-model="description"> - <span>{{ $ts._theme.description }}</span> - </FormTextarea> - </FormGroup> - <FormButton v-else @click="descriptionEnabled = true">{{ $ts.addDescription }}</FormButton> + <div class="_formRoot"> + <FormTextarea v-model="themeCode" tall class="_formBlock"> + <template #label>{{ i18n.locale._theme.code }}</template> + </FormTextarea> + <FormButton primary class="_formBlock" @click="applyThemeCode">{{ i18n.locale.apply }}</FormButton> + </div> + </FormFolder> - <FormGroup> - <FormButton @click="showPreview"><i class="fas fa-eye"></i> {{ $ts.preview }}</FormButton> - <FormButton primary @click="saveAs"><i class="fas fa-save"></i> {{ $ts.saveAs }}</FormButton> - </FormGroup> -</FormBase> + <FormFolder :default-open="false" class="_formBlock"> + <template #label>{{ i18n.locale.addDescription }}</template> + + <div class="_formRoot"> + <FormTextarea v-model="description"> + <template #label>{{ i18n.locale._theme.description }}</template> + </FormTextarea> + </div> + </FormFolder> + </div> +</MkSpacer> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { watch } 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 '@/components/debobigego/base.vue'; -import FormButton from '@/components/debobigego/button.vue'; -import FormTextarea from '@/components/debobigego/textarea.vue'; -import FormGroup from '@/components/debobigego/group.vue'; +import FormButton from '@/components/ui/button.vue'; +import FormTextarea from '@/components/form/textarea.vue'; +import FormFolder from '@/components/form/folder.vue'; -import { Theme, applyTheme, validateTheme, darkTheme, lightTheme } from '@/scripts/theme'; +import { Theme, applyTheme, darkTheme, lightTheme } from '@/scripts/theme'; import { host } from '@/config'; import * as os from '@/os'; -import { ColdDeviceStorage } from '@/store'; +import { ColdDeviceStorage, defaultStore } from '@/store'; import { addTheme } from '@/theme-store'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; +import { useLeaveGuard } from '@/scripts/use-leave-guard'; -export default defineComponent({ - components: { - FormBase, - FormButton, - FormTextarea, - FormGroup, - }, +const 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' }, +] as const; +const accentColors = ['#e36749', '#f29924', '#98c934', '#34c9a9', '#34a1c9', '#606df7', '#8d34c9', '#e84d83']; +const 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' }, +]; - async beforeRouteLeave(to, from) { - if (this.changed && !(await this.leaveConfirm())) { - return false; - } - }, - - 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); - }, +const theme = $ref<Partial<Theme>>({ + base: 'light', + props: lightTheme.props, +}); +let description = $ref<string | null>(null); +let themeCode = $ref<string | null>(null); +let changed = $ref(false); - beforeUnmount() { - window.removeEventListener('beforeunload', this.beforeunload); - }, +useLeaveGuard($$(changed)); - methods: { - beforeunload(e: BeforeUnloadEvent) { - if (this.changed) { - e.preventDefault(); - e.returnValue = ''; - } - }, +function showPreview() { + os.pageWindow('preview'); +} - async leaveConfirm(): Promise<boolean> { - const { canceled } = await os.confirm({ - type: 'warning', - text: this.$ts.leaveConfirm, - }); - return !canceled; - }, +function setBgColor(color: typeof bgColors[number]) { + if (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; + theme.props[prop] = base.props[prop]; + } + } + theme.base = color.kind; + theme.props.bg = color.color; - showPreview() { - os.pageWindow('preview'); - }, + if (theme.props.fg) { + const matchedFgColor = fgColors.find(x => [tinycolor(x.forLight).toRgbString(), tinycolor(x.forDark).toRgbString()].includes(tinycolor(theme.props.fg).toRgbString())); + if (matchedFgColor) setFgColor(matchedFgColor); + } +} - 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; +function setAccentColor(color) { + theme.props.accent = 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); - } - }, +function setFgColor(color) { + theme.props.fg = theme.base === 'light' ? color.forLight : color.forDark; +} - setAccentColor(color) { - this.theme.props.accent = color; - }, +function apply() { + themeCode = JSON5.stringify(theme, null, '\t'); + applyTheme(theme, false); + changed = true; +} - setFgColor(color) { - this.theme.props.fg = this.theme.base === 'light' ? color.forLight : color.forDark; - }, +function applyThemeCode() { + let parsed; - apply() { - this.themeCode = JSON5.stringify(this.theme, null, '\t'); - applyTheme(this.theme, false); - this.changed = true; - }, + try { + parsed = JSON5.parse(themeCode); + } catch (err) { + os.alert({ + type: 'error', + text: i18n.locale._theme.invalid, + }); + return; + } - applyThemeCode() { - let parsed; + theme = parsed; +} - try { - parsed = JSON5.parse(this.themeCode); - } catch (e) { - os.alert({ - type: 'error', - text: this.$ts._theme.invalid - }); - return; - } +async function saveAs() { + const { canceled, result: name } = await os.inputText({ + title: i18n.locale.name, + allowEmpty: false, + }); + if (canceled) return; - this.theme = parsed; - }, + theme.id = uuid(); + theme.name = name; + theme.author = `@${$i.username}@${toUnicode(host)}`; + if (description) theme.desc = description; + addTheme(theme); + applyTheme(theme); + if (defaultStore.state.darkMode) { + ColdDeviceStorage.set('darkTheme', theme); + } else { + ColdDeviceStorage.set('lightTheme', theme); + } + changed = false; + os.alert({ + type: 'success', + text: i18n.t('_theme.installed', { name: theme.name }), + }); +} - async saveAs() { - const { canceled, result: name } = await os.inputText({ - title: this.$ts.name, - allowEmpty: false - }); - if (canceled) return; +watch($$(theme), apply, { deep: true }); - 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.alert({ - type: 'success', - text: this.$t('_theme.installed', { name: this.theme.name }) - }); - } - } +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.locale.themeEditor, + icon: 'fas fa-palette', + bg: 'var(--bg)', + actions: [{ + asFullButton: true, + icon: 'fas fa-eye', + text: i18n.locale.preview, + handler: showPreview, + }, { + asFullButton: true, + icon: 'fas fa-check', + text: i18n.locale.saveAs, + handler: saveAs, + }], + }, }); </script> <style lang="scss" scoped> .cwepdizn { - max-width: 800px; - margin: 0 auto; + ::v-deep(.cwepdizn-colors) { + text-align: center; - > .colorPicker { - > .colors { - padding: 32px; - text-align: center; + > .row { + > .color { + display: inline-block; + position: relative; + width: 64px; + height: 64px; + border-radius: 8px; - > .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 { - 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; + transform: scale(1.1); } + } - &:hover { - > .preview { - transform: scale(1.1); - } - } + &.active { + box-shadow: 0 0 0 2px var(--divider) inset; + } - &.active { - box-shadow: 0 0 0 2px var(--divider) inset; - } + &.rounded { + border-radius: 999px; - &.rounded { + > .preview { border-radius: 999px; - - > .preview { - border-radius: 999px; - } } + } - &.char { - line-height: 42px; - } + &.char { + line-height: 42px; } } } diff --git a/packages/client/src/pages/timeline.tutorial.vue b/packages/client/src/pages/timeline.tutorial.vue index 3775796940..432d28c60b 100644 --- a/packages/client/src/pages/timeline.tutorial.vue +++ b/packages/client/src/pages/timeline.tutorial.vue @@ -65,26 +65,14 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { computed } from 'vue'; import MkButton from '@/components/ui/button.vue'; +import { defaultStore } from '@/store'; -export default defineComponent({ - components: { - MkButton, - }, - - data() { - return { - } - }, - - computed: { - tutorial: { - get() { return this.$store.reactiveState.tutorial.value || 0; }, - set(value) { this.$store.set('tutorial', value); } - }, - }, +const tutorial = computed({ + get() { return defaultStore.reactiveState.tutorial.value || 0; }, + set(value) { defaultStore.set('tutorial', value); } }); </script> diff --git a/packages/client/src/pages/timeline.vue b/packages/client/src/pages/timeline.vue index 216b3c34ea..aabb953aec 100644 --- a/packages/client/src/pages/timeline.vue +++ b/packages/client/src/pages/timeline.vue @@ -1,6 +1,6 @@ <template> <MkSpacer :content-max="800"> - <div v-hotkey.global="keymap" class="cmuxhskf"> + <div ref="rootEl" v-hotkey.global="keymap" class="cmuxhskf"> <XTutorial v-if="$store.reactiveState.tutorial.value != -1" class="tutorial _block"/> <XPostForm v-if="$store.reactiveState.showFixedPostForm.value" class="post-form _block" fixed/> @@ -18,162 +18,144 @@ </template> <script lang="ts"> -import { defineComponent, defineAsyncComponent, computed } from 'vue'; +export default { + name: 'MkTimelinePage', +} +</script> + +<script lang="ts" setup> +import { defineAsyncComponent, computed, watch } from 'vue'; import XTimeline from '@/components/timeline.vue'; import XPostForm from '@/components/post-form.vue'; import { scroll } from '@/scripts/scroll'; import * as os from '@/os'; import * as symbols from '@/symbols'; +import { defaultStore } from '@/store'; +import { i18n } from '@/i18n'; +import { instance } from '@/instance'; +import { $i } from '@/account'; -export default defineComponent({ - name: 'timeline', - - components: { - XTimeline, - XTutorial: defineAsyncComponent(() => import('./timeline.tutorial.vue')), - XPostForm, - }, +const XTutorial = defineAsyncComponent(() => import('./timeline.tutorial.vue')); - 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(); }, - }, ...(this.isLocalTimelineAvailable ? [{ - 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(); }, - }] : []), ...(this.isGlobalTimelineAvailable ? [{ - active: this.src === 'global', - title: this.$ts._timelines.global, - icon: 'fas fa-globe', - iconOnly: true, - onClick: () => { this.src = 'global'; this.saveSrc(); }, - }] : [])], - })), - }; - }, +const isLocalTimelineAvailable = !instance.disableLocalTimeline || ($i != null && ($i.isModerator || $i.isAdmin)); +const isGlobalTimelineAvailable = !instance.disableGlobalTimeline || ($i != null && ($i.isModerator || $i.isAdmin)); +const keymap = { + 't': focus, +}; - computed: { - keymap(): any { - return { - 't': this.focus - }; - }, +const tlComponent = $ref<InstanceType<typeof XTimeline>>(); +const rootEl = $ref<HTMLElement>(); - isLocalTimelineAvailable(): boolean { - return !this.$instance.disableLocalTimeline || this.$i.isModerator || this.$i.isAdmin; - }, +let src = $ref<'home' | 'local' | 'social' | 'global'>(defaultStore.state.tl.src); +let queue = $ref(0); - 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; - }, +function queueUpdated(q: number): void { + queue = q; +} - methods: { - queueUpdated(q) { - this.queue = q; - }, +function top(): void { + scroll(rootEl, { top: 0 }); +} - top() { - scroll(this.$el, { top: 0 }); - }, +async function chooseList(ev: MouseEvent): Promise<void> { + 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 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 function chooseAntenna(ev: MouseEvent): Promise<void> { + 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 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 function chooseChannel(ev: MouseEvent): Promise<void> { + 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); +} - 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); - }, +function saveSrc(): void { + defaultStore.set('tl', { + src: src, + }); +} - saveSrc() { - this.$store.set('tl', { - src: this.src, - }); - }, +async function timetravel(): Promise<void> { + const { canceled, result: date } = await os.inputDate({ + title: i18n.locale.date, + }); + if (canceled) return; - async timetravel() { - const { canceled, result: date } = await os.inputDate({ - title: this.$ts.date, - }); - if (canceled) return; + tlComponent.timetravel(date); +} - this.$refs.tl.timetravel(date); - }, +function focus(): void { + tlComponent.focus(); +} - focus() { - (this.$refs.tl as any).focus(); - } - } +defineExpose({ + [symbols.PAGE_INFO]: computed(() => ({ + title: i18n.locale.timeline, + icon: src === 'local' ? 'fas fa-comments' : src === 'social' ? 'fas fa-share-alt' : src === 'global' ? 'fas fa-globe' : 'fas fa-home', + bg: 'var(--bg)', + actions: [{ + icon: 'fas fa-list-ul', + text: i18n.locale.lists, + handler: chooseList, + }, { + icon: 'fas fa-satellite', + text: i18n.locale.antennas, + handler: chooseAntenna, + }, { + icon: 'fas fa-satellite-dish', + text: i18n.locale.channel, + handler: chooseChannel, + }, { + icon: 'fas fa-calendar-alt', + text: i18n.locale.jumpToSpecifiedDate, + handler: timetravel, + }], + tabs: [{ + active: src === 'home', + title: i18n.locale._timelines.home, + icon: 'fas fa-home', + iconOnly: true, + onClick: () => { src = 'home'; saveSrc(); }, + }, ...(isLocalTimelineAvailable ? [{ + active: src === 'local', + title: i18n.locale._timelines.local, + icon: 'fas fa-comments', + iconOnly: true, + onClick: () => { src = 'local'; saveSrc(); }, + }, { + active: src === 'social', + title: i18n.locale._timelines.social, + icon: 'fas fa-share-alt', + iconOnly: true, + onClick: () => { src = 'social'; saveSrc(); }, + }] : []), ...(isGlobalTimelineAvailable ? [{ + active: src === 'global', + title: i18n.locale._timelines.global, + icon: 'fas fa-globe', + iconOnly: true, + onClick: () => { src = 'global'; saveSrc(); }, + }] : [])], + })), }); </script> diff --git a/packages/client/src/pages/user-ap-info.vue b/packages/client/src/pages/user-ap-info.vue deleted file mode 100644 index 0027381f53..0000000000 --- a/packages/client/src/pages/user-ap-info.vue +++ /dev/null @@ -1,124 +0,0 @@ -<template> -<FormBase> - <FormSuspense v-slot="{ result: ap }" :p="apPromiseFactory"> - <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 '@/components/debobigego/object-view.vue'; -import FormTextarea from '@/components/debobigego/textarea.vue'; -import FormLink from '@/components/debobigego/link.vue'; -import FormBase from '@/components/debobigego/base.vue'; -import FormGroup from '@/components/debobigego/group.vue'; -import FormButton from '@/components/debobigego/button.vue'; -import FormKeyValueView from '@/components/debobigego/key-value-view.vue'; -import FormSuspense from '@/components/debobigego/suspense.vue'; -import * as os from '@/os'; -import number from '@/filters/number'; -import bytes from '@/filters/bytes'; -import * as symbols from '@/symbols'; -import { url } from '@/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/packages/client/src/pages/user-info.vue b/packages/client/src/pages/user-info.vue index 0fd208a64a..4bdc82f601 100644 --- a/packages/client/src/pages/user-info.vue +++ b/packages/client/src/pages/user-info.vue @@ -1,70 +1,75 @@ <template> -<FormBase> +<MkSpacer :content-max="500" :margin-min="16" :margin-max="32"> <FormSuspense :p="init"> - <div class="_debobigegoItem aeakzknw"> - <MkAvatar class="avatar" :user="user" :show-indicator="true"/> - </div> - - <FormLink :to="userPage(user)">Profile</FormLink> + <div class="_formRoot"> + <div class="_formBlock aeakzknw"> + <MkAvatar class="avatar" :user="user" :show-indicator="true"/> + </div> - <FormGroup> - <FormKeyValueView> - <template #key>Acct</template> - <template #value><span class="_monospace">{{ acct(user) }}</span></template> - </FormKeyValueView> + <FormLink :to="userPage(user)">Profile</FormLink> - <FormKeyValueView> - <template #key>ID</template> - <template #value><span class="_monospace">{{ user.id }}</span></template> - </FormKeyValueView> - </FormGroup> + <div class="_formBlock"> + <MkKeyValue :copy="acct(user)" oneline style="margin: 1em 0;"> + <template #key>Acct</template> + <template #value><span class="_monospace">{{ acct(user) }}</span></template> + </MkKeyValue> - <FormGroup v-if="iAmModerator"> - <FormSwitch v-if="user.host == null && $i.isAdmin && (moderator || !user.isAdmin)" v-model="moderator" @update:modelValue="toggleModerator">{{ $ts.moderator }}</FormSwitch> - <FormSwitch v-model="silenced" @update:modelValue="toggleSilence">{{ $ts.silence }}</FormSwitch> - <FormSwitch v-model="suspended" @update:modelValue="toggleSuspend">{{ $ts.suspend }}</FormSwitch> - </FormGroup> + <MkKeyValue :copy="user.id" oneline style="margin: 1em 0;"> + <template #key>ID</template> + <template #value><span class="_monospace">{{ user.id }}</span></template> + </MkKeyValue> + </div> - <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> + <FormSection v-if="iAmModerator"> + <template #label>Moderation</template> + <FormSwitch v-if="user.host == null && $i.isAdmin && (moderator || !user.isAdmin)" v-model="moderator" class="_formBlock" @update:modelValue="toggleModerator">{{ $ts.moderator }}</FormSwitch> + <FormSwitch v-model="silenced" class="_formBlock" @update:modelValue="toggleSilence">{{ $ts.silence }}</FormSwitch> + <FormSwitch v-model="suspended" class="_formBlock" @update:modelValue="toggleSuspend">{{ $ts.suspend }}</FormSwitch> + <FormButton v-if="user.host == null && iAmModerator" class="_formBlock" @click="resetPassword"><i class="fas fa-key"></i> {{ $ts.resetPassword }}</FormButton> + </FormSection> - <FormGroup> - <FormLink :to="`/user-ap-info/${user.id}`">ActivityPub</FormLink> + <FormSection> + <template #label>ActivityPub</template> - <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> + <div class="_formBlock"> + <MkKeyValue v-if="user.host" oneline style="margin: 1em 0;"> + <template #key>{{ $ts.instanceInfo }}</template> + <template #value><MkA :to="`/instance-info/${user.host}`" class="_link">{{ user.host }} <i class="fas fa-angle-right"></i></MkA></template> + </MkKeyValue> + <MkKeyValue v-else oneline style="margin: 1em 0;"> + <template #key>{{ $ts.instanceInfo }}</template> + <template #value>(Local user)</template> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> + <template #key>{{ $ts.updatedAt }}</template> + <template #value><MkTime v-if="user.lastFetchedAt" mode="detail" :time="user.lastFetchedAt"/><span v-else>N/A</span></template> + </MkKeyValue> + <MkKeyValue v-if="ap" oneline style="margin: 1em 0;"> + <template #key>Type</template> + <template #value><span class="_monospace">{{ ap.type }}</span></template> + </MkKeyValue> + </div> - <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> + <FormButton v-if="user.host != null" class="_formBlock" @click="updateRemoteUser"><i class="fas fa-sync"></i> {{ $ts.updateRemoteUser }}</FormButton> + </FormSection> - <FormObjectView tall :value="user"> - <span>Raw</span> - </FormObjectView> + <MkObjectView tall :value="user"> + </MkObjectView> + </div> </FormSuspense> -</FormBase> +</MkSpacer> </template> <script lang="ts"> import { computed, defineAsyncComponent, defineComponent } from 'vue'; -import FormObjectView from '@/components/debobigego/object-view.vue'; -import FormTextarea from '@/components/debobigego/textarea.vue'; -import FormSwitch from '@/components/debobigego/switch.vue'; -import FormLink from '@/components/debobigego/link.vue'; -import FormBase from '@/components/debobigego/base.vue'; -import FormGroup from '@/components/debobigego/group.vue'; -import FormButton from '@/components/debobigego/button.vue'; -import FormKeyValueView from '@/components/debobigego/key-value-view.vue'; -import FormSuspense from '@/components/debobigego/suspense.vue'; +import MkObjectView from '@/components/object-view.vue'; +import FormTextarea from '@/components/form/textarea.vue'; +import FormSwitch from '@/components/form/switch.vue'; +import FormLink from '@/components/form/link.vue'; +import FormSection from '@/components/form/section.vue'; +import FormButton from '@/components/ui/button.vue'; +import MkKeyValue from '@/components/key-value.vue'; +import FormSuspense from '@/components/form/suspense.vue'; import * as os from '@/os'; import number from '@/filters/number'; import bytes from '@/filters/bytes'; @@ -74,14 +79,13 @@ import { userPage, acct } from '@/filters/user'; export default defineComponent({ components: { - FormBase, + FormSection, FormTextarea, FormSwitch, - FormObjectView, + MkObjectView, FormButton, FormLink, - FormGroup, - FormKeyValueView, + MkKeyValue, FormSuspense, }, @@ -97,6 +101,7 @@ export default defineComponent({ [symbols.PAGE_INFO]: computed(() => ({ title: this.user ? acct(this.user) : this.$ts.userInfo, icon: 'fas fa-info-circle', + bg: 'var(--bg)', actions: this.user ? [this.user.url ? { text: this.user.url, icon: 'fas fa-external-link-alt', @@ -108,6 +113,7 @@ export default defineComponent({ init: null, user: null, info: null, + ap: null, moderator: false, silenced: false, suspended: false, @@ -126,6 +132,13 @@ export default defineComponent({ this.init = this.createFetcher(); }, immediate: true + }, + user() { + os.api('ap/get', { + uri: this.user.uri || `${url}/users/${this.user.id}` + }).then(res => { + this.ap = res; + }); } }, @@ -234,7 +247,6 @@ export default defineComponent({ .aeakzknw { > .avatar { display: block; - margin: 0 auto; width: 64px; height: 64px; } diff --git a/packages/client/src/pages/user/clips.vue b/packages/client/src/pages/user/clips.vue index aad5317ce0..870e6f7174 100644 --- a/packages/client/src/pages/user/clips.vue +++ b/packages/client/src/pages/user/clips.vue @@ -28,7 +28,7 @@ export default defineComponent({ data() { return { pagination: { - endpoint: 'users/clips', + endpoint: 'users/clips' as const, limit: 20, params: { userId: this.user.id, diff --git a/packages/client/src/pages/user/follow-list.vue b/packages/client/src/pages/user/follow-list.vue index 9fb8943fb8..98a1fc0f86 100644 --- a/packages/client/src/pages/user/follow-list.vue +++ b/packages/client/src/pages/user/follow-list.vue @@ -1,6 +1,6 @@ <template> <div> - <MkPagination v-slot="{items}" ref="list" :pagination="pagination" class="mk-following-or-followers"> + <MkPagination v-slot="{items}" ref="list" :pagination="type === 'following' ? followingPagination : followersPagination" class="mk-following-or-followers"> <div class="users _isolated"> <MkUserInfo v-for="user in items.map(x => type === 'following' ? x.followee : x.follower)" :key="user.id" class="user" :user="user"/> </div> @@ -8,50 +8,32 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { computed } from 'vue'; +import * as misskey from 'misskey-js'; import MkUserInfo from '@/components/user-info.vue'; import MkPagination from '@/components/ui/pagination.vue'; -export default defineComponent({ - components: { - MkPagination, - MkUserInfo, - }, +const props = defineProps<{ + user: misskey.entities.User; + type: 'following' | 'followers'; +}>(); - props: { - user: { - type: Object, - required: true - }, - type: { - type: String, - required: true - }, - }, +const followingPagination = { + endpoint: 'users/following' as const, + limit: 20, + params: computed(() => ({ + userId: props.user.id, + })), +}; - 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(); - } - } -}); +const followersPagination = { + endpoint: 'users/followers' as const, + limit: 20, + params: computed(() => ({ + userId: props.user.id, + })), +}; </script> <style lang="scss" scoped> diff --git a/packages/client/src/pages/user/gallery.vue b/packages/client/src/pages/user/gallery.vue index 860aa9f44f..07dda4a292 100644 --- a/packages/client/src/pages/user/gallery.vue +++ b/packages/client/src/pages/user/gallery.vue @@ -9,7 +9,7 @@ </template> <script lang="ts"> -import { defineComponent } from 'vue'; +import { computed, defineComponent } from 'vue'; import MkGalleryPostPreview from '@/components/gallery-post-preview.vue'; import MkPagination from '@/components/ui/pagination.vue'; @@ -29,20 +29,14 @@ export default defineComponent({ data() { return { pagination: { - endpoint: 'users/gallery/posts', + endpoint: 'users/gallery/posts' as const, limit: 6, - params: () => ({ + params: computed(() => ({ userId: this.user.id - }) + })), }, }; }, - - watch: { - user() { - this.$refs.list.reload(); - } - } }); </script> diff --git a/packages/client/src/pages/user/index.activity.vue b/packages/client/src/pages/user/index.activity.vue index e51d6c6090..43a4f476f1 100644 --- a/packages/client/src/pages/user/index.activity.vue +++ b/packages/client/src/pages/user/index.activity.vue @@ -8,27 +8,16 @@ </MkContainer> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; -import * as os from '@/os'; +<script lang="ts" setup> +import { } from 'vue'; +import * as misskey from 'misskey-js'; import MkContainer from '@/components/ui/container.vue'; import MkChart from '@/components/chart.vue'; -export default defineComponent({ - components: { - MkContainer, - MkChart, - }, - props: { - user: { - type: Object, - required: true - }, - limit: { - type: Number, - required: false, - default: 40 - } - }, +const props = withDefaults(defineProps<{ + user: misskey.entities.User; + limit?: number; +}>(), { + limit: 40, }); </script> diff --git a/packages/client/src/pages/user/index.timeline.vue b/packages/client/src/pages/user/index.timeline.vue index 2ffa496979..a1329a7411 100644 --- a/packages/client/src/pages/user/index.timeline.vue +++ b/packages/client/src/pages/user/index.timeline.vue @@ -1,60 +1,36 @@ <template> <div v-sticky-container class="yrzkoczt"> - <MkTab v-model="with_" class="tab"> + <MkTab v-model="include" 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)"/> + <XNotes :no-gap="true" :pagination="pagination"/> </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { ref, computed } from 'vue'; +import * as misskey from 'misskey-js'; import XNotes from '@/components/notes.vue'; import MkTab from '@/components/tab.vue'; import * as os from '@/os'; -export default defineComponent({ - components: { - XNotes, - MkTab, - }, +const props = defineProps<{ + user: misskey.entities.UserDetailed; +}>(); - props: { - user: { - type: Object, - required: true, - }, - }, +const include = ref<string | null>(null); - 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), - }) - } - }; - }, - - watch: { - user() { - this.$refs.timeline.reload(); - }, - - with_() { - this.$refs.timeline.reload(); - }, - }, -}); +const pagination = { + endpoint: 'users/notes' as const, + limit: 10, + params: computed(() => ({ + userId: props.user.id, + includeReplies: include.value === 'replies', + withFiles: include.value === 'files', + })), +}; </script> <style lang="scss" scoped> diff --git a/packages/client/src/pages/user/index.vue b/packages/client/src/pages/user/index.vue index 0b96368587..599e24d81c 100644 --- a/packages/client/src/pages/user/index.vue +++ b/packages/client/src/pages/user/index.vue @@ -1,196 +1,125 @@ <template> <div> -<transition name="fade" mode="out-in"> - <div v-if="user && narrow === false" class="ftskorzw wide"> - <MkRemoteCaution v-if="user.host != null" :href="user.url"/> + <transition name="fade" mode="out-in"> + <MkSpacer v-if="user" :content-max="narrow ? 800 : 1100"> + <div v-size="{ max: [500] }" class="ftskorzw" :class="{ wide: !narrow }"> + <div class="main"> + <!-- 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="banner-container" :style="style"> - <div ref="banner" class="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 v-if="$i && $i.id != user.id && user.isFollowed" class="followed"><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 v-if="user.location" class="field"> - <dt class="name"><i class="fas fa-map-marker fa-fw"></i> {{ $ts.location }}</dt> - <dd class="value">{{ user.location }}</dd> - </dl> - <dl v-if="user.birthday" class="field"> - <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 v-if="user.fields.length > 0" class="fields"> - <dl v-for="(field, i) in user.fields" :key="i" class="field"> - <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 :key="user.id" :user="user" class="_gap"/> - <XPhotos :key="user.id" :user="user" class="_gap"/> - </div> - <div class="main"> - <div class="actions"> - <button class="menu _button" @click="menu"><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" :key="note.id" class="note _gap" :note="note" :pinned="true" @update:note="pinnedNoteUpdated(note, $event)"/> - </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 v-size="{ max: [500] }" class="ftskorzw narrow"> - <!-- 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="profile"> - <MkRemoteCaution v-if="user.host != null" :href="user.url" class="warn"/> - - <div :key="user.id" class="_block main"> - <div class="banner-container" :style="style"> - <div ref="banner" class="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 :key="user.id" class="_block main"> + <div class="banner-container" :style="style"> + <div ref="banner" class="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 v-if="$i && $i.id != user.id && user.isFollowed" class="followed">{{ $ts.followsYou }}</span> + <div v-if="$i" class="actions"> + <button class="menu _button" @click="menu"><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 v-if="user.location" class="field"> + <dt class="name"><i class="fas fa-map-marker fa-fw"></i> {{ $ts.location }}</dt> + <dd class="value">{{ user.location }}</dd> + </dl> + <dl v-if="user.birthday" class="field"> + <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 v-if="user.fields.length > 0" class="fields"> + <dl v-for="(field, i) in user.fields" :key="i" class="field"> + <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 v-click-anime :to="userPage(user)" :class="{ active: page === 'index' }"> + <b>{{ number(user.notesCount) }}</b> + <span>{{ $ts.notes }}</span> + </MkA> + <MkA v-click-anime :to="userPage(user, 'following')" :class="{ active: page === 'following' }"> + <b>{{ number(user.followingCount) }}</b> + <span>{{ $ts.following }}</span> + </MkA> + <MkA v-click-anime :to="userPage(user, 'followers')" :class="{ active: page === 'followers' }"> + <b>{{ number(user.followersCount) }}</b> + <span>{{ $ts.followers }}</span> + </MkA> </div> - </div> - <span v-if="$i && $i.id != user.id && user.isFollowed" class="followed">{{ $ts.followsYou }}</span> - <div v-if="$i" class="actions"> - <button class="menu _button" @click="menu"><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 v-if="user.location" class="field"> - <dt class="name"><i class="fas fa-map-marker fa-fw"></i> {{ $ts.location }}</dt> - <dd class="value">{{ user.location }}</dd> - </dl> - <dl v-if="user.birthday" class="field"> - <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 v-if="user.fields.length > 0" class="fields"> - <dl v-for="(field, i) in user.fields" :key="i" class="field"> - <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 v-click-anime :to="userPage(user)" :class="{ active: page === 'index' }"> - <b>{{ number(user.notesCount) }}</b> - <span>{{ $ts.notes }}</span> - </MkA> - <MkA v-click-anime :to="userPage(user, 'following')" :class="{ active: page === 'following' }"> - <b>{{ number(user.followingCount) }}</b> - <span>{{ $ts.following }}</span> - </MkA> - <MkA v-click-anime :to="userPage(user, 'followers')" :class="{ active: page === 'followers' }"> - <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" :key="note.id" class="note _block" :note="note" :pinned="true" @update:note="pinnedNoteUpdated(note, $event)"/> - </div> - <MkInfo v-else-if="$i && $i.id === user.id">{{ $ts.userPagePinTip }}</MkInfo> - <XPhotos :key="user.id" :user="user"/> - <XActivity :key="user.id" :user="user" style="margin-top: var(--margin);"/> - </div> - <div> - <XUserTimeline :user="user"/> + <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" :key="note.id" class="note _block" :note="note" :pinned="true"/> + </div> + <MkInfo v-else-if="$i && $i.id === user.id">{{ $ts.userPagePinTip }}</MkInfo> + <template v-if="narrow"> + <XPhotos :key="user.id" :user="user"/> + <XActivity :key="user.id" :user="user" style="margin-top: var(--margin);"/> + </template> + </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> - </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 v-if="!narrow" class="sub"> + <XPhotos :key="user.id" :user="user"/> + <XActivity :key="user.id" :user="user" style="margin-top: var(--margin);"/> + </div> </div> - </div> - </MkSpacer> - <MkError v-else-if="error" @retry="fetch()"/> - <MkLoading v-else/> -</transition> + </MkSpacer> + <MkError v-else-if="error" @retry="fetch()"/> + <MkLoading v-else/> + </transition> </div> </template> @@ -314,7 +243,7 @@ export default defineComponent({ mounted() { window.requestAnimationFrame(this.parallaxLoop); - this.narrow = true//this.$el.clientWidth < 1000; + this.narrow = this.$el.clientWidth < 1000; }, beforeUnmount() { @@ -356,11 +285,6 @@ export default defineComponent({ 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 @@ -378,447 +302,289 @@ export default defineComponent({ opacity: 0; } -.ftskorzw.wide { +.ftskorzw { - > .banner-container { - position: relative; - height: 300px; - overflow: hidden; - background-size: cover; - background-position: center; + > .main { - > .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; + > .punished { + font-size: 0.8em; + padding: 16px; } - } - - > .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; - } - } + > .profile { - > .status { - display: flex; - padding: 20px 16px; - border-top: solid 0.5px var(--divider); - font-size: 90%; + > .main { + position: relative; + overflow: hidden; - > a { - flex: 1; - text-align: center; + > .banner-container { + position: relative; + height: 250px; + overflow: hidden; + background-size: cover; + background-position: center; - &.active { - color: var(--accent); + > .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; } - &:hover { - text-decoration: none; + > .fade { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 78px; + background: linear-gradient(transparent, rgba(#000, 0.7)); } - > b { - display: block; - line-height: 16px; + > .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; } - > span { - font-size: 75%; - } - } - } + > .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; - > .description { - padding: 20px 16px; - border-top: solid 0.5px var(--divider); - font-size: 90%; - } + > .menu { + vertical-align: bottom; + height: 31px; + width: 31px; + color: #fff; + text-shadow: 0 0 8px #000; + font-size: 16px; + } - > .fields { - padding: 20px 16px; - border-top: solid 0.5px var(--divider); - font-size: 90%; + > .koudoku { + margin-left: 4px; + vertical-align: bottom; + } + } - > .field { - display: flex; - padding: 0; - margin: 0; - align-items: center; + > .title { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + padding: 0 0 8px 154px; + box-sizing: border-box; + color: #fff; - &:not(:last-child) { - margin-bottom: 8px; - } + > .name { + display: block; + margin: 0; + line-height: 32px; + font-weight: bold; + font-size: 1.8em; + text-shadow: 0 0 8px #000; + } - > .name { - width: 30%; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - font-weight: bold; - } + > .bottom { + > * { + display: inline-block; + margin-right: 16px; + line-height: 20px; + opacity: 0.8; - > .value { - width: 70%; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - margin: 0; + &.username { + font-weight: bold; + } + } + } } } - } - } - > .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; + > .title { + display: none; text-align: center; - border-bottom: solid 3px transparent; - - &:hover { - text-decoration: none; - } - - &.active { - color: var(--accent); - border-bottom-color: var(--accent); - } + padding: 50px 8px 16px 8px; + font-weight: bold; + border-bottom: solid 0.5px var(--divider); - &:not(.active):hover { - color: var(--fgHighlighted); + > .bottom { + > * { + display: inline-block; + margin-right: 8px; + opacity: 0.8; + } } + } - > .icon { - margin-right: 6px; - } + > .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); } - > .actions { - display: flex; - align-items: center; - margin-left: auto; + > .description { + padding: 24px 24px 24px 154px; + font-size: 0.95em; - > .menu { - padding: 12px 16px; + > .empty { + margin: 0; + opacity: 0.5; } } - } - } - } -} - -.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; - } + > .fields { + padding: 24px; + font-size: 0.9em; + border-top: solid 0.5px var(--divider); - > .fade { - position: absolute; - bottom: 0; - left: 0; - width: 100%; - height: 78px; - background: linear-gradient(transparent, rgba(#000, 0.7)); - } + > .field { + display: flex; + padding: 0; + margin: 0; + align-items: center; - > .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; - } + &:not(:last-child) { + margin-bottom: 8px; + } - > .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; + > .name { + width: 30%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + font-weight: bold; + text-align: center; + } - > .menu { - vertical-align: bottom; - height: 31px; - width: 31px; - color: #fff; - text-shadow: 0 0 8px #000; - font-size: 16px; + > .value { + width: 70%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + margin: 0; + } } - > .koudoku { - margin-left: 4px; - vertical-align: bottom; + &.system > .field > .name { } } - > .title { - position: absolute; - bottom: 0; - left: 0; - width: 100%; - padding: 0 0 8px 154px; - box-sizing: border-box; - color: #fff; + > .status { + display: flex; + padding: 24px; + border-top: solid 0.5px var(--divider); - > .name { - display: block; - margin: 0; - line-height: 32px; - font-weight: bold; - font-size: 1.8em; - text-shadow: 0 0 8px #000; - } + > a { + flex: 1; + text-align: center; - > .bottom { - > * { - display: inline-block; - margin-right: 16px; - line-height: 20px; - opacity: 0.8; + &.active { + color: var(--accent); + } - &.username { - font-weight: bold; - } + &:hover { + text-decoration: none; } - } - } - } - > .title { - display: none; - text-align: center; - padding: 50px 8px 16px 8px; - font-weight: bold; - border-bottom: solid 0.5px var(--divider); + > b { + display: block; + line-height: 16px; + } - > .bottom { - > * { - display: inline-block; - margin-right: 8px; - opacity: 0.8; + > span { + font-size: 70%; + } } } } + } - > .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; - } + > .contents { + > .content { + margin-bottom: var(--margin); } + } + } - > .fields { - padding: 24px; - font-size: 0.9em; - border-top: solid 0.5px var(--divider); - - > .field { - display: flex; - padding: 0; - margin: 0; - align-items: center; + &.max-width_500px { + > .main { + > .profile > .main { + > .banner-container { + height: 140px; - &:not(:last-child) { - margin-bottom: 8px; + > .fade { + display: none; } - > .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; + > .title { + display: none; } } - &.system > .field > .name { + > .title { + display: block; } - } - > .status { - display: flex; - padding: 24px; - border-top: solid 0.5px var(--divider); + > .avatar { + top: 90px; + left: 0; + right: 0; + width: 92px; + height: 92px; + margin: auto; + } - > a { - flex: 1; + > .description { + padding: 16px; 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; + > .fields { + padding: 16px; } - > .title { - display: none; + > .status { + padding: 16px; } } - > .title { - display: block; - } - - > .avatar { - top: 90px; - left: 0; - right: 0; - width: 92px; - height: 92px; - margin: auto; - } - - > .description { - padding: 16px; - text-align: center; + > .contents { + > .nav { + font-size: 80%; + } } + } + } - > .fields { - padding: 16px; - } + &.wide { + display: flex; + width: 100%; - > .status { - padding: 16px; - } + > .main { + width: 100%; + min-width: 0; } - > .contents { - > .nav { - font-size: 80%; - } + > .sub { + max-width: 350px; + min-width: 350px; + margin-left: var(--margin); } } } diff --git a/packages/client/src/pages/user/pages.vue b/packages/client/src/pages/user/pages.vue index 40d1fe3842..ad101158e0 100644 --- a/packages/client/src/pages/user/pages.vue +++ b/packages/client/src/pages/user/pages.vue @@ -6,42 +6,23 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { computed } from 'vue'; +import * as misskey from 'misskey-js'; import MkPagePreview from '@/components/page-preview.vue'; import MkPagination from '@/components/ui/pagination.vue'; -export default defineComponent({ - components: { - MkPagination, - MkPagePreview, - }, +const props = defineProps<{ + user: misskey.entities.User; +}>(); - 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(); - } - } -}); +const pagination = { + endpoint: 'users/pages' as const, + limit: 20, + params: computed(() => ({ + userId: props.user.id, + })), +}; </script> <style lang="scss" scoped> diff --git a/packages/client/src/pages/user/reactions.vue b/packages/client/src/pages/user/reactions.vue index 69c27de55b..d2c1f92ebb 100644 --- a/packages/client/src/pages/user/reactions.vue +++ b/packages/client/src/pages/user/reactions.vue @@ -7,50 +7,30 @@ <MkReactionIcon class="reaction" :reaction="item.type" :custom-emojis="item.note.emojis" :no-style="true"/> <MkTime :time="item.createdAt" class="createdAt"/> </div> - <MkNote :key="item.id" :note="item.note" @update:note="updated(note, $event)"/> + <MkNote :key="item.id" :note="item.note"/> </div> </MkPagination> </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { computed } from 'vue'; +import * as misskey from 'misskey-js'; import MkPagination from '@/components/ui/pagination.vue'; import MkNote from '@/components/note.vue'; import MkReactionIcon from '@/components/reaction-icon.vue'; -export default defineComponent({ - components: { - MkPagination, - MkNote, - MkReactionIcon, - }, +const props = defineProps<{ + user: misskey.entities.User; +}>(); - 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(); - } - }, -}); +const pagination = { + endpoint: 'users/reactions' as const, + limit: 20, + params: computed(() => ({ + userId: props.user.id, + })), +}; </script> <style lang="scss" scoped> diff --git a/packages/client/src/pages/v.vue b/packages/client/src/pages/v.vue deleted file mode 100644 index 3b1bb20861..0000000000 --- a/packages/client/src/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 '@/config'; -import * as symbols from '@/symbols'; - -export default defineComponent({ - data() { - return { - [symbols.PAGE_INFO]: { - title: 'Misskey', - icon: null - }, - version, - } - }, -}); -</script> |