summaryrefslogtreecommitdiff
path: root/src/client
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2020-11-25 21:31:34 +0900
committerGitHub <noreply@github.com>2020-11-25 21:31:34 +0900
commit014440850014ee86d766bb07467c2970b17a1fc6 (patch)
treeffb652fe1db3365d430ed72ec2c62aaacfbe21fb /src/client
parentフォントレンダリングを調整 (diff)
downloadmisskey-014440850014ee86d766bb07467c2970b17a1fc6.tar.gz
misskey-014440850014ee86d766bb07467c2970b17a1fc6.tar.bz2
misskey-014440850014ee86d766bb07467c2970b17a1fc6.zip
nanka iroiro (#6853)
* wip * Update maps.ts * wip * wip * wip * wip * Update base.vue * wip * wip * wip * wip * Update link.vue * wip * wip * wip * wip * wip * wip * wip * wip * wip * Update privacy.vue * wip * wip * wip * wip * Update range.vue * wip * wip * wip * wip * Update profile.vue * wip * Update a.vue * Update index.vue * wip * Update sidebar.vue * wip * wip * Update account-info.vue * Update a.vue * wip * wip * Update sounds.vue * wip * wip * wip * wip * wip * wip * wip * wip * Update account-info.vue * Update account-info.vue * wip * wip * wip * Update d-persimmon.json5 * wip
Diffstat (limited to 'src/client')
-rw-r--r--src/client/assets/sounds/syuilo/kick.mp3bin0 -> 15672 bytes
-rw-r--r--src/client/assets/sounds/syuilo/snare.mp3bin0 -> 26121 bytes
-rw-r--r--src/client/cold-storage.ts34
-rw-r--r--src/client/components/form-dialog.vue63
-rw-r--r--src/client/components/form/base.vue56
-rw-r--r--src/client/components/form/button.vue81
-rw-r--r--src/client/components/form/form.scss34
-rw-r--r--src/client/components/form/group.vue42
-rw-r--r--src/client/components/form/input.vue306
-rw-r--r--src/client/components/form/key-value-view.vue30
-rw-r--r--src/client/components/form/link.vue90
-rw-r--r--src/client/components/form/pagination.vue42
-rw-r--r--src/client/components/form/radios.vue106
-rw-r--r--src/client/components/form/range.vue122
-rw-r--r--src/client/components/form/select.vue147
-rw-r--r--src/client/components/form/switch.vue132
-rw-r--r--src/client/components/form/textarea.vue136
-rw-r--r--src/client/components/form/tuple.vue36
-rw-r--r--src/client/components/media-image.vue2
-rw-r--r--src/client/components/media-video.vue2
-rw-r--r--src/client/components/taskmanager.api-window.vue5
-rw-r--r--src/client/components/taskmanager.vue3
-rw-r--r--src/client/components/timeline.vue3
-rw-r--r--src/client/components/ui/range.vue4
-rw-r--r--src/client/components/ui/switch.vue6
-rw-r--r--src/client/components/ui/textarea.vue3
-rw-r--r--src/client/init.ts11
-rw-r--r--src/client/os.ts10
-rw-r--r--src/client/pages/announcements.vue2
-rw-r--r--src/client/pages/instance/settings.vue8
-rw-r--r--src/client/pages/messaging/messaging-room.vue3
-rw-r--r--src/client/pages/reversi/game.board.vue13
-rw-r--r--src/client/pages/settings/2fa.vue (renamed from src/client/pages/settings/security.2fa.vue)13
-rw-r--r--src/client/pages/settings/account-info.vue185
-rw-r--r--src/client/pages/settings/api.vue27
-rw-r--r--src/client/pages/settings/apps.vue (renamed from src/client/pages/apps.vue)60
-rw-r--r--src/client/pages/settings/deck.vue90
-rw-r--r--src/client/pages/settings/email-address.vue71
-rw-r--r--src/client/pages/settings/email.vue52
-rw-r--r--src/client/pages/settings/general.vue209
-rw-r--r--src/client/pages/settings/index.vue149
-rw-r--r--src/client/pages/settings/notifications.vue30
-rw-r--r--src/client/pages/settings/other.vue53
-rw-r--r--src/client/pages/settings/privacy.vue56
-rw-r--r--src/client/pages/settings/profile.vue232
-rw-r--r--src/client/pages/settings/reaction.vue69
-rw-r--r--src/client/pages/settings/security.vue85
-rw-r--r--src/client/pages/settings/sidebar.vue56
-rw-r--r--src/client/pages/settings/sounds.vue200
-rw-r--r--src/client/pages/settings/theme.install.vue106
-rw-r--r--src/client/pages/settings/theme.manage.vue103
-rw-r--r--src/client/pages/settings/theme.vue557
-rw-r--r--src/client/pages/settings/word-mute.vue48
-rw-r--r--src/client/pages/user/follow-list.vue2
-rw-r--r--src/client/pages/user/index.activity.vue18
-rw-r--r--src/client/pages/user/index.photos.vue42
-rw-r--r--src/client/pages/user/index.vue538
-rw-r--r--src/client/pages/welcome.entrance.vue28
-rw-r--r--src/client/router.ts3
-rw-r--r--src/client/scripts/sound.ts24
-rw-r--r--src/client/scripts/theme.ts13
-rw-r--r--src/client/store.ts10
-rw-r--r--src/client/style.scss6
-rw-r--r--src/client/themes/_dark.json51
-rw-r--r--src/client/themes/_light.json51
-rw-r--r--src/client/themes/d-battery-saver.json518
-rw-r--r--src/client/themes/d-black.json524
-rw-r--r--src/client/themes/d-blue.json529
-rw-r--r--src/client/themes/d-dark.json5 (renamed from src/client/themes/d-red.json5)14
-rw-r--r--src/client/themes/d-green.json529
-rw-r--r--src/client/themes/d-persimmon.json512
-rw-r--r--src/client/themes/l-apricot.json52
-rw-r--r--src/client/themes/l-blue.json521
-rw-r--r--src/client/themes/l-green.json521
-rw-r--r--src/client/themes/l-light.json5 (renamed from src/client/themes/l-white.json5)2
-rw-r--r--src/client/themes/l-red.json521
-rw-r--r--src/client/ui/_common_/common.vue5
-rw-r--r--src/client/ui/visitor.vue202
-rw-r--r--src/client/ui/visitor/a.vue357
-rw-r--r--src/client/ui/visitor/b.vue372
-rw-r--r--src/client/widgets/digital-clock.vue3
81 files changed, 4126 insertions, 1675 deletions
diff --git a/src/client/assets/sounds/syuilo/kick.mp3 b/src/client/assets/sounds/syuilo/kick.mp3
new file mode 100644
index 0000000000..4e0e72091c
--- /dev/null
+++ b/src/client/assets/sounds/syuilo/kick.mp3
Binary files differ
diff --git a/src/client/assets/sounds/syuilo/snare.mp3 b/src/client/assets/sounds/syuilo/snare.mp3
new file mode 100644
index 0000000000..9244189c2d
--- /dev/null
+++ b/src/client/assets/sounds/syuilo/snare.mp3
Binary files differ
diff --git a/src/client/cold-storage.ts b/src/client/cold-storage.ts
new file mode 100644
index 0000000000..1bee2313fa
--- /dev/null
+++ b/src/client/cold-storage.ts
@@ -0,0 +1,34 @@
+// 常にメモリにロードしておく必要がないような設定情報を保管するストレージ
+
+const PREFIX = 'miux:';
+
+export const defaultDeviceSettings = {
+ sound_masterVolume: 0.3,
+ sound_note: { type: 'syuilo/down', volume: 1 },
+ sound_noteMy: { type: 'syuilo/up', volume: 1 },
+ sound_notification: { type: 'syuilo/pope2', volume: 1 },
+ sound_chat: { type: 'syuilo/pope1', volume: 1 },
+ sound_chatBg: { type: 'syuilo/waon', volume: 1 },
+ sound_antenna: { type: 'syuilo/triple', volume: 1 },
+ sound_channel: { type: 'syuilo/square-pico', volume: 1 },
+ sound_reversiPutBlack: { type: 'syuilo/kick', volume: 0.3 },
+ sound_reversiPutWhite: { type: 'syuilo/snare', volume: 0.3 },
+};
+
+export const device = {
+ get<T extends keyof typeof defaultDeviceSettings>(key: T): typeof defaultDeviceSettings[T] {
+ // TODO: indexedDBにする
+ // ただしその際はnullチェックではなくキー存在チェックにしないとダメ
+ // (indexedDBはnullを保存できるため、ユーザーが意図してnullを格納した可能性がある)
+ const value = localStorage.getItem(PREFIX + key);
+ if (value == null) {
+ return defaultDeviceSettings[key];
+ } else {
+ return JSON.parse(value);
+ }
+ },
+
+ set(key: keyof typeof defaultDeviceSettings, value: any): any {
+ localStorage.setItem(PREFIX + key, JSON.stringify(value));
+ },
+};
diff --git a/src/client/components/form-dialog.vue b/src/client/components/form-dialog.vue
index 0dc02258af..add6b230d3 100644
--- a/src/client/components/form-dialog.vue
+++ b/src/client/components/form-dialog.vue
@@ -1,6 +1,6 @@
<template>
<XModalWindow ref="dialog"
- :width="400"
+ :width="450"
:can-close="false"
:with-ok-button="true"
:ok-button-disabled="false"
@@ -12,42 +12,61 @@
<template #header>
{{ title }}
</template>
- <div class="xkpnjxcv _section">
- <label v-for="item in Object.keys(form).filter(item => !form[item].hidden)" :key="item">
- <MkInput v-if="form[item].type === 'number'" v-model:value="values[item]" type="number" :step="form[item].step || 1">
+ <FormBase class="xkpnjxcv">
+ <template v-for="item in Object.keys(form).filter(item => !form[item].hidden)">
+ <FormInput v-if="form[item].type === 'number'" v-model:value="values[item]" type="number" :step="form[item].step || 1">
<span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $t('optional') }})</span>
<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
- </MkInput>
- <MkInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model:value="values[item]" type="text">
+ </FormInput>
+ <FormInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model:value="values[item]" type="text">
<span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $t('optional') }})</span>
<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
- </MkInput>
- <MkTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model:value="values[item]">
+ </FormInput>
+ <FormTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model:value="values[item]">
<span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $t('optional') }})</span>
<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
- </MkTextarea>
- <MkSwitch v-else-if="form[item].type === 'boolean'" v-model:value="values[item]">
+ </FormTextarea>
+ <FormSwitch v-else-if="form[item].type === 'boolean'" v-model:value="values[item]">
<span v-text="form[item].label || item"></span>
<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
- </MkSwitch>
- </label>
- </div>
+ </FormSwitch>
+ <FormSelect v-else-if="form[item].type === 'enum'" v-model:value="values[item]">
+ <template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $t('optional') }})</span></template>
+ <option v-for="item in form[item].enum" :value="item.value" :key="item.value">{{ item.label }}</option>
+ </FormSelect>
+ <FormRange v-else-if="form[item].type === 'range'" v-model:value="values[item]" :min="form[item].mim" :max="form[item].max" :step="form[item].step">
+ <template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $t('optional') }})</span></template>
+ <template v-if="form[item].description" #desc>{{ form[item].description }}</template>
+ </FormRange>
+ <FormButton v-else-if="form[item].type === 'button'" @click="form[item].action($event, values)">
+ <span v-text="form[item].content || item"></span>
+ </FormButton>
+ </template>
+ </FormBase>
</XModalWindow>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import XModalWindow from '@/components/ui/modal-window.vue';
-import MkInput from './ui/input.vue';
-import MkTextarea from './ui/textarea.vue';
-import MkSwitch from './ui/switch.vue';
+import FormBase from './form/base.vue';
+import FormInput from './form/input.vue';
+import FormTextarea from './form/textarea.vue';
+import FormSwitch from './form/switch.vue';
+import FormSelect from './form/select.vue';
+import FormRange from './form/range.vue';
+import FormButton from './form/button.vue';
export default defineComponent({
components: {
XModalWindow,
- MkInput,
- MkTextarea,
- MkSwitch,
+ FormBase,
+ FormInput,
+ FormTextarea,
+ FormSwitch,
+ FormSelect,
+ FormRange,
+ FormButton,
},
props: {
@@ -95,12 +114,6 @@ export default defineComponent({
<style lang="scss" scoped>
.xkpnjxcv {
- > label {
- display: block;
- &:not(:last-child) {
- margin-bottom: 32px;
- }
- }
}
</style>
diff --git a/src/client/components/form/base.vue b/src/client/components/form/base.vue
new file mode 100644
index 0000000000..249b49c675
--- /dev/null
+++ b/src/client/components/form/base.vue
@@ -0,0 +1,56 @@
+<template>
+<div class="rbusrurv" :class="{ wide: forceWide }" v-size="{ max: [400] }">
+ <slot></slot>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+
+export default defineComponent({
+ props: {
+ forceWide: {
+ type: Boolean,
+ required: false,
+ default: false,
+ }
+ }
+});
+</script>
+
+<style lang="scss" scoped>
+.rbusrurv {
+ line-height: 1.4em;
+ background: var(--bg);
+ padding: 32px;
+
+ &:not(.wide).max-width_400px {
+ padding: 32px 0;
+
+ > ::v-deep(*) {
+ ._formPanel {
+ border: solid 0.5px var(--divider);
+ border-radius: 0;
+ border-left: none;
+ border-right: none;
+ }
+
+ ._form_group {
+ > * {
+ &:not(:first-child) {
+ &._formPanel, ._formPanel {
+ border-top: none;
+ }
+ }
+
+ &:not(:last-child) {
+ &._formPanel, ._formPanel {
+ border-bottom: solid 0.5px var(--divider);
+ }
+ }
+ }
+ }
+ }
+ }
+}
+</style>
diff --git a/src/client/components/form/button.vue b/src/client/components/form/button.vue
new file mode 100644
index 0000000000..b4f0890945
--- /dev/null
+++ b/src/client/components/form/button.vue
@@ -0,0 +1,81 @@
+<template>
+<div class="yzpgjkxe _formItem">
+ <div class="_formLabel"><slot name="label"></slot></div>
+ <button class="main _button _formPanel _formClickable" :class="{ center, primary, danger }">
+ <slot></slot>
+ <div class="suffix">
+ <slot name="suffix"></slot>
+ <div class="icon">
+ <slot name="suffixIcon"></slot>
+ </div>
+ </div>
+ </button>
+ <div class="_formCaption"><slot name="desc"></slot></div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import './form.scss';
+
+export default defineComponent({
+ props: {
+ primary: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ danger: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ center: {
+ type: Boolean,
+ required: false,
+ default: true,
+ }
+ },
+});
+</script>
+
+<style lang="scss" scoped>
+.yzpgjkxe {
+ > .main {
+ display: flex;
+ width: 100%;
+ box-sizing: border-box;
+ padding: 14px 16px;
+ text-align: left;
+ align-items: center;
+
+ &.center {
+ display: block;
+ text-align: center;
+ }
+
+ &.primary {
+ color: var(--accent);
+ }
+
+ &.danger {
+ color: #ff2a2a;
+ }
+
+ > .suffix {
+ display: inline-flex;
+ margin-left: auto;
+ opacity: 0.7;
+
+ > .icon {
+ margin-left: 1em;
+ }
+ }
+ }
+}
+</style>
diff --git a/src/client/components/form/form.scss b/src/client/components/form/form.scss
new file mode 100644
index 0000000000..b541bf826d
--- /dev/null
+++ b/src/client/components/form/form.scss
@@ -0,0 +1,34 @@
+._formPanel {
+ background: var(--panel);
+ border-radius: var(--radius);
+
+ &._formClickable {
+ &:hover {
+ background: var(--panelHighlight);
+ }
+ }
+}
+
+._formLabel {
+ font-size: 80%;
+ padding: 0 16px 8px 16px;
+
+ &:empty {
+ display: none;
+ }
+}
+
+._formCaption {
+ font-size: 80%;
+ padding: 8px 16px 0 16px;
+
+ &:empty {
+ display: none;
+ }
+}
+
+._formItem {
+ & + ._formItem {
+ margin-top: 24px;
+ }
+}
diff --git a/src/client/components/form/group.vue b/src/client/components/form/group.vue
new file mode 100644
index 0000000000..d07852155a
--- /dev/null
+++ b/src/client/components/form/group.vue
@@ -0,0 +1,42 @@
+<template>
+<div class="vrtktovg _formItem" v-size="{ max: [500] }">
+ <div class="_formLabel"><slot name="label"></slot></div>
+ <div class="main _form_group">
+ <slot></slot>
+ </div>
+ <div class="_formCaption"><slot name="caption"></slot></div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+
+export default defineComponent({
+});
+</script>
+
+<style lang="scss" scoped>
+.vrtktovg {
+ > .main {
+ > ::v-deep(*) {
+ margin: 0;
+
+ &:not(:first-child) {
+ &._formPanel, ._formPanel {
+ border-top: none;
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+ }
+ }
+
+ &:not(:last-child) {
+ &._formPanel, ._formPanel {
+ border-bottom: solid 0.5px var(--divider);
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+ }
+ }
+ }
+ }
+}
+</style>
diff --git a/src/client/components/form/input.vue b/src/client/components/form/input.vue
new file mode 100644
index 0000000000..89551a5fc2
--- /dev/null
+++ b/src/client/components/form/input.vue
@@ -0,0 +1,306 @@
+<template>
+<div class="ztzhwixg _formItem" :class="{ inline, disabled }">
+ <div class="_formLabel"><slot></slot></div>
+ <div class="icon" ref="icon"><slot name="icon"></slot></div>
+ <div class="input _formPanel">
+ <div class="prefix" ref="prefixEl"><slot name="prefix"></slot></div>
+ <input v-if="debounce" ref="inputEl"
+ v-debounce="500"
+ :type="type"
+ v-model.lazy="v"
+ :disabled="disabled"
+ :required="required"
+ :readonly="readonly"
+ :placeholder="placeholder"
+ :pattern="pattern"
+ :autocomplete="autocomplete"
+ :spellcheck="spellcheck"
+ :step="step"
+ @focus="focused = true"
+ @blur="focused = false"
+ @keydown="onKeydown($event)"
+ @input="onInput"
+ :list="id"
+ >
+ <input v-else ref="inputEl"
+ :type="type"
+ v-model="v"
+ :disabled="disabled"
+ :required="required"
+ :readonly="readonly"
+ :placeholder="placeholder"
+ :pattern="pattern"
+ :autocomplete="autocomplete"
+ :spellcheck="spellcheck"
+ :step="step"
+ @focus="focused = true"
+ @blur="focused = false"
+ @keydown="onKeydown($event)"
+ @input="onInput"
+ :list="id"
+ >
+ <datalist :id="id" v-if="datalist">
+ <option v-for="data in datalist" :value="data"/>
+ </datalist>
+ <div class="suffix" ref="suffixEl"><slot name="suffix"></slot></div>
+ </div>
+ <button class="save _textButton" v-if="save && changed" @click="() => { changed = false; save(); }">{{ $t('save') }}</button>
+ <div class="_formCaption"><slot name="desc"></slot></div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
+import debounce from 'v-debounce';
+import { faExclamationCircle } from '@fortawesome/free-solid-svg-icons';
+import './form.scss';
+
+export default defineComponent({
+ directives: {
+ debounce
+ },
+ props: {
+ value: {
+ required: false
+ },
+ type: {
+ type: String,
+ required: false
+ },
+ required: {
+ type: Boolean,
+ required: false
+ },
+ readonly: {
+ type: Boolean,
+ required: false
+ },
+ disabled: {
+ type: Boolean,
+ required: false
+ },
+ pattern: {
+ type: String,
+ required: false
+ },
+ placeholder: {
+ type: String,
+ required: false
+ },
+ autofocus: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
+ autocomplete: {
+ required: false
+ },
+ spellcheck: {
+ required: false
+ },
+ step: {
+ required: false
+ },
+ debounce: {
+ required: false
+ },
+ datalist: {
+ type: Array,
+ required: false,
+ },
+ inline: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
+ save: {
+ type: Function,
+ required: false,
+ },
+ },
+ emits: ['change', 'keydown', 'enter'],
+ setup(props, context) {
+ const { value, type, autofocus } = toRefs(props);
+ const v = ref(value.value);
+ const id = Math.random().toString(); // TODO: uuid?
+ const focused = ref(false);
+ const changed = ref(false);
+ const invalid = ref(false);
+ const filled = computed(() => v.value !== '' && v.value != null);
+ const inputEl = ref(null);
+ const prefixEl = ref(null);
+ const suffixEl = ref(null);
+
+ const focus = () => inputEl.value.focus();
+ const onInput = (ev) => {
+ changed.value = true;
+ context.emit('change', ev);
+ };
+ const onKeydown = (ev: KeyboardEvent) => {
+ context.emit('keydown', ev);
+
+ if (ev.code === 'Enter') {
+ context.emit('enter');
+ }
+ };
+
+ watch(value, newValue => {
+ v.value = newValue;
+ });
+
+ watch(v, newValue => {
+ if (type?.value === 'number') {
+ context.emit('update:value', parseFloat(newValue));
+ } else {
+ context.emit('update:value', newValue);
+ }
+
+ invalid.value = inputEl.value.validity.badInput;
+ });
+
+ onMounted(() => {
+ nextTick(() => {
+ if (autofocus.value) {
+ focus();
+ }
+
+ // このコンポーネントが作成された時、非表示状態である場合がある
+ // 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する
+ const clock = setInterval(() => {
+ if (prefixEl.value) {
+ if (prefixEl.value.offsetWidth) {
+ inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + 'px';
+ }
+ }
+ if (suffixEl.value) {
+ if (suffixEl.value.offsetWidth) {
+ inputEl.value.style.paddingRight = suffixEl.value.offsetWidth + 'px';
+ }
+ }
+ }, 100);
+
+ onUnmounted(() => {
+ clearInterval(clock);
+ });
+ });
+ });
+
+ return {
+ id,
+ v,
+ focused,
+ invalid,
+ changed,
+ filled,
+ inputEl,
+ prefixEl,
+ suffixEl,
+ focus,
+ onInput,
+ onKeydown,
+ faExclamationCircle,
+ };
+ },
+});
+</script>
+
+<style lang="scss" scoped>
+.ztzhwixg {
+ position: relative;
+
+ > .icon {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 24px;
+ text-align: center;
+ line-height: 32px;
+
+ &:not(:empty) + .input {
+ margin-left: 28px;
+ }
+ }
+
+ > .input {
+ $height: 52px;
+ position: relative;
+
+ > input {
+ display: block;
+ height: $height;
+ width: 100%;
+ margin: 0;
+ padding: 0 16px;
+ font: inherit;
+ font-weight: normal;
+ font-size: 1em;
+ line-height: $height;
+ color: var(--inputText);
+ background: transparent;
+ border: none;
+ border-radius: 0;
+ outline: none;
+ box-shadow: none;
+ box-sizing: border-box;
+
+ &[type='file'] {
+ display: none;
+ }
+ }
+
+ > .prefix,
+ > .suffix {
+ display: block;
+ position: absolute;
+ z-index: 1;
+ top: 0;
+ padding: 0 16px;
+ font-size: 1em;
+ line-height: $height;
+ color: var(--inputLabel);
+ pointer-events: none;
+
+ &:empty {
+ display: none;
+ }
+
+ > * {
+ display: inline-block;
+ min-width: 16px;
+ max-width: 150px;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+ }
+
+ > .prefix {
+ left: 0;
+ padding-right: 8px;
+ }
+
+ > .suffix {
+ right: 0;
+ padding-left: 8px;
+ }
+ }
+
+ > .save {
+ margin: 6px 0 0 0;
+ font-size: 0.8em;
+ }
+
+ &.inline {
+ display: inline-block;
+ margin: 0;
+ }
+
+ &.disabled {
+ opacity: 0.7;
+
+ &, * {
+ cursor: not-allowed !important;
+ }
+ }
+}
+</style>
diff --git a/src/client/components/form/key-value-view.vue b/src/client/components/form/key-value-view.vue
new file mode 100644
index 0000000000..eadc675f89
--- /dev/null
+++ b/src/client/components/form/key-value-view.vue
@@ -0,0 +1,30 @@
+<template>
+<div class="_formItem">
+ <div class="_formPanel anocepby">
+ <span class="key"><slot name="key"></slot></span>
+ <span class="value"><slot name="value"></slot></span>
+ </div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import './form.scss';
+
+export default defineComponent({
+
+});
+</script>
+
+<style lang="scss" scoped>
+.anocepby {
+ display: flex;
+ align-items: center;
+ padding: 14px 16px;
+
+ > .value {
+ margin-left: auto;
+ opacity: 0.7;
+ }
+}
+</style>
diff --git a/src/client/components/form/link.vue b/src/client/components/form/link.vue
new file mode 100644
index 0000000000..01c46e851a
--- /dev/null
+++ b/src/client/components/form/link.vue
@@ -0,0 +1,90 @@
+<template>
+<div class="qmfkfnzi _formItem">
+ <a class="main _button _formPanel _formClickable" :href="to" target="_blank" v-if="external">
+ <span class="icon"><slot name="icon"></slot></span>
+ <span class="text"><slot></slot></span>
+ <Fa :icon="faExternalLinkAlt" class="right"/>
+ </a>
+ <MkA class="main _button _formPanel _formClickable" :class="{ active }" :to="to" v-else>
+ <span class="icon"><slot name="icon"></slot></span>
+ <span class="text"><slot></slot></span>
+ <Fa :icon="faChevronRight" class="right"/>
+ </MkA>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { faChevronRight, faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons';
+import './form.scss';
+
+export default defineComponent({
+ props: {
+ to: {
+ type: String,
+ required: true
+ },
+ active: {
+ type: Boolean,
+ required: false
+ },
+ external: {
+ type: Boolean,
+ required: false
+ },
+ },
+ data() {
+ return {
+ faChevronRight, faExternalLinkAlt
+ };
+ }
+});
+</script>
+
+<style lang="scss" scoped>
+.qmfkfnzi {
+ > .main {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ box-sizing: border-box;
+ padding: 14px 16px 14px 14px;
+
+ &:hover {
+ text-decoration: none;
+ }
+
+ &.active {
+ color: var(--accent);
+ }
+
+ > .icon {
+ width: 32px;
+ margin-right: 2px;
+ flex-shrink: 0;
+ text-align: center;
+ opacity: 0.8;
+
+ &:empty {
+ display: none;
+
+ & + .text {
+ padding-left: 4px;
+ }
+ }
+ }
+
+ > .text {
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ padding-right: 12px;
+ }
+
+ > .right {
+ margin-left: auto;
+ opacity: 0.7;
+ }
+ }
+}
+</style>
diff --git a/src/client/components/form/pagination.vue b/src/client/components/form/pagination.vue
new file mode 100644
index 0000000000..7dcaedf9bf
--- /dev/null
+++ b/src/client/components/form/pagination.vue
@@ -0,0 +1,42 @@
+<template>
+<FormGroup class="uljviswt _formItem">
+ <template #label><slot name="label"></slot></template>
+ <slot :items="items"></slot>
+ <div class="empty" v-if="empty" key="_empty_">
+ <slot name="empty"></slot>
+ </div>
+ <FormButton v-show="more" class="button" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary>
+ <template v-if="!moreFetching">{{ $t('loadMore') }}</template>
+ <template v-if="moreFetching"><MkLoading inline/></template>
+ </FormButton>
+</FormGroup>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormButton from './button.vue';
+import FormGroup from './group.vue';
+import paging from '@/scripts/paging';
+
+export default defineComponent({
+ components: {
+ FormButton,
+ FormGroup,
+ },
+
+ mixins: [
+ paging({}),
+ ],
+
+ props: {
+ pagination: {
+ required: true
+ },
+ },
+});
+</script>
+
+<style lang="scss" scoped>
+.uljviswt {
+}
+</style>
diff --git a/src/client/components/form/radios.vue b/src/client/components/form/radios.vue
new file mode 100644
index 0000000000..4c7f405cac
--- /dev/null
+++ b/src/client/components/form/radios.vue
@@ -0,0 +1,106 @@
+<script lang="ts">
+import { defineComponent, h } from 'vue';
+import MkRadio from '@/components/ui/radio.vue';
+import './form.scss';
+
+export default defineComponent({
+ components: {
+ MkRadio
+ },
+ props: {
+ modelValue: {
+ required: false
+ },
+ },
+ data() {
+ return {
+ value: this.modelValue,
+ }
+ },
+ watch: {
+ value() {
+ this.$emit('update:modelValue', this.value);
+ }
+ },
+ render() {
+ const label = this.$slots.desc();
+ const options = this.$slots.default();
+
+ return h('div', {
+ class: 'cnklmpwm _formItem'
+ }, [
+ h('div', {
+ class: '_formLabel',
+ }, label),
+ ...options.map(option => h('button', {
+ class: '_button _formPanel _formClickable',
+ key: option.props.value,
+ onClick: () => this.value = option.props.value,
+ }, [h('span', {
+ class: ['check', { checked: this.value === option.props.value }],
+ }), option.children]))
+ ]);
+ }
+});
+</script>
+
+<style lang="scss">
+.cnklmpwm {
+ > button {
+ display: block;
+ width: 100%;
+ box-sizing: border-box;
+ padding: 14px 18px;
+ text-align: left;
+
+ &:not(:first-of-type) {
+ border-top: none !important;
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+ }
+
+ &:not(:last-of-type) {
+ border-bottom: solid 0.5px var(--divider);
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+ }
+
+ > .check {
+ display: inline-block;
+ vertical-align: bottom;
+ position: relative;
+ width: 20px;
+ height: 20px;
+ margin-right: 8px;
+ background: none;
+ border: 2px solid var(--inputBorder);
+ border-radius: 100%;
+ transition: inherit;
+
+ &:after {
+ content: "";
+ display: block;
+ position: absolute;
+ top: 3px;
+ right: 3px;
+ bottom: 3px;
+ left: 3px;
+ border-radius: 100%;
+ opacity: 0;
+ transform: scale(0);
+ transition: .4s cubic-bezier(.25,.8,.25,1);
+ }
+
+ &.checked {
+ border-color: var(--accent);
+
+ &:after {
+ background-color: var(--accent);
+ transform: scale(1);
+ opacity: 1;
+ }
+ }
+ }
+ }
+}
+</style>
diff --git a/src/client/components/form/range.vue b/src/client/components/form/range.vue
new file mode 100644
index 0000000000..3452184c55
--- /dev/null
+++ b/src/client/components/form/range.vue
@@ -0,0 +1,122 @@
+<template>
+<div class="ifitouly _formItem" :class="{ focused, disabled }">
+ <div class="_formLabel"><slot name="label"></slot></div>
+ <div class="_formPanel main">
+ <input
+ type="range"
+ ref="input"
+ v-model="v"
+ :disabled="disabled"
+ :min="min"
+ :max="max"
+ :step="step"
+ @focus="focused = true"
+ @blur="focused = false"
+ @input="$emit('update:value', $event.target.value)"
+ />
+ </div>
+ <div class="_formCaption"><slot name="caption"></slot></div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+
+export default defineComponent({
+ props: {
+ value: {
+ type: Number,
+ required: false,
+ default: 0
+ },
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
+ min: {
+ type: Number,
+ required: false,
+ default: 0
+ },
+ max: {
+ type: Number,
+ required: false,
+ default: 100
+ },
+ step: {
+ type: Number,
+ required: false,
+ default: 1
+ },
+ },
+ data() {
+ return {
+ v: this.value,
+ focused: false
+ };
+ },
+ watch: {
+ value(v) {
+ this.v = parseFloat(v);
+ }
+ },
+});
+</script>
+
+<style lang="scss" scoped>
+.ifitouly {
+ position: relative;
+
+ > .main {
+ padding: 24px 16px;
+
+ > input {
+ display: block;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ background: var(--X10);
+ height: 4px;
+ width: 100%;
+ box-sizing: border-box;
+ margin: 0;
+ outline: 0;
+ border: 0;
+ border-radius: 7px;
+
+ &.disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ }
+
+ &::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ appearance: none;
+ cursor: pointer;
+ width: 20px;
+ height: 20px;
+ display: block;
+ border-radius: 50%;
+ border: none;
+ background: var(--accent);
+ box-shadow: 0 0 6px rgba(0, 0, 0, 0.3);
+ box-sizing: content-box;
+ }
+
+ &::-moz-range-thumb {
+ -moz-appearance: none;
+ appearance: none;
+ cursor: pointer;
+ width: 20px;
+ height: 20px;
+ display: block;
+ border-radius: 50%;
+ border: none;
+ background: var(--accent);
+ box-shadow: 0 0 6px rgba(0, 0, 0, 0.3);
+ }
+ }
+ }
+}
+</style>
diff --git a/src/client/components/form/select.vue b/src/client/components/form/select.vue
new file mode 100644
index 0000000000..b865372f56
--- /dev/null
+++ b/src/client/components/form/select.vue
@@ -0,0 +1,147 @@
+<template>
+<div class="yrtfrpux _formItem" :class="{ disabled, inline }">
+ <div class="_formLabel"><slot name="label"></slot></div>
+ <div class="icon" ref="icon"><slot name="icon"></slot></div>
+ <div class="input _formPanel _formClickable" @click="focus">
+ <div class="prefix" ref="prefix"><slot name="prefix"></slot></div>
+ <select ref="input"
+ v-model="v"
+ :required="required"
+ :disabled="disabled"
+ @focus="focused = true"
+ @blur="focused = false"
+ >
+ <slot></slot>
+ </select>
+ <div class="suffix">
+ <Fa :icon="faChevronDown"/>
+ </div>
+ </div>
+ <div class="_formCaption"><slot name="caption"></slot></div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { faChevronDown } from '@fortawesome/free-solid-svg-icons';
+import './form.scss';
+
+export default defineComponent({
+ props: {
+ value: {
+ required: false
+ },
+ required: {
+ type: Boolean,
+ required: false
+ },
+ disabled: {
+ type: Boolean,
+ required: false
+ },
+ inline: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
+ },
+ data() {
+ return {
+ faChevronDown,
+ };
+ },
+ computed: {
+ v: {
+ get() {
+ return this.value;
+ },
+ set(v) {
+ this.$emit('update:value', v);
+ }
+ },
+ },
+ methods: {
+ focus() {
+ this.$refs.input.focus();
+ }
+ }
+});
+</script>
+
+<style lang="scss" scoped>
+.yrtfrpux {
+ position: relative;
+
+ > .icon {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 24px;
+ text-align: center;
+ line-height: 32px;
+
+ &:not(:empty) + .input {
+ margin-left: 28px;
+ }
+ }
+
+ > .input {
+ display: flex;
+ position: relative;
+
+ > select {
+ display: block;
+ flex: 1;
+ width: 100%;
+ padding: 0 16px;
+ font: inherit;
+ font-weight: normal;
+ font-size: 1em;
+ height: 52px;
+ background: none;
+ border: none;
+ border-radius: 0;
+ outline: none;
+ box-shadow: none;
+ appearance: none;
+ -webkit-appearance: none;
+ color: var(--fg);
+
+ option,
+ optgroup {
+ color: var(--fg);
+ background: var(--bg);
+ }
+ }
+
+ > .prefix,
+ > .suffix {
+ display: block;
+ align-self: center;
+ justify-self: center;
+ font-size: 1em;
+ line-height: 32px;
+ color: var(--inputLabel);
+ pointer-events: none;
+
+ &:empty {
+ display: none;
+ }
+
+ > * {
+ display: block;
+ min-width: 16px;
+ }
+ }
+
+ > .prefix {
+ padding-right: 4px;
+ }
+
+ > .suffix {
+ padding: 0 16px 0 0;
+ opacity: 0.7;
+ }
+ }
+}
+</style>
diff --git a/src/client/components/form/switch.vue b/src/client/components/form/switch.vue
new file mode 100644
index 0000000000..a2941c5996
--- /dev/null
+++ b/src/client/components/form/switch.vue
@@ -0,0 +1,132 @@
+<template>
+<div class="ijnpvmgr _formItem">
+ <div class="main _formPanel _formClickable"
+ :class="{ disabled, checked }"
+ :aria-checked="checked"
+ :aria-disabled="disabled"
+ @click.prevent="toggle"
+ >
+ <input
+ type="checkbox"
+ ref="input"
+ :disabled="disabled"
+ @keydown.enter="toggle"
+ >
+ <span class="button">
+ <span></span>
+ </span>
+ <span class="label">
+ <span><slot></slot></span>
+ </span>
+ </div>
+ <div class="_formCaption"><slot name="desc"></slot></div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import './form.scss';
+
+export default defineComponent({
+ props: {
+ value: {
+ type: Boolean,
+ default: false
+ },
+ disabled: {
+ type: Boolean,
+ default: false
+ }
+ },
+ computed: {
+ checked(): boolean {
+ return this.value;
+ }
+ },
+ methods: {
+ toggle() {
+ if (this.disabled) return;
+ this.$emit('update:value', !this.checked);
+ }
+ }
+});
+</script>
+
+<style lang="scss" scoped>
+.ijnpvmgr {
+ > .main {
+ position: relative;
+ display: flex;
+ padding: 16px;
+ cursor: pointer;
+
+ > * {
+ user-select: none;
+ }
+
+ &.disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ }
+
+ &.checked {
+ > .button {
+ background-color: var(--X10);
+ border-color: var(--X10);
+
+ > * {
+ background-color: var(--accent);
+ transform: translateX(14px);
+ }
+ }
+ }
+
+ > input {
+ position: absolute;
+ width: 0;
+ height: 0;
+ opacity: 0;
+ margin: 0;
+ }
+
+ > .button {
+ position: relative;
+ display: inline-block;
+ flex-shrink: 0;
+ margin: 3px 0 0 0;
+ width: 34px;
+ height: 14px;
+ background: var(--X6);
+ outline: none;
+ border-radius: 14px;
+ transition: all 0.3s;
+ cursor: pointer;
+
+ > * {
+ position: absolute;
+ top: -3px;
+ left: 0;
+ border-radius: 100%;
+ transition: background-color 0.3s, transform 0.3s;
+ width: 20px;
+ height: 20px;
+ background-color: #fff;
+ box-shadow: 0 2px 1px -1px rgba(#000, 0.2), 0 1px 1px 0 rgba(#000, 0.14), 0 1px 3px 0 rgba(#000, 0.12);
+ }
+ }
+
+ > .label {
+ margin-left: 12px;
+ display: block;
+ transition: inherit;
+ color: var(--fg);
+
+ > span {
+ display: block;
+ line-height: 20px;
+ transition: inherit;
+ }
+ }
+ }
+}
+</style>
diff --git a/src/client/components/form/textarea.vue b/src/client/components/form/textarea.vue
new file mode 100644
index 0000000000..d84b48197a
--- /dev/null
+++ b/src/client/components/form/textarea.vue
@@ -0,0 +1,136 @@
+<template>
+<div class="rivhosbp _formItem" :class="{ tall, pre }">
+ <div class="_formLabel"><slot></slot></div>
+ <div class="input _formPanel">
+ <textarea ref="input" :class="{ code, _monospace: code }"
+ :value="value"
+ :required="required"
+ :readonly="readonly"
+ :pattern="pattern"
+ :autocomplete="autocomplete"
+ :spellcheck="!code"
+ @input="onInput"
+ @focus="focused = true"
+ @blur="focused = false"
+ ></textarea>
+ </div>
+ <button class="save _textButton" v-if="save && changed" @click="() => { changed = false; save(); }">{{ $t('save') }}</button>
+ <div class="_formCaption"><slot name="desc"></slot></div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import './form.scss';
+
+export default defineComponent({
+ props: {
+ value: {
+ required: false
+ },
+ required: {
+ type: Boolean,
+ required: false
+ },
+ readonly: {
+ type: Boolean,
+ required: false
+ },
+ pattern: {
+ type: String,
+ required: false
+ },
+ autocomplete: {
+ type: String,
+ required: false
+ },
+ code: {
+ type: Boolean,
+ required: false
+ },
+ tall: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
+ pre: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
+ save: {
+ type: Function,
+ required: false,
+ },
+ },
+ data() {
+ return {
+ changed: false,
+ }
+ },
+ methods: {
+ focus() {
+ this.$refs.input.focus();
+ },
+ onInput(ev) {
+ this.changed = true;
+ this.$emit('update:value', ev.target.value);
+ }
+ }
+});
+</script>
+
+<style lang="scss" scoped>
+.rivhosbp {
+ position: relative;
+
+ > .input {
+ position: relative;
+
+ > textarea {
+ display: block;
+ width: 100%;
+ min-width: 100%;
+ max-width: 100%;
+ min-height: 130px;
+ margin: 0;
+ padding: 16px;
+ box-sizing: border-box;
+ font: inherit;
+ font-weight: normal;
+ font-size: 1em;
+ background: transparent;
+ border: none;
+ border-radius: 0;
+ outline: none;
+ box-shadow: none;
+ color: var(--fg);
+
+ &.code {
+ tab-size: 2;
+ }
+ }
+ }
+
+ > .save {
+ margin: 6px 0 0 0;
+ font-size: 0.8em;
+ }
+
+ &.tall {
+ > .input {
+ > textarea {
+ min-height: 200px;
+ }
+ }
+ }
+
+ &.pre {
+ > .input {
+ > textarea {
+ white-space: pre;
+ }
+ }
+ }
+}
+</style>
diff --git a/src/client/components/form/tuple.vue b/src/client/components/form/tuple.vue
new file mode 100644
index 0000000000..6c8a22d189
--- /dev/null
+++ b/src/client/components/form/tuple.vue
@@ -0,0 +1,36 @@
+<template>
+<div class="wthhikgt _formItem" v-size="{ max: [500] }">
+ <slot></slot>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+
+export default defineComponent({
+});
+</script>
+
+<style lang="scss" scoped>
+.wthhikgt {
+ position: relative;
+ display: flex;
+
+ > ::v-deep(*) {
+ flex: 1;
+ margin: 0;
+
+ &:not(:last-child) {
+ margin-right: 16px;
+ }
+ }
+
+ &.max-width_500px {
+ display: block;
+
+ > ::v-deep(*) {
+ margin: inherit;
+ }
+ }
+}
+</style>
diff --git a/src/client/components/media-image.vue b/src/client/components/media-image.vue
index 64e3efab31..a9d0023cc2 100644
--- a/src/client/components/media-image.vue
+++ b/src/client/components/media-image.vue
@@ -68,7 +68,7 @@ export default defineComponent({
created() {
// Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする
this.$watch('image', () => {
- this.hide = this.image.isSensitive && !this.$store.state.device.alwaysShowNsfw;
+ this.hide = (this.$store.state.device.nsfw === 'force') ? true : this.image.isSensitive && (this.$store.state.device.nsfw !== 'ignore');
if (this.image.blurhash) {
this.color = extractAvgColorFromBlurhash(this.image.blurhash);
}
diff --git a/src/client/components/media-video.vue b/src/client/components/media-video.vue
index 21faddf73b..3dfd60c87f 100644
--- a/src/client/components/media-video.vue
+++ b/src/client/components/media-video.vue
@@ -48,7 +48,7 @@ export default defineComponent({
}
},
created() {
- this.hide = this.video.isSensitive && !this.$store.state.device.alwaysShowNsfw;
+ this.hide = (this.$store.state.device.nsfw === 'force') ? true : this.video.isSensitive && (this.$store.state.device.nsfw !== 'ignore');
},
});
</script>
diff --git a/src/client/components/taskmanager.api-window.vue b/src/client/components/taskmanager.api-window.vue
index 0df3f75fa2..ec685462c9 100644
--- a/src/client/components/taskmanager.api-window.vue
+++ b/src/client/components/taskmanager.api-window.vue
@@ -14,8 +14,8 @@
<option value="res">Response</option>
</MkTab>
- <code v-if="tab === 'req'">{{ reqStr }}</code>
- <code v-if="tab === 'res'">{{ resStr }}</code>
+ <code v-if="tab === 'req'" class="_monospace">{{ reqStr }}</code>
+ <code v-if="tab === 'res'" class="_monospace">{{ resStr }}</code>
</div>
</XWindow>
</template>
@@ -67,7 +67,6 @@ export default defineComponent({
font-size: 0.9em;
tab-size: 2;
white-space: pre;
- font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace;
}
}
</style>
diff --git a/src/client/components/taskmanager.vue b/src/client/components/taskmanager.vue
index 92c56442c3..1ed8c8bd5e 100644
--- a/src/client/components/taskmanager.vue
+++ b/src/client/components/taskmanager.vue
@@ -3,7 +3,7 @@
<template #header>
<Fa :icon="faTerminal" style="margin-right: 0.5em;"/>Task Manager
</template>
- <div class="qljqmnzj">
+ <div class="qljqmnzj _monospace">
<MkTab v-model:value="tab" style="border-bottom: solid 1px var(--divider);">
<option value="windows">Windows</option>
<option value="stream">Stream</option>
@@ -150,7 +150,6 @@ export default defineComponent({
display: flex;
flex-direction: column;
height: 100%;
- font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace;
> .content {
flex: 1;
diff --git a/src/client/components/timeline.vue b/src/client/components/timeline.vue
index 930f47b1a5..df9424d8ed 100644
--- a/src/client/components/timeline.vue
+++ b/src/client/components/timeline.vue
@@ -6,6 +6,7 @@
import { defineComponent } from 'vue';
import XNotes from './notes.vue';
import * as os from '@/os';
+import * as sound from '@/scripts/sound';
export default defineComponent({
components: {
@@ -65,7 +66,7 @@ export default defineComponent({
this.$emit('note');
if (this.sound) {
- os.sound(note.userId === this.$store.state.i.id ? 'noteMy' : 'note');
+ sound.play(note.userId === this.$store.state.i.id ? 'noteMy' : 'note');
}
};
diff --git a/src/client/components/ui/range.vue b/src/client/components/ui/range.vue
index c6e585cf50..4cfe66a8fc 100644
--- a/src/client/components/ui/range.vue
+++ b/src/client/components/ui/range.vue
@@ -1,7 +1,7 @@
<template>
<div class="timctyfi" :class="{ focused, disabled }">
<div class="icon"><slot name="icon"></slot></div>
- <span class="title"><slot name="title"></slot></span>
+ <span class="label"><slot name="label"></slot></span>
<input
type="range"
ref="input"
@@ -19,7 +19,7 @@
</template>
<script lang="ts">
-import { defineComponent } from 'vue';import * as os from '@/os';
+import { defineComponent } from 'vue';
export default defineComponent({
props: {
diff --git a/src/client/components/ui/switch.vue b/src/client/components/ui/switch.vue
index f738257232..762fba6d99 100644
--- a/src/client/components/ui/switch.vue
+++ b/src/client/components/ui/switch.vue
@@ -17,10 +17,8 @@
<span></span>
</span>
<span class="label">
- <span :aria-hidden="!checked"><slot></slot></span>
- <p :aria-hidden="!checked">
- <slot name="desc"></slot>
- </p>
+ <span><slot></slot></span>
+ <p><slot name="desc"></slot></p>
</span>
</div>
</template>
diff --git a/src/client/components/ui/textarea.vue b/src/client/components/ui/textarea.vue
index 7d3250cc45..d49d7e8342 100644
--- a/src/client/components/ui/textarea.vue
+++ b/src/client/components/ui/textarea.vue
@@ -2,7 +2,7 @@
<div class="adhpbeos" :class="{ focused, filled, tall, pre }">
<div class="input">
<span class="label" ref="label"><slot></slot></span>
- <textarea ref="input" :class="{ code }"
+ <textarea ref="input" :class="{ code, _monospace: code }"
:value="value"
:required="required"
:readonly="readonly"
@@ -166,7 +166,6 @@ export default defineComponent({
&.code {
tab-size: 2;
- font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace;
}
}
}
diff --git a/src/client/init.ts b/src/client/init.ts
index cc97947c0a..9294733bbb 100644
--- a/src/client/init.ts
+++ b/src/client/init.ts
@@ -16,7 +16,8 @@ import { router } from './router';
import { applyTheme } from '@/scripts/theme';
import { isDeviceDarkmode } from '@/scripts/is-device-darkmode';
import { i18n, lang } from './i18n';
-import { stream, sound, isMobile, dialog } from '@/os';
+import { stream, isMobile, dialog } from '@/os';
+import * as sound from './scripts/sound';
console.info(`Misskey v${version}`);
@@ -50,7 +51,7 @@ if (_DEV_) {
document.addEventListener('touchend', () => {}, { passive: true });
if (localStorage.getItem('theme') == null) {
- applyTheme(require('@/themes/l-white.json5'));
+ applyTheme(require('@/themes/l-light.json5'));
}
//#region SEE: https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
@@ -307,7 +308,7 @@ if (store.getters.isSignedIn) {
hasUnreadMessagingMessage: true
});
- sound('chatBg');
+ sound.play('chatBg');
});
main.on('readAllAntennas', () => {
@@ -321,7 +322,7 @@ if (store.getters.isSignedIn) {
hasUnreadAntenna: true
});
- sound('antenna');
+ sound.play('antenna');
});
main.on('readAllAnnouncements', () => {
@@ -341,7 +342,7 @@ if (store.getters.isSignedIn) {
hasUnreadChannel: true
});
- sound('channel');
+ sound.play('channel');
});
main.on('readAllAnnouncements', () => {
diff --git a/src/client/os.ts b/src/client/os.ts
index 88d445ebac..d43de4de44 100644
--- a/src/client/os.ts
+++ b/src/client/os.ts
@@ -6,6 +6,7 @@ import { apiUrl, debug } from '@/config';
import MkPostFormDialog from '@/components/post-form-dialog.vue';
import MkWaitingDialog from '@/components/waiting-dialog.vue';
import { resolve } from '@/router';
+import { device } from './cold-storage';
const ua = navigator.userAgent.toLowerCase();
export const isMobile = /mobile|iphone|ipad|android/.test(ua);
@@ -344,15 +345,6 @@ export function post(props: Record<string, any>) {
});
}
-export function sound(type: string) {
- if (store.state.device.sfxVolume === 0) return;
- const sound = store.state.device['sfx' + type.substr(0, 1).toUpperCase() + type.substr(1)];
- if (sound == null) return;
- const audio = new Audio(`/assets/sounds/${sound}.mp3`);
- audio.volume = store.state.device.sfxVolume;
- audio.play();
-}
-
export const deckGlobalEvents = new EventEmitter();
export const uploads = ref([]);
diff --git a/src/client/pages/announcements.vue b/src/client/pages/announcements.vue
index a202ec749f..50dc994025 100644
--- a/src/client/pages/announcements.vue
+++ b/src/client/pages/announcements.vue
@@ -1,6 +1,6 @@
<template>
<div class="_section">
- <MkPagination :pagination="pagination" #default="{items}" class="ruryvtyk _content" ref="list">
+ <MkPagination :pagination="pagination" #default="{items}" class="ruryvtyk _content">
<section class="_card announcement _vMargin" v-for="(announcement, i) in items" :key="announcement.id">
<div class="_title"><span v-if="$store.getters.isSignedIn && !announcement.isRead">🆕 </span>{{ announcement.title }}</div>
<div class="_content">
diff --git a/src/client/pages/instance/settings.vue b/src/client/pages/instance/settings.vue
index 32a6a9595f..542c2942b9 100644
--- a/src/client/pages/instance/settings.vue
+++ b/src/client/pages/instance/settings.vue
@@ -7,6 +7,8 @@
<MkTextarea v-model:value="description">{{ $t('instanceDescription') }}</MkTextarea>
<MkInput v-model:value="iconUrl"><template #icon><Fa :icon="faLink"/></template>{{ $t('iconUrl') }}</MkInput>
<MkInput v-model:value="bannerUrl"><template #icon><Fa :icon="faLink"/></template>{{ $t('bannerUrl') }}</MkInput>
+ <MkInput v-model:value="backgroundImageUrl"><template #icon><Fa :icon="faLink"/></template>{{ $t('backgroundImageUrl') }}</MkInput>
+ <MkInput v-model:value="logoImageUrl"><template #icon><Fa :icon="faLink"/></template>{{ $t('logoImageUrl') }}</MkInput>
<MkInput v-model:value="tosUrl"><template #icon><Fa :icon="faLink"/></template>{{ $t('tosUrl') }}</MkInput>
<MkInput v-model:value="maintainerName">{{ $t('maintainerName') }}</MkInput>
<MkInput v-model:value="maintainerEmail" type="email"><template #icon><Fa :icon="faEnvelope"/></template>{{ $t('maintainerEmail') }}</MkInput>
@@ -292,6 +294,8 @@ export default defineComponent({
email: null,
bannerUrl: null,
iconUrl: null,
+ logoImageUrl: null,
+ backgroundImageUrl: null,
maxNoteTextLength: 0,
enableRegistration: false,
enableLocalTimeline: false,
@@ -345,6 +349,8 @@ export default defineComponent({
this.tosUrl = this.meta.tosUrl;
this.bannerUrl = this.meta.bannerUrl;
this.iconUrl = this.meta.iconUrl;
+ this.logoImageUrl = this.meta.logoImageUrl;
+ this.backgroundImageUrl = this.meta.backgroundImageUrl;
this.enableEmail = this.meta.enableEmail;
this.email = this.meta.email;
this.maintainerName = this.meta.maintainerName;
@@ -498,6 +504,8 @@ export default defineComponent({
tosUrl: this.tosUrl,
bannerUrl: this.bannerUrl,
iconUrl: this.iconUrl,
+ logoImageUrl: this.logoImageUrl,
+ backgroundImageUrl: this.backgroundImageUrl,
maintainerName: this.maintainerName,
maintainerEmail: this.maintainerEmail,
maxNoteTextLength: this.maxNoteTextLength,
diff --git a/src/client/pages/messaging/messaging-room.vue b/src/client/pages/messaging/messaging-room.vue
index f414ccbaa4..d4331c1390 100644
--- a/src/client/pages/messaging/messaging-room.vue
+++ b/src/client/pages/messaging/messaging-room.vue
@@ -38,6 +38,7 @@ import parseAcct from '../../../misc/acct/parse';
import { isBottom, onScrollBottom, scroll } from '@/scripts/scroll';
import * as os from '@/os';
import { popout } from '@/scripts/popout';
+import * as sound from '@/scripts/sound';
const Component = defineComponent({
components: {
@@ -218,7 +219,7 @@ const Component = defineComponent({
},
onMessage(message) {
- os.sound('chat');
+ sound.play('chat');
const _isBottom = isBottom(this.$el, 64);
diff --git a/src/client/pages/reversi/game.board.vue b/src/client/pages/reversi/game.board.vue
index 6559396aca..302d7bc79c 100644
--- a/src/client/pages/reversi/game.board.vue
+++ b/src/client/pages/reversi/game.board.vue
@@ -94,6 +94,7 @@ 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: {
@@ -245,11 +246,7 @@ export default defineComponent({
this.o.put(this.myColor, pos);
// サウンドを再生する
- if (this.$store.state.device.enableSounds) {
- const sound = new Audio(`${url}/assets/reversi-put-me.mp3`);
- sound.volume = this.$store.state.device.soundVolume;
- sound.play();
- }
+ sound.play(this.myColor ? 'reversiPutBlack' : 'reversiPutWhite');
this.connection.send('set', {
pos: pos
@@ -268,10 +265,8 @@ export default defineComponent({
this.$forceUpdate();
// サウンドを再生する
- if (this.$store.state.device.enableSounds && x.color != this.myColor) {
- const sound = new Audio(`${url}/assets/reversi-put-you.mp3`);
- sound.volume = this.$store.state.device.soundVolume;
- sound.play();
+ if (x.color !== this.myColor) {
+ sound.play(x.color ? 'reversiPutBlack' : 'reversiPutWhite');
}
},
diff --git a/src/client/pages/settings/security.2fa.vue b/src/client/pages/settings/2fa.vue
index 22b3878445..dc6d12a40f 100644
--- a/src/client/pages/settings/security.2fa.vue
+++ b/src/client/pages/settings/2fa.vue
@@ -75,14 +75,25 @@ import MkButton from '@/components/ui/button.vue';
import MkInfo from '@/components/ui/info.vue';
import MkInput from '@/components/ui/input.vue';
import MkSwitch from '@/components/ui/switch.vue';
+import FormBase from '@/components/form/base.vue';
+import FormGroup from '@/components/form/group.vue';
+import FormButton from '@/components/form/button.vue';
import * as os from '@/os';
export default defineComponent({
components: {
+ FormBase,
MkButton, MkInfo, MkInput, MkSwitch
},
+
+ emits: ['info'],
+
data() {
return {
+ INFO: {
+ title: this.$t('twoStepAuthentication'),
+ icon: faLock
+ },
data: null,
supportsCredentials: !!navigator.credentials,
usePasswordLessLogin: this.$store.state.i.usePasswordLessLogin,
@@ -92,6 +103,7 @@ export default defineComponent({
faLock
};
},
+
methods: {
register() {
os.dialog({
@@ -225,6 +237,7 @@ export default defineComponent({
});
});
},
+
updatePasswordLessLogin() {
os.api('i/2fa/password-less', {
value: !!this.usePasswordLessLogin
diff --git a/src/client/pages/settings/account-info.vue b/src/client/pages/settings/account-info.vue
new file mode 100644
index 0000000000..c881b91535
--- /dev/null
+++ b/src/client/pages/settings/account-info.vue
@@ -0,0 +1,185 @@
+<template>
+<FormBase>
+ <FormKeyValueView>
+ <template #key>ID</template>
+ <template #value><span class="_monospace">{{ $store.state.i.id }}</span></template>
+ </FormKeyValueView>
+
+ <FormGroup>
+ <FormKeyValueView>
+ <template #key>{{ $t('registeredDate') }}</template>
+ <template #value><MkTime :time="$store.state.i.createdAt" mode="detail"/></template>
+ </FormKeyValueView>
+ </FormGroup>
+
+ <FormGroup v-if="stats">
+ <template #label>{{ $t('statistics') }}</template>
+ <FormKeyValueView>
+ <template #key>{{ $t('notesCount') }}</template>
+ <template #value>{{ number(stats.notesCount) }}</template>
+ </FormKeyValueView>
+ <FormKeyValueView>
+ <template #key>{{ $t('repliesCount') }}</template>
+ <template #value>{{ number(stats.repliesCount) }}</template>
+ </FormKeyValueView>
+ <FormKeyValueView>
+ <template #key>{{ $t('renotesCount') }}</template>
+ <template #value>{{ number(stats.renotesCount) }}</template>
+ </FormKeyValueView>
+ <FormKeyValueView>
+ <template #key>{{ $t('repliedCount') }}</template>
+ <template #value>{{ number(stats.repliedCount) }}</template>
+ </FormKeyValueView>
+ <FormKeyValueView>
+ <template #key>{{ $t('renotedCount') }}</template>
+ <template #value>{{ number(stats.renotedCount) }}</template>
+ </FormKeyValueView>
+ <FormKeyValueView>
+ <template #key>{{ $t('pollVotesCount') }}</template>
+ <template #value>{{ number(stats.pollVotesCount) }}</template>
+ </FormKeyValueView>
+ <FormKeyValueView>
+ <template #key>{{ $t('pollVotedCount') }}</template>
+ <template #value>{{ number(stats.pollVotedCount) }}</template>
+ </FormKeyValueView>
+ <FormKeyValueView>
+ <template #key>{{ $t('sentReactionsCount') }}</template>
+ <template #value>{{ number(stats.sentReactionsCount) }}</template>
+ </FormKeyValueView>
+ <FormKeyValueView>
+ <template #key>{{ $t('receivedReactionsCount') }}</template>
+ <template #value>{{ number(stats.receivedReactionsCount) }}</template>
+ </FormKeyValueView>
+ <FormKeyValueView>
+ <template #key>{{ $t('noteFavoritesCount') }}</template>
+ <template #value>{{ number(stats.noteFavoritesCount) }}</template>
+ </FormKeyValueView>
+ <FormKeyValueView>
+ <template #key>{{ $t('followingCount') }}</template>
+ <template #value>{{ number(stats.followingCount) }}</template>
+ </FormKeyValueView>
+ <FormKeyValueView>
+ <template #key>{{ $t('followingCount') }} ({{ $t('local') }})</template>
+ <template #value>{{ number(stats.localFollowingCount) }}</template>
+ </FormKeyValueView>
+ <FormKeyValueView>
+ <template #key>{{ $t('followingCount') }} ({{ $t('remote') }})</template>
+ <template #value>{{ number(stats.remoteFollowingCount) }}</template>
+ </FormKeyValueView>
+ <FormKeyValueView>
+ <template #key>{{ $t('followersCount') }}</template>
+ <template #value>{{ number(stats.followersCount) }}</template>
+ </FormKeyValueView>
+ <FormKeyValueView>
+ <template #key>{{ $t('followersCount') }} ({{ $t('local') }})</template>
+ <template #value>{{ number(stats.localFollowersCount) }}</template>
+ </FormKeyValueView>
+ <FormKeyValueView>
+ <template #key>{{ $t('followersCount') }} ({{ $t('remote') }})</template>
+ <template #value>{{ number(stats.remoteFollowersCount) }}</template>
+ </FormKeyValueView>
+ <FormKeyValueView>
+ <template #key>{{ $t('pageLikesCount') }}</template>
+ <template #value>{{ number(stats.pageLikesCount) }}</template>
+ </FormKeyValueView>
+ <FormKeyValueView>
+ <template #key>{{ $t('pageLikedCount') }}</template>
+ <template #value>{{ number(stats.pageLikedCount) }}</template>
+ </FormKeyValueView>
+ <FormKeyValueView>
+ <template #key>{{ $t('driveFilesCount') }}</template>
+ <template #value>{{ number(stats.driveFilesCount) }}</template>
+ </FormKeyValueView>
+ <FormKeyValueView>
+ <template #key>{{ $t('driveUsage') }}</template>
+ <template #value>{{ bytes(stats.driveUsage) }}</template>
+ </FormKeyValueView>
+ <FormKeyValueView>
+ <template #key>{{ $t('reversiCount') }}</template>
+ <template #value>{{ number(stats.reversiCount) }}</template>
+ </FormKeyValueView>
+ </FormGroup>
+
+ <FormGroup>
+ <template #label>{{ $t('other') }}</template>
+ <FormKeyValueView>
+ <template #key>emailVerified</template>
+ <template #value>{{ $store.state.i.emailVerified ? $t('yes') : $t('no') }}</template>
+ </FormKeyValueView>
+ <FormKeyValueView>
+ <template #key>twoFactorEnabled</template>
+ <template #value>{{ $store.state.i.twoFactorEnabled ? $t('yes') : $t('no') }}</template>
+ </FormKeyValueView>
+ <FormKeyValueView>
+ <template #key>securityKeys</template>
+ <template #value>{{ $store.state.i.securityKeys ? $t('yes') : $t('no') }}</template>
+ </FormKeyValueView>
+ <FormKeyValueView>
+ <template #key>usePasswordLessLogin</template>
+ <template #value>{{ $store.state.i.usePasswordLessLogin ? $t('yes') : $t('no') }}</template>
+ </FormKeyValueView>
+ <FormKeyValueView>
+ <template #key>isModerator</template>
+ <template #value>{{ $store.state.i.isModerator ? $t('yes') : $t('no') }}</template>
+ </FormKeyValueView>
+ <FormKeyValueView>
+ <template #key>isAdmin</template>
+ <template #value>{{ $store.state.i.isAdmin ? $t('yes') : $t('no') }}</template>
+ </FormKeyValueView>
+ </FormGroup>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineAsyncComponent, defineComponent } from 'vue';
+import { faInfoCircle } from '@fortawesome/free-solid-svg-icons';
+import FormSwitch from '@/components/form/switch.vue';
+import FormSelect from '@/components/form/select.vue';
+import FormLink from '@/components/form/link.vue';
+import FormBase from '@/components/form/base.vue';
+import FormGroup from '@/components/form/group.vue';
+import FormButton from '@/components/form/button.vue';
+import FormKeyValueView from '@/components/form/key-value-view.vue';
+import * as os from '@/os';
+import number from '@/filters/number';
+import bytes from '@/filters/bytes';
+
+export default defineComponent({
+ components: {
+ FormBase,
+ FormSelect,
+ FormSwitch,
+ FormButton,
+ FormLink,
+ FormGroup,
+ FormKeyValueView,
+ },
+
+ emits: ['info'],
+
+ data() {
+ return {
+ INFO: {
+ title: this.$t('accountInfo'),
+ icon: faInfoCircle
+ },
+ stats: null
+ }
+ },
+
+ mounted() {
+ this.$emit('info', this.INFO);
+
+ os.api('users/stats', {
+ userId: this.$store.state.i.id
+ }).then(stats => {
+ this.stats = stats;
+ });
+ },
+
+ methods: {
+ number,
+ bytes,
+ }
+});
+</script>
diff --git a/src/client/pages/settings/api.vue b/src/client/pages/settings/api.vue
index f4cebbee36..8e5e4fbc66 100644
--- a/src/client/pages/settings/api.vue
+++ b/src/client/pages/settings/api.vue
@@ -1,26 +1,27 @@
<template>
-<div>
- <div class="_section">
- <div class="_content">
- <MkButton @click="generateToken">{{ $t('generateAccessToken') }}</MkButton>
- </div>
- </div>
- <div class="_section">
- <MkA to="/api-console" :behavior="isDesktop ? 'window' : null">API console</MkA>
- </div>
-</div>
+<FormBase>
+ <FormButton @click="generateToken" primary>{{ $t('generateAccessToken') }}</FormButton>
+ <FormLink to="/settings/apps">{{ $t('manageAccessTokens') }}</FormLink>
+ <FormLink to="/api-console" :behavior="isDesktop ? 'window' : null">API console</FormLink>
+</FormBase>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faKey } from '@fortawesome/free-solid-svg-icons';
-import MkButton from '@/components/ui/button.vue';
-import MkInput from '@/components/ui/input.vue';
+import FormSwitch from '@/components/form/switch.vue';
+import FormSelect from '@/components/form/select.vue';
+import FormLink from '@/components/form/link.vue';
+import FormBase from '@/components/form/base.vue';
+import FormGroup from '@/components/form/group.vue';
+import FormButton from '@/components/form/button.vue';
import * as os from '@/os';
export default defineComponent({
components: {
- MkButton, MkInput
+ FormBase,
+ FormButton,
+ FormLink,
},
emits: ['info'],
diff --git a/src/client/pages/apps.vue b/src/client/pages/settings/apps.vue
index f9dd0a3584..724a2e8d1f 100644
--- a/src/client/pages/apps.vue
+++ b/src/client/pages/settings/apps.vue
@@ -1,6 +1,6 @@
<template>
-<div>
- <MkPagination :pagination="pagination" class="bfomjevm" ref="list">
+<FormBase>
+ <FormPagination :pagination="pagination" ref="list">
<template #empty>
<div class="_fullinfo">
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
@@ -8,8 +8,8 @@
</div>
</template>
<template #default="{items}">
- <div class="token _panel" v-for="token in items" :key="token.id">
- <img class="icon" :src="token.iconUrl" alt=""/>
+ <div class="_formPanel bfomjevm" v-for="token in items" :key="token.id">
+ <img class="icon" :src="token.iconUrl" alt="" v-if="token.iconUrl"/>
<div class="body">
<div class="name">{{ token.name }}</div>
<div class="description">{{ token.description }}</div>
@@ -33,21 +33,29 @@
</div>
</div>
</template>
- </MkPagination>
-</div>
+ </FormPagination>
+</FormBase>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faTrashAlt, faPlug } from '@fortawesome/free-solid-svg-icons';
-import MkPagination from '@/components/ui/pagination.vue';
+import FormPagination from '@/components/form/pagination.vue';
+import FormSelect from '@/components/form/select.vue';
+import FormLink from '@/components/form/link.vue';
+import FormBase from '@/components/form/base.vue';
+import FormGroup from '@/components/form/group.vue';
+import FormButton from '@/components/form/button.vue';
import * as os from '@/os';
export default defineComponent({
components: {
- MkPagination
+ FormBase,
+ FormPagination,
},
+ emits: ['info'],
+
data() {
return {
INFO: {
@@ -65,6 +73,10 @@ export default defineComponent({
};
},
+ mounted() {
+ this.$emit('info', this.INFO);
+ },
+
methods: {
revoke(token) {
os.api('i/revoke-token', { tokenId: token.id }).then(() => {
@@ -77,26 +89,24 @@ export default defineComponent({
<style lang="scss" scoped>
.bfomjevm {
- > .token {
- display: flex;
- padding: 16px;
+ display: flex;
+ padding: 16px;
- > .icon {
- display: block;
- flex-shrink: 0;
- margin: 0 12px 0 0;
- width: 50px;
- height: 50px;
- border-radius: 8px;
- }
+ > .icon {
+ display: block;
+ flex-shrink: 0;
+ margin: 0 12px 0 0;
+ width: 50px;
+ height: 50px;
+ border-radius: 8px;
+ }
- > .body {
- width: calc(100% - 62px);
- position: relative;
+ > .body {
+ width: calc(100% - 62px);
+ position: relative;
- > .name {
- font-weight: bold;
- }
+ > .name {
+ font-weight: bold;
}
}
}
diff --git a/src/client/pages/settings/deck.vue b/src/client/pages/settings/deck.vue
new file mode 100644
index 0000000000..2eb2f60cba
--- /dev/null
+++ b/src/client/pages/settings/deck.vue
@@ -0,0 +1,90 @@
+<template>
+<FormBase>
+
+ <section class="_card _vMargin">
+ <div class="_title"><Fa :icon="faColumns"/> </div>
+ <div class="_content">
+ <div>{{ $t('defaultNavigationBehaviour') }}</div>
+ <MkSwitch v-model:value="deckNavWindow">{{ $t('openInWindow') }}</MkSwitch>
+ </div>
+ <div class="_content">
+ <MkSwitch v-model:value="deckAlwaysShowMainColumn">
+ {{ $t('_deck.alwaysShowMainColumn') }}
+ </MkSwitch>
+ </div>
+ <div class="_content">
+ <div>{{ $t('_deck.columnAlign') }}</div>
+ <MkRadio v-model="deckColumnAlign" value="left">{{ $t('left') }}</MkRadio>
+ <MkRadio v-model="deckColumnAlign" value="center">{{ $t('center') }}</MkRadio>
+ </div>
+ </section>
+
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { faImage, faCog, faColumns } from '@fortawesome/free-solid-svg-icons';
+import MkButton from '@/components/ui/button.vue';
+import MkSwitch from '@/components/ui/switch.vue';
+import MkSelect from '@/components/ui/select.vue';
+import MkRadio from '@/components/ui/radio.vue';
+import MkRadios from '@/components/ui/radios.vue';
+import MkRange from '@/components/ui/range.vue';
+import FormSwitch from '@/components/form/switch.vue';
+import FormSelect from '@/components/form/select.vue';
+import FormRadios from '@/components/form/radios.vue';
+import FormBase from '@/components/form/base.vue';
+import FormGroup from '@/components/form/group.vue';
+import { clientDb, set } from '@/db';
+import * as os from '@/os';
+
+export default defineComponent({
+ components: {
+ MkButton,
+ MkSwitch,
+ MkSelect,
+ MkRadio,
+ MkRadios,
+ MkRange,
+ FormSwitch,
+ FormSelect,
+ FormRadios,
+ FormBase,
+ FormGroup,
+ },
+
+ emits: ['info'],
+
+ data() {
+ return {
+ INFO: {
+ title: this.$t('deck'),
+ icon: faColumns
+ },
+ faImage, faCog,
+ }
+ },
+
+ computed: {
+ deckNavWindow: {
+ get() { return this.$store.state.device.deckNavWindow; },
+ set(value) { this.$store.commit('device/set', { key: 'deckNavWindow', value }); }
+ },
+
+ deckAlwaysShowMainColumn: {
+ get() { return this.$store.state.device.deckAlwaysShowMainColumn; },
+ set(value) { this.$store.commit('device/set', { key: 'deckAlwaysShowMainColumn', value }); }
+ },
+
+ deckColumnAlign: {
+ get() { return this.$store.state.device.deckColumnAlign; },
+ set(value) { this.$store.commit('device/set', { key: 'deckColumnAlign', value }); }
+ },
+ },
+
+ mounted() {
+ this.$emit('info', this.INFO);
+ },
+});
+</script>
diff --git a/src/client/pages/settings/email-address.vue b/src/client/pages/settings/email-address.vue
new file mode 100644
index 0000000000..7ff89d7910
--- /dev/null
+++ b/src/client/pages/settings/email-address.vue
@@ -0,0 +1,71 @@
+<template>
+<FormBase>
+ <FormGroup>
+ <FormInput v-model:value="emailAddress" type="email">
+ {{ $t('emailAddress') }}
+ <template #desc v-if="$store.state.i.email && !$store.state.i.emailVerified">{{ $t('verificationEmailSent') }}</template>
+ <template #desc v-else-if="emailAddress === $store.state.i.email && $store.state.i.emailVerified">{{ $t('emailVerified') }}</template>
+ </FormInput>
+ </FormGroup>
+ <FormButton @click="save" primary>{{ $t('save') }}</FormButton>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { faCog } from '@fortawesome/free-solid-svg-icons';
+import { faBell, faEnvelope } from '@fortawesome/free-regular-svg-icons';
+import FormButton from '@/components/form/button.vue';
+import FormInput from '@/components/form/input.vue';
+import FormBase from '@/components/form/base.vue';
+import FormGroup from '@/components/form/group.vue';
+import * as os from '@/os';
+
+export default defineComponent({
+ components: {
+ FormBase,
+ FormInput,
+ FormButton,
+ FormGroup,
+ },
+
+ emits: ['info'],
+
+ data() {
+ return {
+ INFO: {
+ title: this.$t('emailAddress'),
+ icon: faEnvelope
+ },
+ emailAddress: null,
+ code: null,
+ faCog
+ }
+ },
+
+ created() {
+ this.emailAddress = this.$store.state.i.email;
+ },
+
+ mounted() {
+ this.$emit('info', this.INFO);
+ },
+
+ methods: {
+ save() {
+ os.dialog({
+ title: this.$t('password'),
+ input: {
+ type: 'password'
+ }
+ }).then(({ canceled, result: password }) => {
+ if (canceled) return;
+ os.api('i/update-email', {
+ password: password,
+ email: this.emailAddress,
+ });
+ });
+ }
+ }
+});
+</script>
diff --git a/src/client/pages/settings/email.vue b/src/client/pages/settings/email.vue
new file mode 100644
index 0000000000..f72ee29a97
--- /dev/null
+++ b/src/client/pages/settings/email.vue
@@ -0,0 +1,52 @@
+<template>
+<FormBase>
+ <FormGroup>
+ <template #label>{{ $t('emailAddress') }}</template>
+ <FormLink to="/settings/email/address">
+ <template v-if="$store.state.i.email && !$store.state.i.emailVerified" #icon><Fa :icon="faExclamationTriangle" style="color: var(--warn);"/></template>
+ <template v-else-if="$store.state.i.email && $store.state.i.emailVerified" #icon><Fa :icon="faCheck" style="color: var(--success);"/></template>
+ {{ $store.state.i.email || $t('notSet') }}
+ </FormLink>
+ </FormGroup>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { faCog, faExclamationTriangle, faCheck } from '@fortawesome/free-solid-svg-icons';
+import { faBell, faEnvelope } from '@fortawesome/free-regular-svg-icons';
+import FormButton from '@/components/form/button.vue';
+import FormLink from '@/components/form/link.vue';
+import FormBase from '@/components/form/base.vue';
+import FormGroup from '@/components/form/group.vue';
+import * as os from '@/os';
+
+export default defineComponent({
+ components: {
+ FormBase,
+ FormLink,
+ FormButton,
+ FormGroup,
+ },
+
+ emits: ['info'],
+
+ data() {
+ return {
+ INFO: {
+ title: this.$t('email'),
+ icon: faEnvelope
+ },
+ faCog, faExclamationTriangle, faCheck
+ }
+ },
+
+ mounted() {
+ this.$emit('info', this.INFO);
+ },
+
+ methods: {
+
+ }
+});
+</script>
diff --git a/src/client/pages/settings/general.vue b/src/client/pages/settings/general.vue
index c88c573ae6..7c2905fdeb 100644
--- a/src/client/pages/settings/general.vue
+++ b/src/client/pages/settings/general.vue
@@ -1,109 +1,110 @@
<template>
-<div class="">
- <section class="_card _vMargin">
- <div class="_title"><Fa :icon="faCog"/> {{ $t('general') }}</div>
- <div class="_content">
- <MkRadios v-model="serverDisconnectedBehavior">
- <template #desc>{{ $t('whenServerDisconnected') }}</template>
- <option value="reload">{{ $t('_serverDisconnectedBehavior.reload') }}</option>
- <option value="dialog">{{ $t('_serverDisconnectedBehavior.dialog') }}</option>
- <option value="quiet">{{ $t('_serverDisconnectedBehavior.quiet') }}</option>
- </MkRadios>
- <MkSwitch v-model:value="imageNewTab">{{ $t('openImageInNewTab') }}</MkSwitch>
- <MkSwitch v-model:value="showFixedPostForm">{{ $t('showFixedPostForm') }}</MkSwitch>
- <MkSwitch v-model:value="enableInfiniteScroll">{{ $t('enableInfiniteScroll') }}</MkSwitch>
- <MkSwitch v-model:value="disablePagesScript">{{ $t('disablePagesScript') }}</MkSwitch>
- <MkSelect v-model:value="lang">
- <template #label>{{ $t('uiLanguage') }}</template>
- <option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option>
- </MkSelect>
- </div>
- </section>
+<FormBase>
+ <FormSwitch v-model:value="showFixedPostForm">{{ $t('showFixedPostForm') }}</FormSwitch>
- <section class="_card _vMargin">
- <div class="_title"><Fa :icon="faCog"/> {{ $t('defaultNavigationBehaviour') }}</div>
- <div class="_content">
- <MkSwitch v-model:value="defaultSideView">{{ $t('openInSideView') }}</MkSwitch>
- </div>
- <div class="_content">
- <MkRadios v-model="chatOpenBehavior">
- <template #desc>{{ $t('chatOpenBehavior') }}</template>
- <option value="page">{{ $t('showInPage') }}</option>
- <option value="window">{{ $t('openInWindow') }}</option>
- <option value="popout">{{ $t('popout') }}</option>
- </MkRadios>
- </div>
- </section>
+ <FormSelect v-model:value="lang">
+ <template #label>{{ $t('uiLanguage') }}</template>
+ <option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option>
+ <template #caption>
+ <i18n-t keypath="i18nInfo" tag="span">
+ <template #link>
+ <MkLink url="https://crowdin.com/project/misskey">Crowdin</MkLink>
+ </template>
+ </i18n-t>
+ </template>
+ </FormSelect>
- <section class="_card _vMargin">
- <div class="_title"><Fa :icon="faCog"/> {{ $t('appearance') }}</div>
- <div class="_content">
- <MkSwitch v-model:value="disableAnimatedMfm">{{ $t('disableAnimatedMfm') }}</MkSwitch>
- <MkSwitch v-model:value="reduceAnimation">{{ $t('reduceUiAnimation') }}</MkSwitch>
- <MkSwitch v-model:value="useBlurEffectForModal">{{ $t('useBlurEffectForModal') }}</MkSwitch>
- <MkSwitch v-model:value="useOsNativeEmojis">
- {{ $t('useOsNativeEmojis') }}
- <template #desc><Mfm text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></template>
- </MkSwitch>
- <MkRadios v-model="fontSize">
- <template #desc>{{ $t('fontSize') }}</template>
- <option value="small"><span style="font-size: 14px;">Aa</span></option>
- <option :value="null"><span style="font-size: 16px;">Aa</span></option>
- <option value="large"><span style="font-size: 18px;">Aa</span></option>
- <option value="veryLarge"><span style="font-size: 20px;">Aa</span></option>
- </MkRadios>
- <MkRadios v-model="instanceTicker">
- <template #desc>{{ $t('instanceTicker') }}</template>
- <option value="none">{{ $t('_instanceTicker.none') }}</option>
- <option value="remote">{{ $t('_instanceTicker.remote') }}</option>
- <option value="always">{{ $t('_instanceTicker.always') }}</option>
- </MkRadios>
- </div>
- </section>
+ <FormGroup>
+ <template #label>{{ $t('behavior') }}</template>
+ <FormSwitch v-model:value="imageNewTab">{{ $t('openImageInNewTab') }}</FormSwitch>
+ <FormSwitch v-model:value="enableInfiniteScroll">{{ $t('enableInfiniteScroll') }}</FormSwitch>
+ <FormSwitch v-model:value="disablePagesScript">{{ $t('disablePagesScript') }}</FormSwitch>
+ </FormGroup>
- <section class="_card _vMargin">
- <div class="_title"><Fa :icon="faColumns"/> {{ $t('deck') }}</div>
- <div class="_content">
- <div>{{ $t('defaultNavigationBehaviour') }}</div>
- <MkSwitch v-model:value="deckNavWindow">{{ $t('openInWindow') }}</MkSwitch>
- </div>
- <div class="_content">
- <MkSwitch v-model:value="deckAlwaysShowMainColumn">
- {{ $t('_deck.alwaysShowMainColumn') }}
- </MkSwitch>
- </div>
- <div class="_content">
- <div>{{ $t('_deck.columnAlign') }}</div>
- <MkRadio v-model="deckColumnAlign" value="left">{{ $t('left') }}</MkRadio>
- <MkRadio v-model="deckColumnAlign" value="center">{{ $t('center') }}</MkRadio>
- </div>
- </section>
+ <FormSelect v-model:value="serverDisconnectedBehavior">
+ <template #label>{{ $t('whenServerDisconnected') }}</template>
+ <option value="reload">{{ $t('_serverDisconnectedBehavior.reload') }}</option>
+ <option value="dialog">{{ $t('_serverDisconnectedBehavior.dialog') }}</option>
+ <option value="quiet">{{ $t('_serverDisconnectedBehavior.quiet') }}</option>
+ </FormSelect>
- <MkButton @click="cacheClear()" primary style="margin: var(--margin) auto;">{{ $t('cacheClear') }}</MkButton>
-</div>
+ <FormGroup>
+ <template #label>{{ $t('appearance') }}</template>
+ <FormSwitch v-model:value="disableAnimatedMfm">{{ $t('disableAnimatedMfm') }}</FormSwitch>
+ <FormSwitch v-model:value="reduceAnimation">{{ $t('reduceUiAnimation') }}</FormSwitch>
+ <FormSwitch v-model:value="useBlurEffectForModal">{{ $t('useBlurEffectForModal') }}</FormSwitch>
+ <FormSwitch v-model:value="useOsNativeEmojis">{{ $t('useOsNativeEmojis') }}
+ <div><Mfm text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></div>
+ </FormSwitch>
+ <FormSwitch v-model:value="loadRawImages">{{ $t('loadRawImages') }}</FormSwitch>
+ <FormSwitch v-model:value="disableShowingAnimatedImages">{{ $t('disableShowingAnimatedImages') }}</FormSwitch>
+ </FormGroup>
+
+ <FormRadios v-model="fontSize">
+ <template #desc>{{ $t('fontSize') }}</template>
+ <option value="small"><span style="font-size: 14px;">Aa</span></option>
+ <option :value="null"><span style="font-size: 16px;">Aa</span></option>
+ <option value="large"><span style="font-size: 18px;">Aa</span></option>
+ <option value="veryLarge"><span style="font-size: 20px;">Aa</span></option>
+ </FormRadios>
+
+ <FormSelect v-model:value="instanceTicker">
+ <template #label>{{ $t('instanceTicker') }}</template>
+ <option value="none">{{ $t('_instanceTicker.none') }}</option>
+ <option value="remote">{{ $t('_instanceTicker.remote') }}</option>
+ <option value="always">{{ $t('_instanceTicker.always') }}</option>
+ </FormSelect>
+
+ <FormSelect v-model:value="nsfw">
+ <template #label>{{ $t('nsfw') }}</template>
+ <option value="respect">{{ $t('_nsfw.respect') }}</option>
+ <option value="ignore">{{ $t('_nsfw.ignore') }}</option>
+ <option value="force">{{ $t('_nsfw.force') }}</option>
+ </FormSelect>
+
+ <FormGroup>
+ <template #label>{{ $t('defaultNavigationBehaviour') }}</template>
+ <FormSwitch v-model:value="defaultSideView">{{ $t('openInSideView') }}</FormSwitch>
+ </FormGroup>
+
+ <FormSelect v-model:value="chatOpenBehavior">
+ <template #label>{{ $t('chatOpenBehavior') }}</template>
+ <option value="page">{{ $t('showInPage') }}</option>
+ <option value="window">{{ $t('openInWindow') }}</option>
+ <option value="popout">{{ $t('popout') }}</option>
+ </FormSelect>
+
+ <FormLink to="/settings/deck">{{ $t('deck') }}</FormLink>
+
+ <FormButton @click="cacheClear()" danger>{{ $t('cacheClear') }}</FormButton>
+</FormBase>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faImage, faCog, faColumns, faCogs } from '@fortawesome/free-solid-svg-icons';
-import MkButton from '@/components/ui/button.vue';
-import MkSwitch from '@/components/ui/switch.vue';
-import MkSelect from '@/components/ui/select.vue';
-import MkRadio from '@/components/ui/radio.vue';
-import MkRadios from '@/components/ui/radios.vue';
-import MkRange from '@/components/ui/range.vue';
+import FormSwitch from '@/components/form/switch.vue';
+import FormSelect from '@/components/form/select.vue';
+import FormRadios from '@/components/form/radios.vue';
+import FormBase from '@/components/form/base.vue';
+import FormGroup from '@/components/form/group.vue';
+import FormLink from '@/components/form/link.vue';
+import FormButton from '@/components/form/button.vue';
+import MkLink from '@/components/link.vue';
import { langs } from '@/config';
import { clientDb, set } from '@/db';
import * as os from '@/os';
export default defineComponent({
components: {
- MkButton,
- MkSwitch,
- MkSelect,
- MkRadio,
- MkRadios,
- MkRange,
+ MkLink,
+ FormSwitch,
+ FormSelect,
+ FormRadios,
+ FormBase,
+ FormGroup,
+ FormLink,
+ FormButton,
},
emits: ['info'],
@@ -167,11 +168,6 @@ export default defineComponent({
set(value) { this.$store.commit('device/set', { key: 'defaultSideView', value }); }
},
- deckNavWindow: {
- get() { return this.$store.state.device.deckNavWindow; },
- set(value) { this.$store.commit('device/set', { key: 'deckNavWindow', value }); }
- },
-
chatOpenBehavior: {
get() { return this.$store.state.device.chatOpenBehavior; },
set(value) { this.$store.commit('device/set', { key: 'chatOpenBehavior', value }); }
@@ -182,19 +178,24 @@ export default defineComponent({
set(value) { this.$store.commit('device/set', { key: 'instanceTicker', value }); }
},
- enableInfiniteScroll: {
- get() { return this.$store.state.device.enableInfiniteScroll; },
- set(value) { this.$store.commit('device/set', { key: 'enableInfiniteScroll', value }); }
+ loadRawImages: {
+ get() { return this.$store.state.device.loadRawImages; },
+ set(value) { this.$store.commit('device/set', { key: 'loadRawImages', value }); }
+ },
+
+ disableShowingAnimatedImages: {
+ get() { return this.$store.state.device.disableShowingAnimatedImages; },
+ set(value) { this.$store.commit('device/set', { key: 'disableShowingAnimatedImages', value }); }
},
- deckAlwaysShowMainColumn: {
- get() { return this.$store.state.device.deckAlwaysShowMainColumn; },
- set(value) { this.$store.commit('device/set', { key: 'deckAlwaysShowMainColumn', value }); }
+ nsfw: {
+ get() { return this.$store.state.device.nsfw; },
+ set(value) { this.$store.commit('device/set', { key: 'nsfw', value }); }
},
- deckColumnAlign: {
- get() { return this.$store.state.device.deckColumnAlign; },
- set(value) { this.$store.commit('device/set', { key: 'deckColumnAlign', value }); }
+ enableInfiniteScroll: {
+ get() { return this.$store.state.device.enableInfiniteScroll; },
+ set(value) { this.$store.commit('device/set', { key: 'enableInfiniteScroll', value }); }
},
},
diff --git a/src/client/pages/settings/index.vue b/src/client/pages/settings/index.vue
index 5451c8616b..a42a4614cc 100644
--- a/src/client/pages/settings/index.vue
+++ b/src/client/pages/settings/index.vue
@@ -1,35 +1,36 @@
<template>
<div class="vvcocwet" :class="{ wide: !narrow }" ref="el">
- <div class="nav" v-if="!narrow || page == null">
- <div class="menu">
- <div class="label">{{ $t('basicSettings') }}</div>
- <MkA class="item" :class="{ active: page === 'profile' }" replace to="/settings/profile"><Fa :icon="faUser" fixed-width class="icon"/>{{ $t('profile') }}</MkA>
- <MkA class="item" :class="{ active: page === 'privacy' }" replace to="/settings/privacy"><Fa :icon="faLockOpen" fixed-width class="icon"/>{{ $t('privacy') }}</MkA>
- <MkA class="item" :class="{ active: page === 'reaction' }" replace to="/settings/reaction"><Fa :icon="faLaugh" fixed-width class="icon"/>{{ $t('reaction') }}</MkA>
- <MkA class="item" :class="{ active: page === 'notifications' }" replace to="/settings/notifications"><Fa :icon="faBell" fixed-width class="icon"/>{{ $t('notifications') }}</MkA>
- <MkA class="item" :class="{ active: page === 'integration' }" replace to="/settings/integration"><Fa :icon="faShareAlt" fixed-width class="icon"/>{{ $t('integration') }}</MkA>
- <MkA class="item" :class="{ active: page === 'security' }" replace to="/settings/security"><Fa :icon="faLock" fixed-width class="icon"/>{{ $t('security') }}</MkA>
- </div>
- <div class="menu">
- <div class="label">{{ $t('clientSettings') }}</div>
- <MkA class="item" :class="{ active: page === 'general' }" replace to="/settings/general"><Fa :icon="faCogs" fixed-width class="icon"/>{{ $t('general') }}</MkA>
- <MkA class="item" :class="{ active: page === 'theme' }" replace to="/settings/theme"><Fa :icon="faPalette" fixed-width class="icon"/>{{ $t('theme') }}</MkA>
- <MkA class="item" :class="{ active: page === 'sidebar' }" replace to="/settings/sidebar"><Fa :icon="faListUl" fixed-width class="icon"/>{{ $t('sidebar') }}</MkA>
- <MkA class="item" :class="{ active: page === 'sounds' }" replace to="/settings/sounds"><Fa :icon="faMusic" fixed-width class="icon"/>{{ $t('sounds') }}</MkA>
- <MkA class="item" :class="{ active: page === 'plugins' }" replace to="/settings/plugins"><Fa :icon="faPlug" fixed-width class="icon"/>{{ $t('plugins') }}</MkA>
- </div>
- <div class="menu">
- <div class="label">{{ $t('otherSettings') }}</div>
- <MkA class="item" :class="{ active: page === 'import-export' }" replace to="/settings/import-export"><Fa :icon="faBoxes" fixed-width class="icon"/>{{ $t('importAndExport') }}</MkA>
- <MkA class="item" :class="{ active: page === 'mute-block' }" replace to="/settings/mute-block"><Fa :icon="faBan" fixed-width class="icon"/>{{ $t('muteAndBlock') }}</MkA>
- <MkA class="item" :class="{ active: page === 'word-mute' }" replace to="/settings/word-mute"><Fa :icon="faCommentSlash" fixed-width class="icon"/>{{ $t('wordMute') }}</MkA>
- <MkA class="item" :class="{ active: page === 'api' }" replace to="/settings/api"><Fa :icon="faKey" fixed-width class="icon"/>API</MkA>
- <MkA class="item" :class="{ active: page === 'other' }" replace to="/settings/other"><Fa :icon="faEllipsisH" fixed-width class="icon"/>{{ $t('other') }}</MkA>
- </div>
- <div class="menu">
- <button class="_button item" @click="logout">{{ $t('logout') }}</button>
- </div>
- </div>
+ <FormBase class="nav" v-if="!narrow || page == null" :force-wide="!narrow">
+ <FormGroup>
+ <template #label>{{ $t('basicSettings') }}</template>
+ <FormLink :active="page === 'profile'" replace to="/settings/profile"><template #icon><Fa :icon="faUser"/></template>{{ $t('profile') }}</FormLink>
+ <FormLink :active="page === 'privacy'" replace to="/settings/privacy"><template #icon><Fa :icon="faLockOpen"/></template>{{ $t('privacy') }}</FormLink>
+ <FormLink :active="page === 'reaction'" replace to="/settings/reaction"><template #icon><Fa :icon="faLaugh"/></template>{{ $t('reaction') }}</FormLink>
+ <FormLink :active="page === 'notifications'" replace to="/settings/notifications"><template #icon><Fa :icon="faBell"/></template>{{ $t('notifications') }}</FormLink>
+ <FormLink :active="page === 'email'" replace to="/settings/email"><template #icon><Fa :icon="faEnvelope"/></template>{{ $t('email') }}</FormLink>
+ <FormLink :active="page === 'integration'" replace to="/settings/integration"><template #icon><Fa :icon="faShareAlt"/></template>{{ $t('integration') }}</FormLink>
+ <FormLink :active="page === 'security'" replace to="/settings/security"><template #icon><Fa :icon="faLock"/></template>{{ $t('security') }}</FormLink>
+ </FormGroup>
+ <FormGroup>
+ <template #label>{{ $t('clientSettings') }}</template>
+ <FormLink :active="page === 'general'" replace to="/settings/general"><template #icon><Fa :icon="faCogs"/></template>{{ $t('general') }}</FormLink>
+ <FormLink :active="page === 'theme'" replace to="/settings/theme"><template #icon><Fa :icon="faPalette"/></template>{{ $t('theme') }}</FormLink>
+ <FormLink :active="page === 'sidebar'" replace to="/settings/sidebar"><template #icon><Fa :icon="faListUl"/></template>{{ $t('sidebar') }}</FormLink>
+ <FormLink :active="page === 'sounds'" replace to="/settings/sounds"><template #icon><Fa :icon="faMusic"/></template>{{ $t('sounds') }}</FormLink>
+ <FormLink :active="page === 'plugins'" replace to="/settings/plugins"><template #icon><Fa :icon="faPlug"/></template>{{ $t('plugins') }}</FormLink>
+ </FormGroup>
+ <FormGroup>
+ <template #label>{{ $t('otherSettings') }}</template>
+ <FormLink :active="page === 'import-export'" replace to="/settings/import-export"><template #icon><Fa :icon="faBoxes"/></template>{{ $t('importAndExport') }}</FormLink>
+ <FormLink :active="page === 'mute-block'" replace to="/settings/mute-block"><template #icon><Fa :icon="faBan"/></template>{{ $t('muteAndBlock') }}</FormLink>
+ <FormLink :active="page === 'word-mute'" replace to="/settings/word-mute"><template #icon><Fa :icon="faCommentSlash"/></template>{{ $t('wordMute') }}</FormLink>
+ <FormLink :active="page === 'api'" replace to="/settings/api"><template #icon><Fa :icon="faKey"/></template>API</FormLink>
+ <FormLink :active="page === 'other'" replace to="/settings/other"><template #icon><Fa :icon="faEllipsisH"/></template>{{ $t('other') }}</FormLink>
+ </FormGroup>
+ <FormGroup>
+ <FormButton @click="logout" danger>{{ $t('logout') }}</FormButton>
+ </FormGroup>
+ </FormBase>
<div class="main">
<component :is="component" @info="onInfo"/>
</div>
@@ -37,13 +38,25 @@
</template>
<script lang="ts">
-import { computed, defineAsyncComponent, defineComponent, onMounted, ref } from 'vue';
+import { computed, defineAsyncComponent, defineComponent, nextTick, onMounted, ref, watch } from 'vue';
import { faCog, faPalette, faPlug, faUser, faListUl, faLock, faCommentSlash, faMusic, faCogs, faEllipsisH, faBan, faShareAlt, faLockOpen, faKey, faBoxes } from '@fortawesome/free-solid-svg-icons';
-import { faLaugh, faBell } from '@fortawesome/free-regular-svg-icons';
+import { faLaugh, faBell, faEnvelope } from '@fortawesome/free-regular-svg-icons';
import { store } from '@/store';
import { i18n } from '@/i18n';
+import FormLink from '@/components/form/link.vue';
+import FormGroup from '@/components/form/group.vue';
+import FormBase from '@/components/form/base.vue';
+import FormButton from '@/components/form/button.vue';
+import { scroll } from '../../scripts/scroll';
export default defineComponent({
+ components: {
+ FormBase,
+ FormLink,
+ FormGroup,
+ FormButton,
+ },
+
props: {
page: {
type: String,
@@ -72,21 +85,35 @@ export default defineComponent({
case 'word-mute': return defineAsyncComponent(() => import('./word-mute.vue'));
case 'integration': return defineAsyncComponent(() => import('./integration.vue'));
case 'security': return defineAsyncComponent(() => import('./security.vue'));
+ case '2fa': return defineAsyncComponent(() => import('./2fa.vue'));
case 'api': return defineAsyncComponent(() => import('./api.vue'));
+ case 'apps': return defineAsyncComponent(() => import('./apps.vue'));
case 'other': return defineAsyncComponent(() => import('./other.vue'));
case 'general': return defineAsyncComponent(() => import('./general.vue'));
+ case 'email': return defineAsyncComponent(() => import('./email.vue'));
+ case 'email/address': return defineAsyncComponent(() => import('./email-address.vue'));
case 'theme': return defineAsyncComponent(() => import('./theme.vue'));
+ case 'theme/install': return defineAsyncComponent(() => import('./theme.install.vue'));
+ case 'theme/manage': return defineAsyncComponent(() => import('./theme.manage.vue'));
case 'sidebar': return defineAsyncComponent(() => import('./sidebar.vue'));
case 'sounds': return defineAsyncComponent(() => import('./sounds.vue'));
+ case 'deck': return defineAsyncComponent(() => import('./deck.vue'));
case 'plugins': return defineAsyncComponent(() => import('./plugins.vue'));
case 'import-export': return defineAsyncComponent(() => import('./import-export.vue'));
+ case 'account-info': return defineAsyncComponent(() => import('./account-info.vue'));
case 'regedit': return defineAsyncComponent(() => import('./regedit.vue'));
default: return null;
}
});
+ watch(component, () => {
+ nextTick(() => {
+ scroll(el.value, 0);
+ });
+ });
+
onMounted(() => {
- narrow.value = el.value.offsetWidth < 650;
+ narrow.value = el.value.offsetWidth < 1025;
});
return {
@@ -100,7 +127,7 @@ export default defineComponent({
store.dispatch('logout');
location.href = '/';
},
- faPalette, faPlug, faUser, faListUl, faLock, faLaugh, faCommentSlash, faMusic, faBell, faCogs, faEllipsisH, faBan, faShareAlt, faLockOpen, faKey, faBoxes,
+ faPalette, faPlug, faUser, faListUl, faLock, faLaugh, faCommentSlash, faMusic, faBell, faCogs, faEllipsisH, faBan, faShareAlt, faLockOpen, faKey, faBoxes, faEnvelope,
};
},
});
@@ -108,63 +135,19 @@ export default defineComponent({
<style lang="scss" scoped>
.vvcocwet {
- > .nav {
- > .menu {
- margin: 16px 0;
-
- > .label {
- padding: 8px 32px;
- font-size: 80%;
- opacity: 0.7;
- }
-
- > .item {
- display: block;
- width: 100%;
- box-sizing: border-box;
- padding: 0 32px;
- line-height: 40px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- //background: var(--panel);
- //border-bottom: solid 1px var(--divider);
- transition: padding 0.2s ease, color 0.1s ease;
-
- &:first-of-type {
- //border-top: solid 1px var(--divider);
- }
-
- &.active {
- color: var(--accent);
- padding-left: 42px;
- }
-
- &:hover {
- text-decoration: none;
- padding-left: 42px;
- }
-
- > .icon {
- margin-right: 0.5em;
- }
- }
- }
- }
-
&.wide {
display: flex;
+ max-width: 1100px;
+ margin: 0 auto;
> .nav {
- width: 30%;
- max-width: 300px;
- font-size: 0.95em;
- border-right: solid 1px var(--divider);
+ width: 32%;
+ box-sizing: border-box;
+ border-right: solid 0.5px var(--divider);
}
> .main {
flex: 1;
- padding: 32px;
--baseContentWidth: 100%;
::v-deep(._section) {
diff --git a/src/client/pages/settings/notifications.vue b/src/client/pages/settings/notifications.vue
index ff0c276398..d26a11ef78 100644
--- a/src/client/pages/settings/notifications.vue
+++ b/src/client/pages/settings/notifications.vue
@@ -1,29 +1,31 @@
<template>
-<div>
- <div class="_section">
- <MkButton full primary @click="configure"><Fa :icon="faCog"/> {{ $t('notificationSetting') }}</MkButton>
- </div>
- <div class="_section">
- <MkButton full @click="readAllNotifications">{{ $t('markAsReadAllNotifications') }}</MkButton>
- <MkButton full @click="readAllUnreadNotes">{{ $t('markAsReadAllUnreadNotes') }}</MkButton>
- <MkButton full @click="readAllMessagingMessages">{{ $t('markAsReadAllTalkMessages') }}</MkButton>
- </div>
-</div>
+<FormBase>
+ <FormLink @click="configure">{{ $t('notificationSetting') }}</FormLink>
+ <FormGroup>
+ <FormButton @click="readAllNotifications">{{ $t('markAsReadAllNotifications') }}</FormButton>
+ <FormButton @click="readAllUnreadNotes">{{ $t('markAsReadAllUnreadNotes') }}</FormButton>
+ <FormButton @click="readAllMessagingMessages">{{ $t('markAsReadAllTalkMessages') }}</FormButton>
+ </FormGroup>
+</FormBase>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faCog } from '@fortawesome/free-solid-svg-icons';
import { faBell } from '@fortawesome/free-regular-svg-icons';
-import MkButton from '@/components/ui/button.vue';
-import MkSwitch from '@/components/ui/switch.vue';
+import FormButton from '@/components/form/button.vue';
+import FormLink from '@/components/form/link.vue';
+import FormBase from '@/components/form/base.vue';
+import FormGroup from '@/components/form/group.vue';
import { notificationTypes } from '../../../types';
import * as os from '@/os';
export default defineComponent({
components: {
- MkButton,
- MkSwitch,
+ FormBase,
+ FormLink,
+ FormButton,
+ FormGroup,
},
emits: ['info'],
diff --git a/src/client/pages/settings/other.vue b/src/client/pages/settings/other.vue
index 9c44d1b4f4..b3bab0e232 100644
--- a/src/client/pages/settings/other.vue
+++ b/src/client/pages/settings/other.vue
@@ -1,40 +1,43 @@
<template>
-<div>
- <div class="_section">
- <div class="_card">
- <div class="_content">
- <MkSwitch v-model:value="$store.state.i.injectFeaturedNote" @update:value="onChangeInjectFeaturedNote">
- {{ $t('showFeaturedNotesInTimeline') }}
- </MkSwitch>
- </div>
- </div>
- </div>
- <div class="_section">
- <MkSwitch v-model:value="debug" @update:value="changeDebug">
+<FormBase>
+ <FormSwitch :value="$store.state.i.injectFeaturedNote" @update:value="onChangeInjectFeaturedNote">
+ {{ $t('showFeaturedNotesInTimeline') }}
+ </FormSwitch>
+
+ <FormLink to="/settings/account-info">{{ $t('accountInfo') }}</FormLink>
+
+ <FormGroup>
+ <FormSwitch v-model:value="debug" @update:value="changeDebug">
DEBUG MODE
- </MkSwitch>
- <div v-if="debug">
- <MkA to="/settings/regedit">RegEdit</MkA>
- <MkButton @click="taskmanager">Task Manager</MkButton>
- </div>
- </div>
-</div>
+ </FormSwitch>
+ <template v-if="debug">
+ <FormLink to="/settings/regedit">RegEdit</FormLink>
+ <FormButton @click="taskmanager">Task Manager</FormButton>
+ </template>
+ </FormGroup>
+</FormBase>
</template>
<script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue';
import { faEllipsisH } from '@fortawesome/free-solid-svg-icons';
-import MkSelect from '@/components/ui/select.vue';
-import MkSwitch from '@/components/ui/switch.vue';
-import MkButton from '@/components/ui/button.vue';
+import FormSwitch from '@/components/form/switch.vue';
+import FormSelect from '@/components/form/select.vue';
+import FormLink from '@/components/form/link.vue';
+import FormBase from '@/components/form/base.vue';
+import FormGroup from '@/components/form/group.vue';
+import FormButton from '@/components/form/button.vue';
import * as os from '@/os';
import { debug } from '@/config';
export default defineComponent({
components: {
- MkSelect,
- MkSwitch,
- MkButton,
+ FormBase,
+ FormSelect,
+ FormSwitch,
+ FormButton,
+ FormLink,
+ FormGroup,
},
emits: ['info'],
diff --git a/src/client/pages/settings/privacy.vue b/src/client/pages/settings/privacy.vue
index 27a949836a..09db077502 100644
--- a/src/client/pages/settings/privacy.vue
+++ b/src/client/pages/settings/privacy.vue
@@ -1,36 +1,43 @@
<template>
-<div class="_section">
- <div class="_card">
- <div class="_content">
- <MkSwitch v-model:value="isLocked" @update:value="save()">{{ $t('makeFollowManuallyApprove') }}</MkSwitch>
- <MkSwitch v-model:value="autoAcceptFollowed" v-if="isLocked" @update:value="save()">{{ $t('autoAcceptFollowed') }}</MkSwitch>
- </div>
- <div class="_content">
- <MkSwitch v-model:value="rememberNoteVisibility" @update:value="save()">{{ $t('rememberNoteVisibility') }}</MkSwitch>
- <MkSelect v-model:value="defaultNoteVisibility" style="margin-bottom: 8px;" v-if="!rememberNoteVisibility">
- <template #label>{{ $t('defaultNoteVisibility') }}</template>
- <option value="public">{{ $t('_visibility.public') }}</option>
- <option value="home">{{ $t('_visibility.home') }}</option>
- <option value="followers">{{ $t('_visibility.followers') }}</option>
- <option value="specified">{{ $t('_visibility.specified') }}</option>
- </MkSelect>
- <MkSwitch v-model:value="defaultNoteLocalOnly" v-if="!rememberNoteVisibility">{{ $t('_visibility.localOnly') }}</MkSwitch>
- </div>
- </div>
-</div>
+<FormBase>
+ <FormGroup>
+ <FormSwitch v-model:value="isLocked" @update:value="save()">{{ $t('makeFollowManuallyApprove') }}</FormSwitch>
+ <FormSwitch v-model:value="autoAcceptFollowed" :disabled="!isLocked" @update:value="save()">{{ $t('autoAcceptFollowed') }}</FormSwitch>
+ <template #caption>{{ $t('lockedAccountInfo') }}</template>
+ </FormGroup>
+ <FormSwitch v-model:value="noCrawle" @update:value="save()">
+ {{ $t('noCrawle') }}
+ <template #desc>{{ $t('noCrawleDescription') }}</template>
+ </FormSwitch>
+ <FormSwitch v-model:value="rememberNoteVisibility" @update:value="save()">{{ $t('rememberNoteVisibility') }}</FormSwitch>
+ <FormGroup v-if="!rememberNoteVisibility">
+ <template #label>{{ $t('defaultNoteVisibility') }}</template>
+ <FormSelect v-model:value="defaultNoteVisibility">
+ <option value="public">{{ $t('_visibility.public') }}</option>
+ <option value="home">{{ $t('_visibility.home') }}</option>
+ <option value="followers">{{ $t('_visibility.followers') }}</option>
+ <option value="specified">{{ $t('_visibility.specified') }}</option>
+ </FormSelect>
+ <FormSwitch v-model:value="defaultNoteLocalOnly">{{ $t('_visibility.localOnly') }}</FormSwitch>
+ </FormGroup>
+</FormBase>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faLockOpen } from '@fortawesome/free-solid-svg-icons';
-import MkSelect from '@/components/ui/select.vue';
-import MkSwitch from '@/components/ui/switch.vue';
+import FormSwitch from '@/components/form/switch.vue';
+import FormSelect from '@/components/form/select.vue';
+import FormBase from '@/components/form/base.vue';
+import FormGroup from '@/components/form/group.vue';
import * as os from '@/os';
export default defineComponent({
components: {
- MkSelect,
- MkSwitch,
+ FormBase,
+ FormSelect,
+ FormGroup,
+ FormSwitch,
},
emits: ['info'],
@@ -43,6 +50,7 @@ export default defineComponent({
},
isLocked: false,
autoAcceptFollowed: false,
+ noCrawle: false,
}
},
@@ -66,6 +74,7 @@ export default defineComponent({
created() {
this.isLocked = this.$store.state.i.isLocked;
this.autoAcceptFollowed = this.$store.state.i.autoAcceptFollowed;
+ this.noCrawle = this.$store.state.i.noCrawle;
},
mounted() {
@@ -77,6 +86,7 @@ export default defineComponent({
os.api('i/update', {
isLocked: !!this.isLocked,
autoAcceptFollowed: !!this.autoAcceptFollowed,
+ noCrawle: !!this.noCrawle,
});
}
}
diff --git a/src/client/pages/settings/profile.vue b/src/client/pages/settings/profile.vue
index 6a523e08cf..4fc4783c49 100644
--- a/src/client/pages/settings/profile.vue
+++ b/src/client/pages/settings/profile.vue
@@ -1,79 +1,67 @@
<template>
-<div class="_section">
- <div class="llvierxe _card">
- <div class="_title"><Fa :icon="faUser"/> {{ $t('profile') }}<small style="display: block; font-weight: normal; opacity: 0.6;">@{{ $store.state.i.username }}@{{ host }}</small></div>
- <div class="_content">
- <div class="header" :style="{ backgroundImage: $store.state.i.bannerUrl ? `url(${ $store.state.i.bannerUrl })` : null }" @click="changeBanner">
- <MkAvatar class="avatar" :user="$store.state.i" :disable-preview="true" :disable-link="true" @click.stop="changeAvatar"/>
- </div>
-
- <MkInput v-model:value="name" :max="30">
- <span>{{ $t('_profile.name') }}</span>
- </MkInput>
+<FormBase class="llvierxe">
+ <div class="header _formItem" :style="{ backgroundImage: $store.state.i.bannerUrl ? `url(${ $store.state.i.bannerUrl })` : null }" @click="changeBanner">
+ <MkAvatar class="avatar" :user="$store.state.i" :disable-preview="true" :disable-link="true" @click.stop="changeAvatar"/>
+ </div>
- <MkTextarea v-model:value="description" :max="500">
- <span>{{ $t('_profile.description') }}</span>
- <template #desc>{{ $t('_profile.youCanIncludeHashtags') }}</template>
- </MkTextarea>
+ <FormInput v-model:value="name" :max="30">
+ <span>{{ $t('_profile.name') }}</span>
+ </FormInput>
- <MkInput v-model:value="location">
- <span>{{ $t('location') }}</span>
- <template #prefix><Fa :icon="faMapMarkerAlt"/></template>
- </MkInput>
+ <FormTextarea v-model:value="description" :max="500">
+ <span>{{ $t('_profile.description') }}</span>
+ <template #desc>{{ $t('_profile.youCanIncludeHashtags') }}</template>
+ </FormTextarea>
- <MkInput v-model:value="birthday" type="date">
- <template #title>{{ $t('birthday') }}</template>
- <template #prefix><Fa :icon="faBirthdayCake"/></template>
- </MkInput>
+ <FormInput v-model:value="location">
+ <span>{{ $t('location') }}</span>
+ <template #prefix><Fa :icon="faMapMarkerAlt"/></template>
+ </FormInput>
- <details class="fields">
- <summary>{{ $t('_profile.metadata') }}</summary>
- <div class="row">
- <MkInput v-model:value="fieldName0">{{ $t('_profile.metadataLabel') }}</MkInput>
- <MkInput v-model:value="fieldValue0">{{ $t('_profile.metadataContent') }}</MkInput>
- </div>
- <div class="row">
- <MkInput v-model:value="fieldName1">{{ $t('_profile.metadataLabel') }}</MkInput>
- <MkInput v-model:value="fieldValue1">{{ $t('_profile.metadataContent') }}</MkInput>
- </div>
- <div class="row">
- <MkInput v-model:value="fieldName2">{{ $t('_profile.metadataLabel') }}</MkInput>
- <MkInput v-model:value="fieldValue2">{{ $t('_profile.metadataContent') }}</MkInput>
- </div>
- <div class="row">
- <MkInput v-model:value="fieldName3">{{ $t('_profile.metadataLabel') }}</MkInput>
- <MkInput v-model:value="fieldValue3">{{ $t('_profile.metadataContent') }}</MkInput>
- </div>
- </details>
+ <FormInput v-model:value="birthday" type="date">
+ <span>{{ $t('birthday') }}</span>
+ <template #prefix><Fa :icon="faBirthdayCake"/></template>
+ </FormInput>
- <MkSwitch v-model:value="isBot">{{ $t('flagAsBot') }}</MkSwitch>
- <MkSwitch v-model:value="isCat">{{ $t('flagAsCat') }}</MkSwitch>
- </div>
- <div class="_footer">
- <MkButton @click="save(true)" primary><Fa :icon="faSave"/> {{ $t('save') }}</MkButton>
- </div>
- </div>
-</div>
+ <FormGroup>
+ <FormButton @click="editMetadata" primary>{{ $t('_profile.metadataEdit') }}</FormButton>
+ <template #caption>{{ $t('_profile.metadataDescription') }}</template>
+ </FormGroup>
+
+ <FormSwitch v-model:value="isCat">{{ $t('flagAsCat') }}<template #desc>{{ $t('flagAsCatDescription') }}</template></FormSwitch>
+
+ <FormSwitch v-model:value="isBot">{{ $t('flagAsBot') }}<template #desc>{{ $t('flagAsBotDescription') }}</template></FormSwitch>
+
+ <FormSwitch v-model:value="alwaysMarkNsfw">{{ $t('alwaysMarkSensitive') }}</FormSwitch>
+
+ <FormButton @click="save(true)" primary><Fa :icon="faSave"/> {{ $t('save') }}</FormButton>
+</FormBase>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faUnlockAlt, faCogs, faUser, faMapMarkerAlt, faBirthdayCake } from '@fortawesome/free-solid-svg-icons';
import { faSave } from '@fortawesome/free-regular-svg-icons';
-import MkButton from '@/components/ui/button.vue';
-import MkInput from '@/components/ui/input.vue';
-import MkTextarea from '@/components/ui/textarea.vue';
-import MkSwitch from '@/components/ui/switch.vue';
+import FormButton from '@/components/form/button.vue';
+import FormInput from '@/components/form/input.vue';
+import FormTextarea from '@/components/form/textarea.vue';
+import FormSwitch from '@/components/form/switch.vue';
+import FormTuple from '@/components/form/tuple.vue';
+import FormBase from '@/components/form/base.vue';
+import FormGroup from '@/components/form/group.vue';
import { host } from '@/config';
import { selectFile } from '@/scripts/select-file';
import * as os from '@/os';
export default defineComponent({
components: {
- MkButton,
- MkInput,
- MkTextarea,
- MkSwitch,
+ FormButton,
+ FormInput,
+ FormTextarea,
+ FormSwitch,
+ FormTuple,
+ FormBase,
+ FormGroup,
},
emits: ['info'],
@@ -101,6 +89,7 @@ export default defineComponent({
bannerId: null,
isBot: false,
isCat: false,
+ alwaysMarkNsfw: false,
saving: false,
faSave, faUnlockAlt, faCogs, faUser, faMapMarkerAlt, faBirthdayCake
}
@@ -115,6 +104,7 @@ export default defineComponent({
this.bannerId = this.$store.state.i.bannerId;
this.isBot = this.$store.state.i.isBot;
this.isCat = this.$store.state.i.isCat;
+ this.alwaysMarkNsfw = this.$store.state.i.alwaysMarkNsfw;
this.fieldName0 = this.$store.state.i.fields[0] ? this.$store.state.i.fields[0].name : null;
this.fieldValue0 = this.$store.state.i.fields[0] ? this.$store.state.i.fields[0].value : null;
@@ -147,7 +137,60 @@ export default defineComponent({
});
},
- save(notify) {
+ async editMetadata() {
+ const { canceled, result } = await os.form(this.$t('_profile.metadata'), {
+ fieldName0: {
+ type: 'string',
+ label: this.$t('_profile.metadataLabel') + ' 1',
+ default: this.fieldName0,
+ },
+ fieldValue0: {
+ type: 'string',
+ label: this.$t('_profile.metadataContent') + ' 1',
+ default: this.fieldValue0,
+ },
+ fieldName1: {
+ type: 'string',
+ label: this.$t('_profile.metadataLabel') + ' 2',
+ default: this.fieldName1,
+ },
+ fieldValue1: {
+ type: 'string',
+ label: this.$t('_profile.metadataContent') + ' 2',
+ default: this.fieldValue1,
+ },
+ fieldName2: {
+ type: 'string',
+ label: this.$t('_profile.metadataLabel') + ' 3',
+ default: this.fieldName2,
+ },
+ fieldValue2: {
+ type: 'string',
+ label: this.$t('_profile.metadataContent') + ' 3',
+ default: this.fieldValue2,
+ },
+ fieldName3: {
+ type: 'string',
+ label: this.$t('_profile.metadataLabel') + ' 4',
+ default: this.fieldName3,
+ },
+ fieldValue3: {
+ type: 'string',
+ label: this.$t('_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 },
@@ -155,6 +198,19 @@ export default defineComponent({
{ name: this.fieldName3, value: this.fieldValue3 },
];
+ os.api('i/update', {
+ fields,
+ }).then(i => {
+ os.success();
+ }).catch(err => {
+ os.dialog({
+ type: 'error',
+ text: err.id
+ });
+ });
+ },
+
+ save(notify) {
this.saving = true;
os.api('i/update', {
@@ -162,9 +218,9 @@ export default defineComponent({
description: this.description || null,
location: this.location || null,
birthday: this.birthday || null,
- fields,
isBot: !!this.isBot,
isCat: !!this.isCat,
+ alwaysMarkNsfw: !!this.alwaysMarkNsfw,
}).then(i => {
this.saving = false;
this.$store.state.i.avatarId = i.avatarId;
@@ -189,41 +245,29 @@ export default defineComponent({
<style lang="scss" scoped>
.llvierxe {
- > ._content {
- > .header {
- position: relative;
- height: 150px;
- overflow: hidden;
- background-size: cover;
- background-position: center;
- border-radius: 5px;
- border: solid 1px var(--divider);
- box-sizing: border-box;
- cursor: pointer;
+ > .header {
+ position: relative;
+ height: 150px;
+ overflow: hidden;
+ background-size: cover;
+ background-position: center;
+ border-radius: 5px;
+ border: solid 1px var(--divider);
+ box-sizing: border-box;
+ cursor: pointer;
- > .avatar {
- position: absolute;
- top: 0;
- bottom: 0;
- left: 0;
- right: 0;
- display: block;
- width: 72px;
- height: 72px;
- margin: auto;
- cursor: pointer;
- box-shadow: 0 0 0 6px rgba(0, 0, 0, 0.5);
- }
- }
-
- > .fields {
- > .row {
- > * {
- display: inline-block;
- width: 50%;
- margin-bottom: 0;
- }
- }
+ > .avatar {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ display: block;
+ width: 72px;
+ height: 72px;
+ margin: auto;
+ cursor: pointer;
+ box-shadow: 0 0 0 6px rgba(0, 0, 0, 0.5);
}
}
}
diff --git a/src/client/pages/settings/reaction.vue b/src/client/pages/settings/reaction.vue
index 88de091441..75dae29068 100644
--- a/src/client/pages/settings/reaction.vue
+++ b/src/client/pages/settings/reaction.vue
@@ -1,9 +1,8 @@
<template>
-<div class="_section">
- <div class="_card">
- <div class="_title"><Fa :icon="faLaugh"/> {{ $t('reaction') }}</div>
- <div class="_content">
- <div class="_caption" style="padding: 0 8px 8px 8px;">{{ $t('reactionSettingDescription') }}</div>
+<FormBase>
+ <div class="_formItem">
+ <div class="_formLabel">{{ $t('reactionSettingDescription') }}</div>
+ <div class="_formPanel">
<XDraggable class="zoaiodol" :list="reactions" animation="150" delay="100" delay-on-touch-only="true">
<button class="_button item" v-for="reaction in reactions" :key="reaction" @click="remove(reaction, $event)">
<MkEmoji :emoji="reaction" :normal="true"/>
@@ -12,26 +11,25 @@
<button>a</button>
</template>
</XDraggable>
- <div class="_caption" style="padding: 8px;">{{ $t('reactionSettingDescription2') }} <button class="_textButton" @click="chooseEmoji">{{ $t('chooseEmoji') }}</button></div>
- <MkRadios v-model="reactionPickerWidth">
- <template #desc>{{ $t('width') }}</template>
- <option :value="1">{{ $t('small') }}</option>
- <option :value="2">{{ $t('medium') }}</option>
- <option :value="3">{{ $t('large') }}</option>
- </MkRadios>
- <MkRadios v-model="reactionPickerHeight">
- <template #desc>{{ $t('height') }}</template>
- <option :value="1">{{ $t('small') }}</option>
- <option :value="2">{{ $t('medium') }}</option>
- <option :value="3">{{ $t('large') }}</option>
- </MkRadios>
- </div>
- <div class="_footer">
- <MkButton inline @click="preview"><Fa :icon="faEye"/> {{ $t('preview') }}</MkButton>
- <MkButton inline @click="setDefault"><Fa :icon="faUndo"/> {{ $t('default') }}</MkButton>
</div>
+ <div class="_formCaption">{{ $t('reactionSettingDescription2') }} <button class="_textButton" @click="chooseEmoji">{{ $t('chooseEmoji') }}</button></div>
</div>
-</div>
+
+ <FormRadios v-model="reactionPickerWidth">
+ <template #desc>{{ $t('width') }}</template>
+ <option :value="1">{{ $t('small') }}</option>
+ <option :value="2">{{ $t('medium') }}</option>
+ <option :value="3">{{ $t('large') }}</option>
+ </FormRadios>
+ <FormRadios v-model="reactionPickerHeight">
+ <template #desc>{{ $t('height') }}</template>
+ <option :value="1">{{ $t('small') }}</option>
+ <option :value="2">{{ $t('medium') }}</option>
+ <option :value="3">{{ $t('large') }}</option>
+ </FormRadios>
+ <FormButton @click="preview"><Fa :icon="faEye"/> {{ $t('preview') }}</FormButton>
+ <FormButton danger @click="setDefault"><Fa :icon="faUndo"/> {{ $t('default') }}</FormButton>
+</FormBase>
</template>
<script lang="ts">
@@ -39,20 +37,19 @@ import { defineComponent } from 'vue';
import { faLaugh, faSave, faEye } from '@fortawesome/free-regular-svg-icons';
import { faUndo } from '@fortawesome/free-solid-svg-icons';
import { VueDraggableNext } from 'vue-draggable-next';
-import MkInput from '@/components/ui/input.vue';
-import MkButton from '@/components/ui/button.vue';
-import MkSwitch from '@/components/ui/switch.vue';
-import MkRadios from '@/components/ui/radios.vue';
-import { emojiRegexWithCustom } from '../../../misc/emoji-regex';
+import FormInput from '@/components/form/input.vue';
+import FormRadios from '@/components/form/radios.vue';
+import FormBase from '@/components/form/base.vue';
+import FormButton from '@/components/form/button.vue';
import { defaultSettings } from '@/store';
import * as os from '@/os';
export default defineComponent({
components: {
- MkInput,
- MkButton,
- MkSwitch,
- MkRadios,
+ FormInput,
+ FormButton,
+ FormBase,
+ FormRadios,
XDraggable: VueDraggableNext,
},
@@ -62,7 +59,11 @@ export default defineComponent({
return {
INFO: {
title: this.$t('reaction'),
- icon: faLaugh
+ icon: faLaugh,
+ action: {
+ icon: faEye,
+ handler: this.preview
+ }
},
reactions: JSON.parse(JSON.stringify(this.$store.state.settings.reactions)),
faLaugh, faSave, faEye, faUndo
@@ -144,8 +145,6 @@ export default defineComponent({
<style lang="scss" scoped>
.zoaiodol {
- border: solid 1px var(--divider);
- border-radius: var(--radius);
padding: 16px;
> .item {
diff --git a/src/client/pages/settings/security.vue b/src/client/pages/settings/security.vue
index 98863679c4..8d84d08f78 100644
--- a/src/client/pages/settings/security.vue
+++ b/src/client/pages/settings/security.vue
@@ -1,29 +1,45 @@
<template>
-<div>
- <div class="_section">
- <X2fa/>
- </div>
- <div class="_section">
- <MkButton primary @click="change()" full>{{ $t('changePassword') }}</MkButton>
- </div>
- <div class="_section">
- <MkButton class="_vMargin" primary @click="regenerateToken" full><Fa :icon="faSyncAlt"/> {{ $t('regenerateLoginToken') }}</MkButton>
- <div class="_caption _vMargin" style="padding: 0 6px;">{{ $t('regenerateLoginTokenDescription') }}</div>
- </div>
-</div>
+<FormBase>
+ <X2fa/>
+ <FormLink to="/settings/2fa"><template #icon><Fa :icon="faMobileAlt"/></template>{{ $t('twoStepAuthentication') }}</FormLink>
+ <FormButton primary @click="change()">{{ $t('changePassword') }}</FormButton>
+ <FormPagination :pagination="pagination">
+ <template #label>{{ $t('signinHistory') }}</template>
+ <template #default="{items}">
+ <div class="_formPanel timnmucd" v-for="item in items" :key="item.id">
+ <header>
+ <Fa class="icon succ" :icon="faCheck" v-if="item.success"/>
+ <Fa class="icon fail" :icon="faTimesCircle" v-else/>
+ <code class="ip _monospace">{{ item.ip }}</code>
+ <MkTime :time="item.createdAt" class="time"/>
+ </header>
+ </div>
+ </template>
+ </FormPagination>
+ <FormGroup>
+ <FormButton danger @click="regenerateToken"><Fa :icon="faSyncAlt"/> {{ $t('regenerateLoginToken') }}</FormButton>
+ <template #caption>{{ $t('regenerateLoginTokenDescription') }}</template>
+ </FormGroup>
+</FormBase>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
-import { faLock, faSyncAlt } from '@fortawesome/free-solid-svg-icons';
-import MkButton from '@/components/ui/button.vue';
-import X2fa from './security.2fa.vue';
+import { faCheck, faTimesCircle, faLock, faSyncAlt, faMobileAlt } from '@fortawesome/free-solid-svg-icons';
+import FormBase from '@/components/form/base.vue';
+import FormLink from '@/components/form/link.vue';
+import FormGroup from '@/components/form/group.vue';
+import FormButton from '@/components/form/button.vue';
+import FormPagination from '@/components/form/pagination.vue';
import * as os from '@/os';
export default defineComponent({
components: {
- MkButton,
- X2fa,
+ FormBase,
+ FormLink,
+ FormButton,
+ FormPagination,
+ FormGroup,
},
emits: ['info'],
@@ -34,7 +50,11 @@ export default defineComponent({
title: this.$t('security'),
icon: faLock
},
- faLock, faSyncAlt
+ pagination: {
+ endpoint: 'i/signin-history',
+ limit: 5,
+ },
+ faLock, faSyncAlt, faCheck, faTimesCircle, faMobileAlt,
}
},
@@ -98,3 +118,32 @@ export default defineComponent({
}
});
</script>
+
+<style lang="scss" scoped>
+.timnmucd {
+ padding: 16px;
+
+ > header {
+ display: flex;
+ align-items: center;
+
+ > .icon {
+ width: 1em;
+ margin-right: 0.75em;
+
+ &.succ {
+ color: var(--success);
+ }
+
+ &.fail {
+ color: var(--error);
+ }
+ }
+
+ > .time {
+ margin-left: auto;
+ opacity: 0.7;
+ }
+ }
+}
+</style>
diff --git a/src/client/pages/settings/sidebar.vue b/src/client/pages/settings/sidebar.vue
index 2ab5acf936..4138aaf733 100644
--- a/src/client/pages/settings/sidebar.vue
+++ b/src/client/pages/settings/sidebar.vue
@@ -1,41 +1,41 @@
<template>
-<div class="_section">
- <div class="_card">
- <div class="_content">
- <MkTextarea v-model:value="items" tall>
- <span>{{ $t('sidebar') }}</span>
- <template #desc><button class="_textButton" @click="addItem">{{ $t('addItem') }}</button></template>
- </MkTextarea>
- </div>
- <div class="_content">
- <div>{{ $t('display') }}</div>
- <MkRadio v-model="sidebarDisplay" value="full">{{ $t('_sidebar.full') }}</MkRadio>
- <MkRadio v-model="sidebarDisplay" value="icon">{{ $t('_sidebar.icon') }}</MkRadio>
- <!-- <MkRadio v-model="sidebarDisplay" value="hide" disabled>{{ $t('_sidebar.hide') }}</MkRadio>--> <!-- TODO: サイドバーを完全に隠せるようにすると、別途ハンバーガーボタンのようなものをUIに表示する必要があり面倒 -->
- </div>
- <div class="_footer">
- <MkButton inline @click="save()" primary><Fa :icon="faSave"/> {{ $t('save') }}</MkButton>
- <MkButton inline @click="reset()"><Fa :icon="faRedo"/> {{ $t('default') }}</MkButton>
- </div>
- </div>
-</div>
+<FormBase>
+ <FormTextarea v-model:value="items" tall>
+ <span>{{ $t('sidebar') }}</span>
+ <template #desc><button class="_textButton" @click="addItem">{{ $t('addItem') }}</button></template>
+ </FormTextarea>
+
+ <FormRadios v-model="sidebarDisplay">
+ <template #desc>{{ $t('display') }}</template>
+ <option value="full">{{ $t('_sidebar.full') }}</option>
+ <option value="icon">{{ $t('_sidebar.icon') }}</option>
+ <!-- <MkRadio v-model="sidebarDisplay" value="hide" disabled>{{ $t('_sidebar.hide') }}</MkRadio>--> <!-- TODO: サイドバーを完全に隠せるようにすると、別途ハンバーガーボタンのようなものをUIに表示する必要があり面倒 -->
+ </FormRadios>
+
+ <FormButton @click="save()" primary><Fa :icon="faSave"/> {{ $t('save') }}</FormButton>
+ <FormButton @click="reset()" danger><Fa :icon="faRedo"/> {{ $t('default') }}</FormButton>
+</FormBase>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faListUl, faSave, faRedo } from '@fortawesome/free-solid-svg-icons';
-import MkButton from '@/components/ui/button.vue';
-import MkTextarea from '@/components/ui/textarea.vue';
-import MkRadio from '@/components/ui/radio.vue';
+import FormSwitch from '@/components/form/switch.vue';
+import FormTextarea from '@/components/form/textarea.vue';
+import FormRadios from '@/components/form/radios.vue';
+import FormBase from '@/components/form/base.vue';
+import FormGroup from '@/components/form/group.vue';
+import FormButton from '@/components/form/button.vue';
import { defaultDeviceUserSettings } from '@/store';
import * as os from '@/os';
import { sidebarDef } from '@/sidebar';
export default defineComponent({
components: {
- MkButton,
- MkTextarea,
- MkRadio,
+ FormBase,
+ FormButton,
+ FormTextarea,
+ FormRadios,
},
emits: ['info'],
@@ -102,7 +102,3 @@ export default defineComponent({
},
});
</script>
-
-<style lang="scss" scoped>
-
-</style>
diff --git a/src/client/pages/settings/sounds.vue b/src/client/pages/settings/sounds.vue
index fc6b751fed..f19be54e82 100644
--- a/src/client/pages/settings/sounds.vue
+++ b/src/client/pages/settings/sounds.vue
@@ -1,62 +1,35 @@
<template>
-<div class="_section">
- <div class="_card">
- <div class="_title"><Fa :icon="faMusic"/> {{ $t('sounds') }}</div>
- <div class="_content">
- <MkRange v-model:value="sfxVolume" :min="0" :max="1" :step="0.1">
- <Fa slot="icon" :icon="volumeIcon"/>
- <span slot="title">{{ $t('volume') }}</span>
- </MkRange>
- </div>
- <div class="_content">
- <MkSelect v-model:value="sfxNote">
- <template #label>{{ $t('_sfx.note') }}</template>
- <option v-for="sound in sounds" :value="sound" :key="sound">{{ sound || $t('none') }}</option>
- <template #text><button class="_textButton" @click="listen(sfxNote)" v-if="sfxNote"><Fa :icon="faPlay"/> {{ $t('listen') }}</button></template>
- </MkSelect>
- <MkSelect v-model:value="sfxNoteMy">
- <template #label>{{ $t('_sfx.noteMy') }}</template>
- <option v-for="sound in sounds" :value="sound" :key="sound">{{ sound || $t('none') }}</option>
- <template #text><button class="_textButton" @click="listen(sfxNoteMy)" v-if="sfxNoteMy"><Fa :icon="faPlay"/> {{ $t('listen') }}</button></template>
- </MkSelect>
- <MkSelect v-model:value="sfxNotification">
- <template #label>{{ $t('_sfx.notification') }}</template>
- <option v-for="sound in sounds" :value="sound" :key="sound">{{ sound || $t('none') }}</option>
- <template #text><button class="_textButton" @click="listen(sfxNotification)" v-if="sfxNotification"><Fa :icon="faPlay"/> {{ $t('listen') }}</button></template>
- </MkSelect>
- <MkSelect v-model:value="sfxChat">
- <template #label>{{ $t('_sfx.chat') }}</template>
- <option v-for="sound in sounds" :value="sound" :key="sound">{{ sound || $t('none') }}</option>
- <template #text><button class="_textButton" @click="listen(sfxChat)" v-if="sfxChat"><Fa :icon="faPlay"/> {{ $t('listen') }}</button></template>
- </MkSelect>
- <MkSelect v-model:value="sfxChatBg">
- <template #label>{{ $t('_sfx.chatBg') }}</template>
- <option v-for="sound in sounds" :value="sound" :key="sound">{{ sound || $t('none') }}</option>
- <template #text><button class="_textButton" @click="listen(sfxChatBg)" v-if="sfxChatBg"><Fa :icon="faPlay"/> {{ $t('listen') }}</button></template>
- </MkSelect>
- <MkSelect v-model:value="sfxAntenna">
- <template #label>{{ $t('_sfx.antenna') }}</template>
- <option v-for="sound in sounds" :value="sound" :key="sound">{{ sound || $t('none') }}</option>
- <template #text><button class="_textButton" @click="listen(sfxAntenna)" v-if="sfxAntenna"><Fa :icon="faPlay"/> {{ $t('listen') }}</button></template>
- </MkSelect>
- <MkSelect v-model:value="sfxChannel">
- <template #label>{{ $t('_sfx.channel') }}</template>
- <option v-for="sound in sounds" :value="sound" :key="sound">{{ sound || $t('none') }}</option>
- <template #text><button class="_textButton" @click="listen(sfxChannel)" v-if="sfxChannel"><Fa :icon="faPlay"/> {{ $t('listen') }}</button></template>
- </MkSelect>
- </div>
- </div>
-</div>
+<FormBase>
+ <FormRange v-model:value="masterVolume" :min="0" :max="1" :step="0.05">
+ <template #label><Fa :icon="volumeIcon" :key="volumeIcon"/> {{ $t('masterVolume') }}</template>
+ </FormRange>
+
+ <FormGroup>
+ <template #label>{{ $t('sounds') }}</template>
+ <FormButton v-for="type in Object.keys(sounds)" :key="type" :center="false" @click="edit(type)">
+ {{ $t('_sfx.' + type) }}
+ <template #suffix>{{ sounds[type].type || $t('none') }}</template>
+ <template #suffixIcon><Fa :icon="faChevronDown"/></template>
+ </FormButton>
+ </FormGroup>
+
+ <FormButton @click="reset()" danger><Fa :icon="faRedo"/> {{ $t('default') }}</FormButton>
+</FormBase>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
-import { faMusic, faPlay, faVolumeUp, faVolumeMute } from '@fortawesome/free-solid-svg-icons';
-import MkSelect from '@/components/ui/select.vue';
-import MkRange from '@/components/ui/range.vue';
+import { faMusic, faPlay, faVolumeUp, faVolumeMute, faChevronDown, faRedo } from '@fortawesome/free-solid-svg-icons';
+import FormRange from '@/components/form/range.vue';
+import FormSelect from '@/components/form/select.vue';
+import FormBase from '@/components/form/base.vue';
+import FormButton from '@/components/form/button.vue';
+import FormGroup from '@/components/form/group.vue';
import * as os from '@/os';
+import { device, defaultDeviceSettings } from '@/cold-storage';
+import { playFile } from '@/scripts/sound';
-const sounds = [
+const soundsTypes = [
null,
'syuilo/up',
'syuilo/down',
@@ -73,6 +46,8 @@ const sounds = [
'syuilo/square-pico',
'syuilo/reverved',
'syuilo/ryukyu',
+ 'syuilo/kick',
+ 'syuilo/snare',
'aisha/1',
'aisha/2',
'aisha/3',
@@ -82,71 +57,98 @@ const sounds = [
export default defineComponent({
components: {
- MkSelect,
- MkRange,
+ FormSelect,
+ FormButton,
+ FormBase,
+ FormRange,
+ FormGroup,
},
+ emits: ['info'],
+
data() {
return {
- sounds,
- faMusic, faPlay, faVolumeUp, faVolumeMute,
+ INFO: {
+ title: this.$t('sounds'),
+ icon: faMusic
+ },
+ sounds: {},
+ faMusic, faPlay, faVolumeUp, faVolumeMute, faChevronDown, faRedo,
}
},
computed: {
- sfxVolume: {
- get() { return this.$store.state.device.sfxVolume; },
- set(value) { this.$store.commit('device/set', { key: 'sfxVolume', value: parseFloat(value, 10) }); }
- },
-
- sfxNote: {
- get() { return this.$store.state.device.sfxNote; },
- set(value) { this.$store.commit('device/set', { key: 'sfxNote', value }); }
- },
-
- sfxNoteMy: {
- get() { return this.$store.state.device.sfxNoteMy; },
- set(value) { this.$store.commit('device/set', { key: 'sfxNoteMy', value }); }
+ masterVolume: { // TODO: (外部)関数にcomputedを使うのはアレなので直す
+ get() { return device.get('sound_masterVolume'); },
+ set(value) { device.set('sound_masterVolume', value); }
},
+ volumeIcon() {
+ return this.masterVolume === 0 ? faVolumeMute : faVolumeUp;
+ }
+ },
- sfxNotification: {
- get() { return this.$store.state.device.sfxNotification; },
- set(value) { this.$store.commit('device/set', { key: 'sfxNotification', value }); }
- },
+ created() {
+ this.sounds.note = device.get('sound_note');
+ this.sounds.noteMy = device.get('sound_noteMy');
+ this.sounds.notification = device.get('sound_notification');
+ this.sounds.chat = device.get('sound_chat');
+ this.sounds.chatBg = device.get('sound_chatBg');
+ this.sounds.antenna = device.get('sound_antenna');
+ this.sounds.channel = device.get('sound_channel');
+ this.sounds.reversiPutBlack = device.get('sound_reversiPutBlack');
+ this.sounds.reversiPutWhite = device.get('sound_reversiPutWhite');
+ },
- sfxChat: {
- get() { return this.$store.state.device.sfxChat; },
- set(value) { this.$store.commit('device/set', { key: 'sfxChat', value }); }
- },
+ mounted() {
+ this.$emit('info', this.INFO);
+ },
- sfxChatBg: {
- get() { return this.$store.state.device.sfxChatBg; },
- set(value) { this.$store.commit('device/set', { key: 'sfxChatBg', value }); }
- },
+ methods: {
+ async edit(type) {
+ const { canceled, result } = await os.form(this.$t('_sfx.' + type), {
+ type: {
+ type: 'enum',
+ enum: soundsTypes.map(x => ({
+ value: x,
+ label: x == null ? this.$t('none') : x,
+ })),
+ label: this.$t('sound'),
+ default: this.sounds[type].type,
+ },
+ volume: {
+ type: 'range',
+ mim: 0,
+ max: 1,
+ step: 0.05,
+ label: this.$t('volume'),
+ default: this.sounds[type].volume
+ },
+ listen: {
+ type: 'button',
+ content: this.$t('listen'),
+ action: (_, values) => {
+ playFile(values.type, values.volume);
+ }
+ }
+ });
+ if (canceled) return;
- sfxAntenna: {
- get() { return this.$store.state.device.sfxAntenna; },
- set(value) { this.$store.commit('device/set', { key: 'sfxAntenna', value }); }
- },
+ const v = {
+ type: result.type,
+ volume: result.volume,
+ };
- sfxChannel: {
- get() { return this.$store.state.device.sfxChannel; },
- set(value) { this.$store.commit('device/set', { key: 'sfxChannel', value }); }
+ device.set('sound_' + type, v);
+ this.sounds[type] = v;
},
- volumeIcon: {
- get() {
- return this.sfxVolume === 0 ? faVolumeMute : faVolumeUp;
+ reset() {
+ for (const sound of Object.keys(this.sounds)) {
+ const v = defaultDeviceSettings['sound_' + sound];
+ device.set('sound_' + sound, v);
+ this.sounds[sound] = v;
}
}
- },
-
- methods: {
- listen(sound) {
- const audio = new Audio(`/assets/sounds/${sound}.mp3`);
- audio.volume = this.$store.state.device.sfxVolume;
- audio.play();
- },
}
});
</script>
diff --git a/src/client/pages/settings/theme.install.vue b/src/client/pages/settings/theme.install.vue
new file mode 100644
index 0000000000..c3f2565cca
--- /dev/null
+++ b/src/client/pages/settings/theme.install.vue
@@ -0,0 +1,106 @@
+<template>
+<FormBase>
+ <FormGroup>
+ <FormTextarea v-model:value="installThemeCode">
+ <span>{{ $t('_theme.code') }}</span>
+ </FormTextarea>
+ <FormButton @click="() => preview(installThemeCode)" :disabled="installThemeCode == null" inline><Fa :icon="faEye"/> {{ $t('preview') }}</FormButton>
+ </FormGroup>
+
+ <FormButton @click="() => install(installThemeCode)" :disabled="installThemeCode == null" primary inline><Fa :icon="faCheck"/> {{ $t('install') }}</FormButton>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt, faEye } from '@fortawesome/free-solid-svg-icons';
+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/form/base.vue';
+import FormGroup from '@/components/form/group.vue';
+import FormLink from '@/components/form/link.vue';
+import FormButton from '@/components/form/button.vue';
+import { applyTheme, validateTheme } from '@/scripts/theme';
+import * as os from '@/os';
+
+export default defineComponent({
+ components: {
+ FormTextarea,
+ FormSelect,
+ FormRadios,
+ FormBase,
+ FormGroup,
+ FormLink,
+ FormButton,
+ },
+
+ emits: ['info'],
+
+ data() {
+ return {
+ INFO: {
+ title: this.$t('_theme.install'),
+ icon: faDownload
+ },
+ installThemeCode: null,
+ faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt, faEye
+ }
+ },
+
+ mounted() {
+ this.$emit('info', this.INFO);
+ },
+
+ methods: {
+ parseThemeCode(code) {
+ let theme;
+
+ try {
+ theme = JSON5.parse(code);
+ } catch (e) {
+ os.dialog({
+ type: 'error',
+ text: this.$t('_theme.invalid')
+ });
+ return false;
+ }
+ if (!validateTheme(theme)) {
+ os.dialog({
+ type: 'error',
+ text: this.$t('_theme.invalid')
+ });
+ return false;
+ }
+ if (this.$store.state.device.themes.some(t => t.id === theme.id)) {
+ os.dialog({
+ type: 'info',
+ text: this.$t('_theme.alreadyInstalled')
+ });
+ return false;
+ }
+
+ return theme;
+ },
+
+ preview(code) {
+ const theme = this.parseThemeCode(code);
+ if (theme) applyTheme(theme, false);
+ },
+
+ install(code) {
+ const theme = this.parseThemeCode(code);
+ if (!theme) return;
+ const themes = this.$store.state.device.themes.concat(theme);
+ this.$store.commit('device/set', {
+ key: 'themes', value: themes
+ });
+ os.dialog({
+ type: 'success',
+ text: this.$t('_theme.installed', { name: theme.name })
+ });
+ },
+ }
+});
+</script>
diff --git a/src/client/pages/settings/theme.manage.vue b/src/client/pages/settings/theme.manage.vue
new file mode 100644
index 0000000000..a7bd97a4d5
--- /dev/null
+++ b/src/client/pages/settings/theme.manage.vue
@@ -0,0 +1,103 @@
+<template>
+<FormBase>
+ <FormSelect v-model:value="selectedThemeId">
+ <template #label>{{ $t('installedThemes') }}</template>
+ <option v-for="x in installedThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
+ <optgroup :label="$t('builtinThemes')">
+ <option v-for="x in builtinThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
+ </optgroup>
+ </FormSelect>
+ <template v-if="selectedTheme">
+ <FormInput readonly :value="selectedTheme.author">
+ <span>{{ $t('author') }}</span>
+ </FormInput>
+ <FormTextarea readonly tall :value="selectedThemeCode">
+ <span>{{ $t('_theme.code') }}</span>
+ <template #desc><button @click="copyThemeCode()" class="_textButton">{{ $t('copy') }}</button></template>
+ </FormTextarea>
+ <FormButton @click="uninstall()" danger v-if="!builtinThemes.some(t => t.id == selectedTheme.id)"><Fa :icon="faTrashAlt"/> {{ $t('uninstall') }}</FormButton>
+ </template>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt, faEye } from '@fortawesome/free-solid-svg-icons';
+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/form/base.vue';
+import FormGroup from '@/components/form/group.vue';
+import FormInput from '@/components/form/input.vue';
+import FormButton from '@/components/form/button.vue';
+import { Theme, builtinThemes } from '@/scripts/theme';
+import copyToClipboard from '@/scripts/copy-to-clipboard';
+import * as os from '@/os';
+
+export default defineComponent({
+ components: {
+ FormTextarea,
+ FormSelect,
+ FormRadios,
+ FormBase,
+ FormGroup,
+ FormInput,
+ FormButton,
+ },
+
+ emits: ['info'],
+
+ data() {
+ return {
+ INFO: {
+ title: this.$t('_theme.manage'),
+ icon: faFolderOpen
+ },
+ builtinThemes,
+ selectedThemeId: null,
+ faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt, faEye
+ }
+ },
+
+ computed: {
+ themes(): Theme[] {
+ return builtinThemes.concat(this.$store.state.device.themes);
+ },
+
+ installedThemes(): Theme[] {
+ return this.$store.state.device.themes;
+ },
+
+ selectedTheme() {
+ if (this.selectedThemeId == null) return null;
+ return this.themes.find(x => x.id === this.selectedThemeId);
+ },
+
+ selectedThemeCode() {
+ if (this.selectedTheme == null) return null;
+ return JSON5.stringify(this.selectedTheme, null, '\t');
+ },
+ },
+
+ mounted() {
+ this.$emit('info', this.INFO);
+ },
+
+ methods: {
+ copyThemeCode() {
+ copyToClipboard(this.selectedThemeCode);
+ os.success();
+ },
+
+ uninstall() {
+ const theme = this.selectedTheme;
+ const themes = this.$store.state.device.themes.filter(t => t.id != theme.id);
+ this.$store.commit('device/set', {
+ key: 'themes', value: themes
+ });
+ os.success();
+ },
+ }
+});
+</script>
diff --git a/src/client/pages/settings/theme.vue b/src/client/pages/settings/theme.vue
index c023d56dea..50dcf4952c 100644
--- a/src/client/pages/settings/theme.vue
+++ b/src/client/pages/settings/theme.vue
@@ -1,7 +1,26 @@
<template>
-<div class="">
- <div class="rfqxtzch _card _vMargin">
- <div class="_content">
+<FormBase>
+ <FormSelect v-model:value="lightTheme" v-if="!darkMode">
+ <template #label>{{ $t('themeForLightMode') }}</template>
+ <optgroup :label="$t('lightThemes')">
+ <option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
+ </optgroup>
+ <optgroup :label="$t('darkThemes')">
+ <option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
+ </optgroup>
+ </FormSelect>
+ <FormSelect v-model:value="darkTheme" v-else>
+ <template #label>{{ $t('themeForDarkMode') }}</template>
+ <optgroup :label="$t('darkThemes')">
+ <option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
+ </optgroup>
+ <optgroup :label="$t('lightThemes')">
+ <option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
+ </optgroup>
+ </FormSelect>
+
+ <FormGroup>
+ <div class="rfqxtzch _formItem _formPanel">
<div class="darkMode" :class="{ disabled: syncDeviceDarkMode }">
<div class="toggleWrapper">
<input type="checkbox" class="dn" id="dn" v-model="darkMode" :disabled="syncDeviceDarkMode"/>
@@ -23,85 +42,47 @@
</div>
</div>
</div>
- <div class="_content">
- <MkSwitch v-model:value="syncDeviceDarkMode">{{ $t('syncDeviceDarkMode') }}</MkSwitch>
- </div>
- </div>
- <div class="_card _vMargin">
- <div class="_content">
- <MkSelect v-model:value="lightTheme">
- <template #label>{{ $t('themeForLightMode') }}</template>
- <optgroup :label="$t('lightThemes')">
- <option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
- </optgroup>
- <optgroup :label="$t('darkThemes')">
- <option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
- </optgroup>
- </MkSelect>
- <MkSelect v-model:value="darkTheme">
- <template #label>{{ $t('themeForDarkMode') }}</template>
- <optgroup :label="$t('darkThemes')">
- <option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
- </optgroup>
- <optgroup :label="$t('lightThemes')">
- <option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
- </optgroup>
- </MkSelect>
- <a href="https://assets.msky.cafe/theme/list" rel="noopener" target="_blank" class="_link">{{ $t('_theme.explore') }}</a>・<MkA to="/theme-editor" class="_link">{{ $t('_theme.make') }}</MkA>
- </div>
- <div class="_content">
- <MkButton primary v-if="wallpaper == null" @click="setWallpaper">{{ $t('setWallpaper') }}</MkButton>
- <MkButton primary v-else @click="wallpaper = null">{{ $t('removeWallpaper') }}</MkButton>
- </div>
- </div>
- <div class="_card _vMargin">
- <div class="_title"><Fa :icon="faDownload"/> {{ $t('_theme.install') }}</div>
- <div class="_content">
- <MkTextarea v-model:value="installThemeCode">
- <span>{{ $t('_theme.code') }}</span>
- </MkTextarea>
- <MkButton @click="() => install(installThemeCode)" :disabled="installThemeCode == null" primary inline><Fa :icon="faCheck"/> {{ $t('install') }}</MkButton>
- <MkButton @click="() => preview(installThemeCode)" :disabled="installThemeCode == null" inline><Fa :icon="faEye"/> {{ $t('preview') }}</MkButton>
- </div>
- </div>
- <div class="_card _vMargin">
- <div class="_title"><Fa :icon="faFolderOpen"/> {{ $t('_theme.manage') }}</div>
- <div class="_content">
- <MkSelect v-model:value="selectedThemeId">
- <option v-for="x in installedThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
- </MkSelect>
- <template v-if="selectedTheme">
- <MkTextarea readonly tall :value="selectedThemeCode">
- <span>{{ $t('_theme.code') }}</span>
- <template #desc><button @click="copyThemeCode()" class="_textButton">{{ $t('copy') }}</button></template>
- </MkTextarea>
- <MkButton @click="uninstall()" v-if="!builtinThemes.some(t => t.id == selectedTheme.id)"><Fa :icon="faTrashAlt"/> {{ $t('uninstall') }}</MkButton>
- </template>
- </div>
- </div>
-</div>
+ <FormSwitch v-model:value="syncDeviceDarkMode">{{ $t('syncDeviceDarkMode') }}</FormSwitch>
+ </FormGroup>
+
+ <FormButton primary v-if="wallpaper == null" @click="setWallpaper">{{ $t('setWallpaper') }}</FormButton>
+ <FormButton primary v-else @click="wallpaper = null">{{ $t('removeWallpaper') }}</FormButton>
+
+ <FormGroup>
+ <FormLink to="https://assets.msky.cafe/theme/list" external>{{ $t('_theme.explore') }}</FormLink>
+ <FormLink to="/theme-editor">{{ $t('_theme.make') }}</FormLink>
+ </FormGroup>
+
+ <FormLink to="/settings/theme/install"><template #icon><Fa :icon="faDownload"/></template>{{ $t('_theme.install') }}</FormLink>
+
+ <FormLink to="/settings/theme/manage"><template #icon><Fa :icon="faFolderOpen"/></template>{{ $t('_theme.manage') }}</FormLink>
+</FormBase>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt, faEye } from '@fortawesome/free-solid-svg-icons';
-import * as JSON5 from 'json5';
-import MkButton from '@/components/ui/button.vue';
-import MkSelect from '@/components/ui/select.vue';
-import MkSwitch from '@/components/ui/switch.vue';
-import MkTextarea from '@/components/ui/textarea.vue';
-import { Theme, builtinThemes, applyTheme, validateTheme } from '@/scripts/theme';
+import FormSwitch from '@/components/form/switch.vue';
+import FormSelect from '@/components/form/select.vue';
+import FormRadios from '@/components/form/radios.vue';
+import FormBase from '@/components/form/base.vue';
+import FormGroup from '@/components/form/group.vue';
+import FormLink from '@/components/form/link.vue';
+import FormButton from '@/components/form/button.vue';
+import { Theme, builtinThemes, applyTheme } from '@/scripts/theme';
import { selectFile } from '@/scripts/select-file';
import { isDeviceDarkmode } from '@/scripts/is-device-darkmode';
-import copyToClipboard from '@/scripts/copy-to-clipboard';
import * as os from '@/os';
export default defineComponent({
components: {
- MkButton,
- MkSelect,
- MkSwitch,
- MkTextarea,
+ FormSwitch,
+ FormSelect,
+ FormRadios,
+ FormBase,
+ FormGroup,
+ FormLink,
+ FormButton,
},
emits: ['info'],
@@ -113,8 +94,6 @@ export default defineComponent({
icon: faPalette
},
builtinThemes,
- installThemeCode: null,
- selectedThemeId: null,
wallpaper: localStorage.getItem('wallpaper'),
faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt, faEye
}
@@ -156,16 +135,6 @@ export default defineComponent({
get() { return this.$store.state.device.syncDeviceDarkMode; },
set(value) { this.$store.commit('device/set', { key: 'syncDeviceDarkMode', value }); }
},
-
- selectedTheme() {
- if (this.selectedThemeId == null) return null;
- return this.themes.find(x => x.id === this.selectedThemeId);
- },
-
- selectedThemeCode() {
- if (this.selectedTheme == null) return null;
- return JSON5.stringify(this.selectedTheme, null, '\t');
- },
},
watch: {
@@ -207,292 +176,230 @@ export default defineComponent({
this.wallpaper = file.url;
});
},
-
- copyThemeCode() {
- copyToClipboard(this.selectedThemeCode);
- os.success();
- },
-
- parseThemeCode(code) {
- let theme;
-
- try {
- theme = JSON5.parse(code);
- } catch (e) {
- os.dialog({
- type: 'error',
- text: this.$t('_theme.invalid')
- });
- return false;
- }
- if (!validateTheme(theme)) {
- os.dialog({
- type: 'error',
- text: this.$t('_theme.invalid')
- });
- return false;
- }
- if (this.$store.state.device.themes.some(t => t.id === theme.id)) {
- os.dialog({
- type: 'info',
- text: this.$t('_theme.alreadyInstalled')
- });
- return false;
- }
-
- return theme;
- },
-
- preview(code) {
- const theme = this.parseThemeCode(code);
- if (theme) applyTheme(theme, false);
- },
-
- install(code) {
- const theme = this.parseThemeCode(code);
- if (!theme) return;
- const themes = this.$store.state.device.themes.concat(theme);
- this.$store.commit('device/set', {
- key: 'themes', value: themes
- });
- os.dialog({
- type: 'success',
- text: this.$t('_theme.installed', { name: theme.name })
- });
- },
-
- uninstall() {
- const theme = this.selectedTheme;
- const themes = this.$store.state.device.themes.filter(t => t.id != theme.id);
- this.$store.commit('device/set', {
- key: 'themes', value: themes
- });
- os.success();
- },
}
});
</script>
<style lang="scss" scoped>
.rfqxtzch {
- > ._content {
- > .darkMode {
- position: relative;
- padding: 32px 0;
-
- &.disabled {
- opacity: 0.7;
+ padding: 16px;
- &, * {
- cursor: not-allowed !important;
- }
- }
+ > .darkMode {
+ position: relative;
+ padding: 32px 0;
- .toggleWrapper {
- position: absolute;
- top: 50%;
- left: 50%;
- overflow: hidden;
- padding: 0 100px;
- transform: translate3d(-50%, -50%, 0);
+ &.disabled {
+ opacity: 0.7;
- input {
- position: absolute;
- left: -99em;
- }
+ &, * {
+ cursor: not-allowed !important;
}
+ }
- .toggle {
- cursor: pointer;
- display: inline-block;
- position: relative;
- width: 90px;
- height: 50px;
- background-color: #83D8FF;
- border-radius: 90px - 6;
- transition: background-color 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
+ .toggleWrapper {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ overflow: hidden;
+ padding: 0 100px;
+ transform: translate3d(-50%, -50%, 0);
- > .before, > .after {
- position: absolute;
- top: 15px;
- font-size: 18px;
- transition: color 1s ease;
- }
+ input {
+ position: absolute;
+ left: -99em;
+ }
+ }
- > .before {
- left: -70px;
- color: var(--accent);
- }
+ .toggle {
+ cursor: pointer;
+ display: inline-block;
+ position: relative;
+ width: 90px;
+ height: 50px;
+ background-color: #83D8FF;
+ border-radius: 90px - 6;
+ transition: background-color 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
- > .after {
- right: -68px;
- color: var(--fg);
- }
+ > .before, > .after {
+ position: absolute;
+ top: 15px;
+ font-size: 18px;
+ transition: color 1s ease;
}
- .toggle__handler {
- display: inline-block;
- position: relative;
- z-index: 1;
- top: 3px;
- left: 3px;
- width: 50px - 6;
- height: 50px - 6;
- background-color: #FFCF96;
- border-radius: 50px;
- box-shadow: 0 2px 6px rgba(0,0,0,.3);
- transition: all 400ms cubic-bezier(0.68, -0.55, 0.265, 1.55) !important;
- transform: rotate(-45deg);
+ > .before {
+ left: -70px;
+ color: var(--accent);
+ }
- .crater {
- position: absolute;
- background-color: #E8CDA5;
- opacity: 0;
- transition: opacity 200ms ease-in-out !important;
- border-radius: 100%;
- }
+ > .after {
+ right: -68px;
+ color: var(--fg);
+ }
+ }
- .crater--1 {
- top: 18px;
- left: 10px;
- width: 4px;
- height: 4px;
- }
+ .toggle__handler {
+ display: inline-block;
+ position: relative;
+ z-index: 1;
+ top: 3px;
+ left: 3px;
+ width: 50px - 6;
+ height: 50px - 6;
+ background-color: #FFCF96;
+ border-radius: 50px;
+ box-shadow: 0 2px 6px rgba(0,0,0,.3);
+ transition: all 400ms cubic-bezier(0.68, -0.55, 0.265, 1.55) !important;
+ transform: rotate(-45deg);
- .crater--2 {
- top: 28px;
- left: 22px;
- width: 6px;
- height: 6px;
- }
+ .crater {
+ position: absolute;
+ background-color: #E8CDA5;
+ opacity: 0;
+ transition: opacity 200ms ease-in-out !important;
+ border-radius: 100%;
+ }
- .crater--3 {
- top: 10px;
- left: 25px;
- width: 8px;
- height: 8px;
- }
+ .crater--1 {
+ top: 18px;
+ left: 10px;
+ width: 4px;
+ height: 4px;
}
- .star {
- position: absolute;
- background-color: #ffffff;
- transition: all 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
- border-radius: 50%;
+ .crater--2 {
+ top: 28px;
+ left: 22px;
+ width: 6px;
+ height: 6px;
}
- .star--1 {
+ .crater--3 {
top: 10px;
- left: 35px;
- z-index: 0;
- width: 30px;
- height: 3px;
+ left: 25px;
+ width: 8px;
+ height: 8px;
}
+ }
- .star--2 {
- top: 18px;
- left: 28px;
- z-index: 1;
- width: 30px;
- height: 3px;
- }
+ .star {
+ position: absolute;
+ background-color: #ffffff;
+ transition: all 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
+ border-radius: 50%;
+ }
- .star--3 {
- top: 27px;
- left: 40px;
- z-index: 0;
- width: 30px;
- height: 3px;
- }
+ .star--1 {
+ top: 10px;
+ left: 35px;
+ z-index: 0;
+ width: 30px;
+ height: 3px;
+ }
- .star--4,
- .star--5,
- .star--6 {
- opacity: 0;
- transition: all 300ms 0 cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
- }
+ .star--2 {
+ top: 18px;
+ left: 28px;
+ z-index: 1;
+ width: 30px;
+ height: 3px;
+ }
- .star--4 {
- top: 16px;
- left: 11px;
- z-index: 0;
- width: 2px;
- height: 2px;
- transform: translate3d(3px,0,0);
- }
+ .star--3 {
+ top: 27px;
+ left: 40px;
+ z-index: 0;
+ width: 30px;
+ height: 3px;
+ }
- .star--5 {
- top: 32px;
- left: 17px;
- z-index: 0;
- width: 3px;
- height: 3px;
- transform: translate3d(3px,0,0);
- }
+ .star--4,
+ .star--5,
+ .star--6 {
+ opacity: 0;
+ transition: all 300ms 0 cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
+ }
- .star--6 {
- top: 36px;
- left: 28px;
- z-index: 0;
- width: 2px;
- height: 2px;
- transform: translate3d(3px,0,0);
- }
+ .star--4 {
+ top: 16px;
+ left: 11px;
+ z-index: 0;
+ width: 2px;
+ height: 2px;
+ transform: translate3d(3px,0,0);
+ }
- input:checked {
- + .toggle {
- background-color: #749DD6;
+ .star--5 {
+ top: 32px;
+ left: 17px;
+ z-index: 0;
+ width: 3px;
+ height: 3px;
+ transform: translate3d(3px,0,0);
+ }
- > .before {
- color: var(--fg);
- }
+ .star--6 {
+ top: 36px;
+ left: 28px;
+ z-index: 0;
+ width: 2px;
+ height: 2px;
+ transform: translate3d(3px,0,0);
+ }
- > .after {
- color: var(--accent);
- }
+ input:checked {
+ + .toggle {
+ background-color: #749DD6;
- .toggle__handler {
- background-color: #FFE5B5;
- transform: translate3d(40px, 0, 0) rotate(0);
+ > .before {
+ color: var(--fg);
+ }
- .crater { opacity: 1; }
- }
+ > .after {
+ color: var(--accent);
+ }
+
+ .toggle__handler {
+ background-color: #FFE5B5;
+ transform: translate3d(40px, 0, 0) rotate(0);
+
+ .crater { opacity: 1; }
+ }
- .star--1 {
- width: 2px;
- height: 2px;
- }
+ .star--1 {
+ width: 2px;
+ height: 2px;
+ }
- .star--2 {
- width: 4px;
- height: 4px;
- transform: translate3d(-5px, 0, 0);
- }
+ .star--2 {
+ width: 4px;
+ height: 4px;
+ transform: translate3d(-5px, 0, 0);
+ }
- .star--3 {
- width: 2px;
- height: 2px;
- transform: translate3d(-7px, 0, 0);
- }
+ .star--3 {
+ width: 2px;
+ height: 2px;
+ transform: translate3d(-7px, 0, 0);
+ }
- .star--4,
- .star--5,
- .star--6 {
- opacity: 1;
- transform: translate3d(0,0,0);
- }
+ .star--4,
+ .star--5,
+ .star--6 {
+ opacity: 1;
+ transform: translate3d(0,0,0);
+ }
- .star--4 {
- transition: all 300ms 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
- }
+ .star--4 {
+ transition: all 300ms 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
+ }
- .star--5 {
- transition: all 300ms 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
- }
+ .star--5 {
+ transition: all 300ms 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
+ }
- .star--6 {
- transition: all 300ms 400ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
- }
+ .star--6 {
+ transition: all 300ms 400ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
}
}
}
diff --git a/src/client/pages/settings/word-mute.vue b/src/client/pages/settings/word-mute.vue
index 444b2e598c..3148c635bc 100644
--- a/src/client/pages/settings/word-mute.vue
+++ b/src/client/pages/settings/word-mute.vue
@@ -1,47 +1,53 @@
<template>
-<div class="_section">
- <div class="_card">
- <MkTab v-model:value="tab">
- <option value="soft">{{ $t('_wordMute.soft') }}</option>
- <option value="hard">{{ $t('_wordMute.hard') }}</option>
- </MkTab>
- <div class="_content">
+<div>
+ <MkTab v-model:value="tab">
+ <option value="soft">{{ $t('_wordMute.soft') }}</option>
+ <option value="hard">{{ $t('_wordMute.hard') }}</option>
+ </MkTab>
+ <FormBase>
+ <div class="_formItem">
<div v-show="tab === 'soft'">
<MkInfo>{{ $t('_wordMute.softDescription') }}</MkInfo>
- <MkTextarea v-model:value="softMutedWords">
+ <FormTextarea v-model:value="softMutedWords">
<span>{{ $t('_wordMute.muteWords') }}</span>
<template #desc>{{ $t('_wordMute.muteWordsDescription') }}<br>{{ $t('_wordMute.muteWordsDescription2') }}</template>
- </MkTextarea>
+ </FormTextarea>
</div>
<div v-show="tab === 'hard'">
<MkInfo>{{ $t('_wordMute.hardDescription') }}</MkInfo>
- <MkTextarea v-model:value="hardMutedWords" style="margin-bottom: 16px;">
+ <FormTextarea v-model:value="hardMutedWords">
<span>{{ $t('_wordMute.muteWords') }}</span>
<template #desc>{{ $t('_wordMute.muteWordsDescription') }}<br>{{ $t('_wordMute.muteWordsDescription2') }}</template>
- </MkTextarea>
- <div v-if="hardWordMutedNotesCount != null" class="_caption">{{ $t('_wordMute.mutedNotes') }}: {{ hardWordMutedNotesCount | number }}</div>
+ </FormTextarea>
+ <FormKeyValueView v-if="hardWordMutedNotesCount != null">
+ <template #key>{{ $t('_wordMute.mutedNotes') }}</template>
+ <template #value>{{ number(hardWordMutedNotesCount) }}</template>
+ </FormKeyValueView>
</div>
</div>
- <div class="_footer">
- <MkButton @click="save()" primary inline :disabled="!changed"><Fa :icon="faSave"/> {{ $t('save') }}</MkButton>
- </div>
- </div>
+ <FormButton @click="save()" primary inline :disabled="!changed"><Fa :icon="faSave"/> {{ $t('save') }}</FormButton>
+ </FormBase>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faCommentSlash, faSave } from '@fortawesome/free-solid-svg-icons';
-import MkButton from '@/components/ui/button.vue';
-import MkTextarea from '@/components/ui/textarea.vue';
+import FormTextarea from '@/components/form/textarea.vue';
+import FormBase from '@/components/form/base.vue';
+import FormKeyValueView from '@/components/form/key-value-view.vue';
+import FormButton from '@/components/form/button.vue';
import MkTab from '@/components/tab.vue';
import MkInfo from '@/components/ui/info.vue';
import * as os from '@/os';
+import number from '@/filters/number';
export default defineComponent({
components: {
- MkButton,
- MkTextarea,
+ FormBase,
+ FormButton,
+ FormTextarea,
+ FormKeyValueView,
MkTab,
MkInfo,
},
@@ -97,6 +103,8 @@ export default defineComponent({
});
this.changed = false;
},
+
+ number
}
});
</script>
diff --git a/src/client/pages/user/follow-list.vue b/src/client/pages/user/follow-list.vue
index 6761210ff6..90a67e9a8e 100644
--- a/src/client/pages/user/follow-list.vue
+++ b/src/client/pages/user/follow-list.vue
@@ -1,5 +1,5 @@
<template>
-<div class="_section">
+<div>
<MkPagination :pagination="pagination" #default="{items}" class="mk-following-or-followers _content" ref="list">
<div class="users">
<MkUserInfo class="user" v-for="user in items.map(x => type === 'following' ? x.followee : x.follower)" :user="user" :key="user.id"/>
diff --git a/src/client/pages/user/index.activity.vue b/src/client/pages/user/index.activity.vue
index 30c02ec54a..1f059146e3 100644
--- a/src/client/pages/user/index.activity.vue
+++ b/src/client/pages/user/index.activity.vue
@@ -1,15 +1,24 @@
<template>
-<div>
- <div ref="chart"></div>
-</div>
+<MkContainer>
+ <template #header><Fa :icon="faChartBar" style="margin-right: 0.5em;"/>{{ $t('activity') }}</template>
+
+ <div style="padding: 8px;">
+ <div ref="chart"></div>
+ </div>
+</MkContainer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import ApexCharts from 'apexcharts';
+import { faChartBar } from '@fortawesome/free-solid-svg-icons';
import * as os from '@/os';
+import MkContainer from '@/components/ui/container.vue';
export default defineComponent({
+ components: {
+ MkContainer,
+ },
props: {
user: {
type: Object,
@@ -25,7 +34,8 @@ export default defineComponent({
return {
fetching: true,
data: [],
- peak: null
+ peak: null,
+ faChartBar,
};
},
mounted() {
diff --git a/src/client/pages/user/index.photos.vue b/src/client/pages/user/index.photos.vue
index aabcbebe8a..7d498cfb30 100644
--- a/src/client/pages/user/index.photos.vue
+++ b/src/client/pages/user/index.photos.vue
@@ -1,29 +1,43 @@
<template>
-<div class="ujigsodd">
- <MkLoading v-if="fetching"/>
- <div class="stream" v-if="!fetching && images.length > 0">
- <MkA v-for="image in images"
- class="img"
- :style="`background-image: url(${thumbnail(image.file)})`"
- :to="notePage(image.note)"
- ></MkA>
+<MkContainer>
+ <template #header><Fa :icon="faImage" style="margin-right: 0.5em;"/>{{ $t('images') }}</template>
+ <div class="ujigsodd">
+ <MkLoading v-if="fetching"/>
+ <div class="stream" v-if="!fetching && images.length > 0">
+ <MkA v-for="image in images"
+ class="img"
+ :style="`background-image: url(${thumbnail(image.file)})`"
+ :to="notePage(image.note)"
+ ></MkA>
+ </div>
+ <p class="empty" v-if="!fetching && images.length == 0">{{ $t('nothing') }}</p>
</div>
- <p class="empty" v-if="!fetching && images.length == 0">{{ $t('nothing') }}</p>
-</div>
+</MkContainer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
+import { faImage } from '@fortawesome/free-solid-svg-icons';
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
import notePage from '../../filters/note';
import * as os from '@/os';
+import MkContainer from '@/components/ui/container.vue';
export default defineComponent({
- props: ['user'],
+ components: {
+ MkContainer,
+ },
+ props: {
+ user: {
+ type: Object,
+ required: true
+ },
+ },
data() {
return {
fetching: true,
- images: []
+ images: [],
+ faImage
};
},
mounted() {
@@ -37,7 +51,7 @@ export default defineComponent({
os.api('users/notes', {
userId: this.user.id,
fileType: image,
- excludeNsfw: !this.$store.state.device.alwaysShowNsfw,
+ excludeNsfw: this.$store.state.device.nsfw !== 'ignore',
limit: 9,
}).then(notes => {
for (const note of notes) {
@@ -66,6 +80,8 @@ export default defineComponent({
<style lang="scss" scoped>
.ujigsodd {
+ padding: 8px;
+
> .stream {
display: flex;
justify-content: center;
diff --git a/src/client/pages/user/index.vue b/src/client/pages/user/index.vue
index 015d83f755..ceafa7ba97 100644
--- a/src/client/pages/user/index.vue
+++ b/src/client/pages/user/index.vue
@@ -1,115 +1,113 @@
<template>
-<div class="mk-user-page" v-if="user" v-size="{ max: [500] }">
- <!-- TODO -->
- <!-- <div class="punished" v-if="user.isSuspended"><Fa :icon="faExclamationTriangle" style="margin-right: 8px;"/> {{ $t('userSuspended') }}</div> -->
- <!-- <div class="punished" v-if="user.isSilenced"><Fa :icon="faExclamationTriangle" style="margin-right: 8px;"/> {{ $t('userSilenced') }}</div> -->
+<div>
+ <div class="mk-user-page" v-if="user" v-size="{ max: [500] }" :class="{ _section: narrow === false }">
+ <!-- TODO -->
+ <!-- <div class="punished" v-if="user.isSuspended"><Fa :icon="faExclamationTriangle" style="margin-right: 8px;"/> {{ $t('userSuspended') }}</div> -->
+ <!-- <div class="punished" v-if="user.isSilenced"><Fa :icon="faExclamationTriangle" style="margin-right: 8px;"/> {{ $t('userSilenced') }}</div> -->
- <div class="profile _section _fitBottom">
- <MkRemoteCaution v-if="user.host != null" :href="user.url" class="_content _vMargin"/>
+ <div class="main">
+ <div class="profile _vMargin" :class="{ _section: narrow === true }">
+ <MkRemoteCaution v-if="user.host != null" :href="user.url" class="_content _vMargin"/>
- <div class="_content _vMargin" :key="user.id">
- <div class="banner-container" :style="style">
- <div class="banner" ref="banner" :style="style"></div>
- <div class="fade"></div>
- <div class="title">
- <MkUserName class="name" :user="user" :nowrap="true"/>
- <div class="bottom">
- <span class="username"><MkAcct :user="user" :detail="true" /></span>
- <span v-if="user.isAdmin" :title="$t('isAdmin')" style="color: var(--badge);"><Fa :icon="faBookmark"/></span>
- <span v-if="!user.isAdmin && user.isModerator" :title="$t('isModerator')" style="color: var(--badge);"><Fa :icon="farBookmark"/></span>
- <span v-if="user.isLocked" :title="$t('isLocked')"><Fa :icon="faLock"/></span>
- <span v-if="user.isBot" :title="$t('isBot')"><Fa :icon="faRobot"/></span>
+ <div class="_content _panel _vMargin" :key="user.id">
+ <div class="banner-container" :style="style">
+ <div class="banner" ref="banner" :style="style"></div>
+ <div class="fade"></div>
+ <div class="title">
+ <MkUserName class="name" :user="user" :nowrap="true"/>
+ <div class="bottom">
+ <span class="username"><MkAcct :user="user" :detail="true" /></span>
+ <span v-if="user.isAdmin" :title="$t('isAdmin')" style="color: var(--badge);"><Fa :icon="faBookmark"/></span>
+ <span v-if="!user.isAdmin && user.isModerator" :title="$t('isModerator')" style="color: var(--badge);"><Fa :icon="farBookmark"/></span>
+ <span v-if="user.isLocked" :title="$t('isLocked')"><Fa :icon="faLock"/></span>
+ <span v-if="user.isBot" :title="$t('isBot')"><Fa :icon="faRobot"/></span>
+ </div>
+ </div>
+ <span class="followed" v-if="$store.getters.isSignedIn && $store.state.i.id != user.id && user.isFollowed">{{ $t('followsYou') }}</span>
+ <div class="actions" v-if="$store.getters.isSignedIn">
+ <button @click="menu" class="menu _button"><Fa :icon="faEllipsisH"/></button>
+ <MkFollowButton v-if="$store.state.i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
+ </div>
+ </div>
+ <MkAvatar class="avatar" :user="user" :disable-preview="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="$t('isAdmin')" style="color: var(--badge);"><Fa :icon="faBookmark"/></span>
+ <span v-if="!user.isAdmin && user.isModerator" :title="$t('isModerator')" style="color: var(--badge);"><Fa :icon="farBookmark"/></span>
+ <span v-if="user.isLocked" :title="$t('isLocked')"><Fa :icon="faLock"/></span>
+ <span v-if="user.isBot" :title="$t('isBot')"><Fa :icon="faRobot"/></span>
+ </div>
+ </div>
+ <div class="description">
+ <Mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/>
+ <p v-else class="empty">{{ $t('noAccountDescription') }}</p>
+ </div>
+ <div class="fields system">
+ <dl class="field" v-if="user.location">
+ <dt class="name"><Fa :icon="faMapMarker" fixed-width/> {{ $t('location') }}</dt>
+ <dd class="value">{{ user.location }}</dd>
+ </dl>
+ <dl class="field" v-if="user.birthday">
+ <dt class="name"><Fa :icon="faBirthdayCake" fixed-width/> {{ $t('birthday') }}</dt>
+ <dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ $t('yearsOld', { age }) }})</dd>
+ </dl>
+ <dl class="field">
+ <dt class="name"><Fa :icon="faCalendarAlt" fixed-width/> {{ $t('registeredDate') }}</dt>
+ <dd class="value">{{ new Date(user.createdAt).toLocaleString() }} (<MkTime :time="user.createdAt"/>)</dd>
+ </dl>
+ </div>
+ <div class="fields" v-if="user.fields.length > 0">
+ <dl class="field" v-for="(field, i) in user.fields" :key="i">
+ <dt class="name">
+ <Mfm :text="field.name" :plain="true" :custom-emojis="user.emojis" :colored="false"/>
+ </dt>
+ <dd class="value">
+ <Mfm :text="field.value" :author="user" :i="$store.state.i" :custom-emojis="user.emojis" :colored="false"/>
+ </dd>
+ </dl>
+ </div>
+ <div class="status">
+ <MkA :to="userPage(user)" :class="{ active: page === 'index' }">
+ <b>{{ number(user.notesCount) }}</b>
+ <span>{{ $t('notes') }}</span>
+ </MkA>
+ <MkA :to="userPage(user, 'following')" :class="{ active: page === 'following' }">
+ <b>{{ number(user.followingCount) }}</b>
+ <span>{{ $t('following') }}</span>
+ </MkA>
+ <MkA :to="userPage(user, 'followers')" :class="{ active: page === 'followers' }">
+ <b>{{ number(user.followersCount) }}</b>
+ <span>{{ $t('followers') }}</span>
+ </MkA>
</div>
</div>
- <span class="followed" v-if="$store.getters.isSignedIn && $store.state.i.id != user.id && user.isFollowed">{{ $t('followsYou') }}</span>
- <div class="actions" v-if="$store.getters.isSignedIn">
- <button @click="menu" class="menu _button"><Fa :icon="faEllipsisH"/></button>
- <MkFollowButton v-if="$store.state.i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
- </div>
- </div>
- <MkAvatar class="avatar" :user="user" :disable-preview="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="$t('isAdmin')" style="color: var(--badge);"><Fa :icon="faBookmark"/></span>
- <span v-if="!user.isAdmin && user.isModerator" :title="$t('isModerator')" style="color: var(--badge);"><Fa :icon="farBookmark"/></span>
- <span v-if="user.isLocked" :title="$t('isLocked')"><Fa :icon="faLock"/></span>
- <span v-if="user.isBot" :title="$t('isBot')"><Fa :icon="faRobot"/></span>
- </div>
- </div>
- <div class="description">
- <Mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/>
- <p v-else class="empty">{{ $t('noAccountDescription') }}</p>
- </div>
- <div class="fields system">
- <dl class="field" v-if="user.location">
- <dt class="name"><Fa :icon="faMapMarker" fixed-width/> {{ $t('location') }}</dt>
- <dd class="value">{{ user.location }}</dd>
- </dl>
- <dl class="field" v-if="user.birthday">
- <dt class="name"><Fa :icon="faBirthdayCake" fixed-width/> {{ $t('birthday') }}</dt>
- <dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ $t('yearsOld', { age }) }})</dd>
- </dl>
- <dl class="field">
- <dt class="name"><Fa :icon="faCalendarAlt" fixed-width/> {{ $t('registeredDate') }}</dt>
- <dd class="value">{{ new Date(user.createdAt).toLocaleString() }} (<MkTime :time="user.createdAt"/>)</dd>
- </dl>
- </div>
- <div class="fields" v-if="user.fields.length > 0">
- <dl class="field" v-for="(field, i) in user.fields" :key="i">
- <dt class="name">
- <Mfm :text="field.name" :plain="true" :custom-emojis="user.emojis" :colored="false"/>
- </dt>
- <dd class="value">
- <Mfm :text="field.value" :author="user" :i="$store.state.i" :custom-emojis="user.emojis" :colored="false"/>
- </dd>
- </dl>
- </div>
- <div class="status">
- <MkA :to="userPage(user)" :class="{ active: page === 'index' }">
- <b>{{ number(user.notesCount) }}</b>
- <span>{{ $t('notes') }}</span>
- </MkA>
- <MkA :to="userPage(user, 'following')" :class="{ active: page === 'following' }">
- <b>{{ number(user.followingCount) }}</b>
- <span>{{ $t('following') }}</span>
- </MkA>
- <MkA :to="userPage(user, 'followers')" :class="{ active: page === 'followers' }">
- <b>{{ number(user.followersCount) }}</b>
- <span>{{ $t('followers') }}</span>
- </MkA>
</div>
- </div>
- </div>
- <template v-if="page === 'index'">
- <div class="_section">
- <div class="_content _vMargin" v-if="user.pinnedNotes.length > 0">
- <XNote v-for="note in user.pinnedNotes" class="note _vMargin" :note="note" @update:note="pinnedNoteUpdated(note, $event)" :key="note.id" :detail="true" :pinned="true"/>
- </div>
- <MkFolder :body-togglable="true" class="_content _vMargin" persist-key="user-images">
- <template #header><Fa :icon="faImage" style="margin-right: 0.5em;"/>{{ $t('images') }}</template>
- <div>
- <XPhotos :user="user" :key="user.id"/>
+ <template v-if="page === 'index'">
+ <div v-if="user.pinnedNotes.length > 0" :class="{ _section: narrow === true, _vMargin: narrow === false }">
+ <XNote v-for="note in user.pinnedNotes" class="note _content _vMargin" :note="note" @update:note="pinnedNoteUpdated(note, $event)" :key="note.id" :detail="true" :pinned="true"/>
</div>
- </MkFolder>
- <MkFolder :body-togglable="true" class="_content _vMargin" persist-key="user-activity">
- <template #header><Fa :icon="faChartBar" style="margin-right: 0.5em;"/>{{ $t('activity') }}</template>
- <div>
- <XActivity :user="user" :key="user.id"/>
+ <div v-if="narrow === true" class="_section">
+ <XPhotos class="_content _vMargin" :user="user" :key="user.id"/>
+ <XActivity class="_content _vMargin" :user="user" :key="user.id"/>
</div>
- </MkFolder>
+ <div :class="{ _section: narrow === true, _vMargin: narrow === false }">
+ <XUserTimeline :user="user" class="_content"/>
+ </div>
+ </template>
+ <XFollowList v-else-if="page === 'following'" :class="{ _section: narrow === true, _vMargin: narrow === false }" type="following" :user="user"/>
+ <XFollowList v-else-if="page === 'followers'" :class="{ _section: narrow === true, _vMargin: narrow === false }" type="followers" :user="user"/>
</div>
- <div class="_section">
- <XUserTimeline :user="user" class="_content"/>
+ <div class="side" v-if="narrow === false">
+ <XPhotos class="_vMargin" :user="user" :key="user.id"/>
+ <XActivity class="_vMargin" :user="user" :key="user.id"/>
</div>
- </template>
- <XFollowList v-else-if="page === 'following'" type="following" :user="user"/>
- <XFollowList v-else-if="page === 'followers'" type="followers" :user="user"/>
-</div>
-<div v-else-if="error">
- <MkError @retry="fetch()"/>
+ </div>
+ <div v-else-if="error">
+ <MkError @retry="fetch()"/>
+ </div>
</div>
</template>
@@ -170,6 +168,7 @@ export default defineComponent({
user: null,
error: null,
parallaxAnimationId: null,
+ narrow: null,
faExclamationTriangle, faEllipsisH, faRobot, faLock, faBookmark, farBookmark, faChartBar, faImage, faBirthdayCake, faMapMarker, faCalendarAlt
};
},
@@ -197,6 +196,7 @@ export default defineComponent({
mounted() {
window.requestAnimationFrame(this.parallaxLoop);
+ this.narrow = this.$el.clientWidth < 1000;
},
beforeUnmount() {
@@ -254,220 +254,234 @@ export default defineComponent({
<style lang="scss" scoped>
.mk-user-page {
- > .punished {
- font-size: 0.8em;
- padding: 16px;
- }
+ display: flex;
+ max-width: 1050px;
+ margin: 0 auto;
+
+ > .main {
+ flex: 1;
- > .profile {
- > ._content {
- position: relative;
- overflow: hidden;
+ > .punished {
+ font-size: 0.8em;
+ padding: 16px;
+ }
- > .banner-container {
+ > .profile {
+ > ._content {
position: relative;
- height: 250px;
overflow: hidden;
- background-size: cover;
- background-position: center;
- border-radius: 12px;
- > .banner {
- height: 100%;
- background-color: #4c5e6d;
+ > .banner-container {
+ position: relative;
+ height: 250px;
+ overflow: hidden;
background-size: cover;
background-position: center;
- box-shadow: 0 0 128px rgba(0, 0, 0, 0.5) inset;
- will-change: background-position;
- }
- > .fade {
- position: absolute;
- bottom: 0;
- left: 0;
- width: 100%;
- height: 78px;
- background: linear-gradient(transparent, rgba(#000, 0.7));
- }
-
- > .followed {
- position: absolute;
- top: 12px;
- left: 12px;
- padding: 4px 8px;
- color: #fff;
- background: rgba(0, 0, 0, 0.7);
- font-size: 0.7em;
- border-radius: 6px;
- }
+ > .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;
+ }
- > .actions {
- position: absolute;
- top: 12px;
- right: 12px;
- -webkit-backdrop-filter: blur(8px);
- backdrop-filter: blur(8px);
- background: rgba(0, 0, 0, 0.2);
- padding: 8px;
- border-radius: 24px;
+ > .fade {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ height: 78px;
+ background: linear-gradient(transparent, rgba(#000, 0.7));
+ }
- > .menu {
- vertical-align: bottom;
- height: 31px;
- width: 31px;
+ > .followed {
+ position: absolute;
+ top: 12px;
+ left: 12px;
+ padding: 4px 8px;
color: #fff;
- text-shadow: 0 0 8px #000;
- font-size: 16px;
+ background: rgba(0, 0, 0, 0.7);
+ font-size: 0.7em;
+ border-radius: 6px;
}
- > .koudoku {
- margin-left: 4px;
- vertical-align: bottom;
- }
- }
+ > .actions {
+ position: absolute;
+ top: 12px;
+ right: 12px;
+ -webkit-backdrop-filter: blur(8px);
+ backdrop-filter: blur(8px);
+ background: rgba(0, 0, 0, 0.2);
+ padding: 8px;
+ border-radius: 24px;
- > .title {
- position: absolute;
- bottom: 0;
- left: 0;
- width: 100%;
- padding: 0 0 8px 154px;
- box-sizing: border-box;
- color: #fff;
+ > .menu {
+ vertical-align: bottom;
+ height: 31px;
+ width: 31px;
+ color: #fff;
+ text-shadow: 0 0 8px #000;
+ font-size: 16px;
+ }
- > .name {
- display: block;
- margin: 0;
- line-height: 32px;
- font-weight: bold;
- font-size: 1.8em;
- text-shadow: 0 0 8px #000;
+ > .koudoku {
+ margin-left: 4px;
+ vertical-align: bottom;
+ }
}
- > .bottom {
- > * {
- display: inline-block;
- margin-right: 16px;
- line-height: 20px;
- opacity: 0.8;
+ > .title {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ padding: 0 0 8px 154px;
+ box-sizing: border-box;
+ color: #fff;
+
+ > .name {
+ display: block;
+ margin: 0;
+ line-height: 32px;
+ font-weight: bold;
+ font-size: 1.8em;
+ text-shadow: 0 0 8px #000;
+ }
+
+ > .bottom {
+ > * {
+ display: inline-block;
+ margin-right: 16px;
+ line-height: 20px;
+ opacity: 0.8;
- &.username {
- font-weight: bold;
+ &.username {
+ font-weight: bold;
+ }
}
}
}
}
- }
- > .title {
- display: none;
- text-align: center;
- padding: 50px 8px 16px 8px;
- font-weight: bold;
- border-bottom: solid 1px var(--divider);
+ > .title {
+ display: none;
+ text-align: center;
+ padding: 50px 8px 16px 8px;
+ font-weight: bold;
+ border-bottom: solid 1px var(--divider);
- > .bottom {
- > * {
- display: inline-block;
- margin-right: 8px;
- opacity: 0.8;
+ > .bottom {
+ > * {
+ display: inline-block;
+ margin-right: 8px;
+ opacity: 0.8;
+ }
}
}
- }
- > .avatar {
- display: block;
- position: absolute;
- top: 170px;
- left: 16px;
- z-index: 2;
- width: 120px;
- height: 120px;
- box-shadow: 1px 1px 3px rgba(#000, 0.2);
- }
+ > .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;
+ > .description {
+ padding: 24px 24px 24px 154px;
+ font-size: 0.95em;
- > .empty {
- margin: 0;
- opacity: 0.5;
+ > .empty {
+ margin: 0;
+ opacity: 0.5;
+ }
}
- }
- > .fields {
- padding: 24px;
- font-size: 0.9em;
- border-top: solid 1px var(--divider);
+ > .fields {
+ padding: 24px;
+ font-size: 0.9em;
+ border-top: solid 1px var(--divider);
- > .field {
- display: flex;
- padding: 0;
- margin: 0;
- align-items: center;
+ > .field {
+ display: flex;
+ padding: 0;
+ margin: 0;
+ align-items: center;
- &:not(:last-child) {
- margin-bottom: 8px;
- }
+ &:not(:last-child) {
+ margin-bottom: 8px;
+ }
- > .name {
- width: 30%;
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
- font-weight: bold;
- text-align: center;
- }
+ > .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;
+ > .value {
+ width: 70%;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
}
- }
- &.system > .field > .name {
+ &.system > .field > .name {
+ }
}
- }
- > .status {
- display: flex;
- padding: 24px;
- border-top: solid 1px var(--divider);
+ > .status {
+ display: flex;
+ padding: 24px;
+ border-top: solid 1px var(--divider);
- > a {
- flex: 1;
- text-align: center;
+ > a {
+ flex: 1;
+ text-align: center;
- &.active {
- color: var(--accent);
- }
+ &.active {
+ color: var(--accent);
+ }
- &:hover {
- text-decoration: none;
- }
+ &:hover {
+ text-decoration: none;
+ }
- > b {
- display: block;
- line-height: 16px;
- }
+ > b {
+ display: block;
+ line-height: 16px;
+ }
- > span {
- font-size: 70%;
+ > span {
+ font-size: 70%;
+ }
}
}
}
}
+
+ > .content {
+ margin-bottom: var(--margin);
+ }
}
- > .content {
- margin-bottom: var(--margin);
+ > .side {
+ flex-basis: 300px;
+ margin-left: var(--margin);
}
&.max-width_500px {
- > .profile > ._content {
+ display: block;
+
+ > .main > .profile > ._content {
> .banner-container {
height: 140px;
diff --git a/src/client/pages/welcome.entrance.vue b/src/client/pages/welcome.entrance.vue
index b1cd6d50c6..d5ea47bb85 100644
--- a/src/client/pages/welcome.entrance.vue
+++ b/src/client/pages/welcome.entrance.vue
@@ -1,11 +1,5 @@
<template>
<div class="rsqzvsbo _section" v-if="meta">
- <div class="about">
- <h1>{{ instanceName }}</h1>
- <div class="desc" v-html="meta.description || $t('introMisskey')"></div>
- <MkButton @click="signup()" style="display: inline-block; margin-right: 16px;" primary>{{ $t('signup') }}</MkButton>
- <MkButton @click="signin()" style="display: inline-block;">{{ $t('login') }}</MkButton>
- </div>
<div class="blocks">
<XBlock class="block" v-for="path in meta.pinnedPages" :initial-path="path" :key="path"/>
</div>
@@ -68,28 +62,6 @@ export default defineComponent({
.rsqzvsbo {
text-align: center;
- > .about {
- display: inline-block;
- padding: 24px;
- margin-bottom: var(--margin);
- -webkit-backdrop-filter: blur(8px);
- backdrop-filter: blur(8px);
- background: rgba(0, 0, 0, 0.5);
- border-radius: var(--radius);
- text-align: center;
- box-sizing: border-box;
- min-width: 300px;
- max-width: 800px;
-
- &, * {
- color: #fff !important;
- }
-
- > h1 {
- margin: 0 0 16px 0;
- }
- }
-
> .blocks {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
diff --git a/src/client/router.ts b/src/client/router.ts
index 5ad3345d55..a21c6494b9 100644
--- a/src/client/router.ts
+++ b/src/client/router.ts
@@ -22,7 +22,7 @@ export const router = createRouter({
{ path: '/@:user/pages/:page', component: page('page'), props: route => ({ pageName: route.params.page, username: route.params.user }) },
{ path: '/@:user/pages/:pageName/view-source', component: page('page-editor/page-editor'), props: route => ({ initUser: route.params.user, initPageName: route.params.pageName }) },
{ path: '/@:acct/room', props: true, component: page('room/room') },
- { path: '/settings/:page?', name: 'settings', component: page('settings/index'), props: route => ({ page: route.params.page || null }) },
+ { path: '/settings/:page(.*)?', name: 'settings', component: page('settings/index'), props: route => ({ page: route.params.page || null }) },
{ path: '/announcements', component: page('announcements') },
{ path: '/about', component: page('about') },
{ path: '/about-misskey', component: page('about-misskey') },
@@ -57,7 +57,6 @@ export const router = createRouter({
{ path: '/my/groups/:group', component: page('my-groups/group') },
{ path: '/my/antennas', component: page('my-antennas/index') },
{ path: '/my/clips', component: page('my-clips/index') },
- { path: '/my/apps', component: page('apps') },
{ path: '/scratchpad', component: page('scratchpad') },
{ path: '/instance', component: page('instance/index') },
{ path: '/instance/emojis', component: page('instance/emojis') },
diff --git a/src/client/scripts/sound.ts b/src/client/scripts/sound.ts
new file mode 100644
index 0000000000..13fd9a80f5
--- /dev/null
+++ b/src/client/scripts/sound.ts
@@ -0,0 +1,24 @@
+import { device } from '@/cold-storage';
+
+const cache = new Map<string, HTMLAudioElement>();
+
+export function play(type: string) {
+ const sound = device.get('sound_' + type as any);
+ if (sound.type == null) return;
+ playFile(sound.type, sound.volume);
+}
+
+export function playFile(file: string, volume: number) {
+ const masterVolume = device.get('sound_masterVolume');
+ if (masterVolume === 0) return;
+
+ let audio: HTMLAudioElement;
+ if (cache.has(file)) {
+ audio = cache.get(file);
+ } else {
+ audio = new Audio(`/assets/sounds/${file}.mp3`);
+ cache.set(file, audio);
+ }
+ audio.volume = masterVolume - ((1 - volume) * masterVolume);
+ audio.play();
+}
diff --git a/src/client/scripts/theme.ts b/src/client/scripts/theme.ts
index c1fc88bf0e..c1580c6367 100644
--- a/src/client/scripts/theme.ts
+++ b/src/client/scripts/theme.ts
@@ -15,19 +15,12 @@ export const darkTheme: Theme = require('../themes/_dark.json5');
export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X'));
export const builtinThemes = [
- require('../themes/l-white.json5'),
- require('../themes/l-red.json5'),
- require('../themes/l-green.json5'),
- require('../themes/l-blue.json5'),
+ require('../themes/l-light.json5'),
require('../themes/l-apricot.json5'),
- require('../themes/d-black.json5'),
- require('../themes/d-red.json5'),
- require('../themes/d-green.json5'),
- require('../themes/d-blue.json5'),
+ require('../themes/d-dark.json5'),
require('../themes/d-persimmon.json5'),
-
- require('../themes/d-battery-saver.json5'),
+ require('../themes/d-black.json5'),
] as Theme[];
let timeout = null;
diff --git a/src/client/store.ts b/src/client/store.ts
index cb7f993378..2c63e79503 100644
--- a/src/client/store.ts
+++ b/src/client/store.ts
@@ -55,7 +55,7 @@ export const defaultDeviceUserSettings = {
export const defaultDeviceSettings = {
lang: null,
loadRawImages: false,
- alwaysShowNsfw: false,
+ nsfw: 'respect', // respect, force, ignore
useOsNativeEmojis: false,
serverDisconnectedBehavior: 'quiet',
accounts: [],
@@ -87,14 +87,6 @@ export const defaultDeviceSettings = {
deckColumnAlign: 'left',
deckAlwaysShowMainColumn: true,
deckMainColumnPlace: 'left',
- sfxVolume: 0.3,
- sfxNote: 'syuilo/down',
- sfxNoteMy: 'syuilo/up',
- sfxNotification: 'syuilo/pope2',
- sfxChat: 'syuilo/pope1',
- sfxChatBg: 'syuilo/waon',
- sfxAntenna: 'syuilo/triple',
- sfxChannel: 'syuilo/square-pico',
userData: {},
};
diff --git a/src/client/style.scss b/src/client/style.scss
index d7a78dc9c9..85a54706e6 100644
--- a/src/client/style.scss
+++ b/src/client/style.scss
@@ -448,10 +448,14 @@ hr {
opacity: 0.7;
}
+._monospace {
+ font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace;
+}
+
._code {
+ @extend ._monospace;
background: #2d2d2d;
color: #ccc;
- font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace;
font-size: 14px;
line-height: 1.5;
padding: 5px;
diff --git a/src/client/themes/_dark.json5 b/src/client/themes/_dark.json5
index ee6d9b49e9..f290586eb4 100644
--- a/src/client/themes/_dark.json5
+++ b/src/client/themes/_dark.json5
@@ -19,6 +19,7 @@
divider: 'rgba(255, 255, 255, 0.1)',
indicator: '@accent',
panel: '#000',
+ panelHighlight: ':lighten<3<@panel',
panelHeaderBg: ':lighten<3<@panel',
panelHeaderFg: '@fg',
panelHeaderDivider: 'rgba(0, 0, 0, 0)',
diff --git a/src/client/themes/_light.json5 b/src/client/themes/_light.json5
index 8821999395..0a1125cab7 100644
--- a/src/client/themes/_light.json5
+++ b/src/client/themes/_light.json5
@@ -19,6 +19,7 @@
divider: 'rgba(0, 0, 0, 0.1)',
indicator: '@accent',
panel: '#fff',
+ panelHighlight: ':darken<3<@panel',
panelHeaderBg: ':lighten<3<@panel',
panelHeaderFg: '@fg',
panelHeaderDivider: 'rgba(0, 0, 0, 0)',
diff --git a/src/client/themes/d-battery-saver.json5 b/src/client/themes/d-battery-saver.json5
deleted file mode 100644
index e6499ace96..0000000000
--- a/src/client/themes/d-battery-saver.json5
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- id: '8c539dc1-0fab-4d47-9194-39c508e9bfe1',
-
- name: 'Battery Saver',
- author: 'syuilo',
-
- base: 'dark',
-
- props: {
- divider: '#2d2d2d',
- panelHeaderBg: '@panel',
- panelHeaderDivider: '@divider',
- panelShadow: '" 0 0 0 1px var(--divider)',
- shadow: 'rgba(255, 255, 255, 0.05)',
- modalBg: 'rgba(255, 255, 255, 0.1)',
- messageBg: '#1d1d1d',
- },
-}
diff --git a/src/client/themes/d-black.json5 b/src/client/themes/d-black.json5
index 1e30d56473..b52e0fc394 100644
--- a/src/client/themes/d-black.json5
+++ b/src/client/themes/d-black.json5
@@ -1,29 +1,19 @@
{
- id: '8050783a-7f63-445a-b270-36d0f6ba1677',
+ id: '8c539dc1-0fab-4d47-9194-39c508e9bfe1',
name: 'Mi Black',
author: 'syuilo',
- desc: 'Default light theme',
base: 'dark',
props: {
- bg: '#272727',
- fg: 'rgb(199, 209, 216)',
- fgHighlighted: '#fff',
- divider: 'rgba(255, 255, 255, 0.14)',
- panel: '@bg',
- panelShadow: '" 0 0 0 1px var(--divider)',
+ divider: '#2d2d2d',
+ panel: '#0a0a0a',
panelHeaderBg: '@panel',
panelHeaderDivider: '@divider',
- infoFg: '@accent',
- infoBg: 'rgb(0, 0, 0)',
- header: ':alpha<0.7<@bg',
- navBg: '#363636',
- renote: '@accent',
- mention: '#da6d35',
- mentionMe: '#d44c4c',
- hashtag: '#4cb8d4',
- link: '@accent',
+ panelShadow: '" 0 8px 24px rgb(0 0 0 / 25%)',
+ shadow: 'rgba(255, 255, 255, 0.05)',
+ modalBg: 'rgba(255, 255, 255, 0.1)',
+ messageBg: '#1d1d1d',
},
}
diff --git a/src/client/themes/d-blue.json5 b/src/client/themes/d-blue.json5
deleted file mode 100644
index 96e6240e90..0000000000
--- a/src/client/themes/d-blue.json5
+++ /dev/null
@@ -1,29 +0,0 @@
-{
- id: 'ab4eb6d5-dcc0-4457-8a3c-98aad8ea3979',
-
- name: 'Mi D Blue',
- author: 'syuilo',
-
- base: 'dark',
-
- props: {
- accent: 'rgb(81 185 189)',
- bg: 'rgb(54, 54, 54)',
- fg: 'rgb(199, 209, 216)',
- fgHighlighted: '#fff',
- divider: 'rgba(255, 255, 255, 0.14)',
- panel: '@bg',
- panelShadow: '" 0 0 0 1px var(--divider)',
- panelHeaderBg: '@panel',
- panelHeaderDivider: '@divider',
- infoFg: '@accent',
- infoBg: 'rgb(0, 0, 0)',
- header: ':alpha<0.7<@bg',
- navBg: 'rgb(71, 71, 71)',
- renote: '@accent',
- mention: '#da6d35',
- mentionMe: '#d44c4c',
- hashtag: '#4cb8d4',
- link: '@accent',
- },
-}
diff --git a/src/client/themes/d-red.json5 b/src/client/themes/d-dark.json5
index 0f137322c0..7dd29b4a0f 100644
--- a/src/client/themes/d-red.json5
+++ b/src/client/themes/d-dark.json5
@@ -1,25 +1,25 @@
{
- id: '60960086-26da-4f3c-bb0c-f6a4f89e0f60',
+ id: '8050783a-7f63-445a-b270-36d0f6ba1677',
- name: 'Mi D Red',
+ name: 'Mi Dark',
author: 'syuilo',
+ desc: 'Default light theme',
base: 'dark',
props: {
- accent: 'rgb(196 115 69)',
- bg: 'rgb(54, 54, 54)',
+ bg: '#232323',
fg: 'rgb(199, 209, 216)',
fgHighlighted: '#fff',
divider: 'rgba(255, 255, 255, 0.14)',
- panel: '@bg',
- panelShadow: '" 0 0 0 1px var(--divider)',
+ panel: '#2d2d2d',
+ panelShadow: '" 0 8px 24px rgb(0 0 0 / 25%)',
panelHeaderBg: '@panel',
panelHeaderDivider: '@divider',
infoFg: '@accent',
infoBg: 'rgb(0, 0, 0)',
header: ':alpha<0.7<@bg',
- navBg: 'rgb(71, 71, 71)',
+ navBg: '#363636',
renote: '@accent',
mention: '#da6d35',
mentionMe: '#d44c4c',
diff --git a/src/client/themes/d-green.json5 b/src/client/themes/d-green.json5
deleted file mode 100644
index f1f90d1c78..0000000000
--- a/src/client/themes/d-green.json5
+++ /dev/null
@@ -1,29 +0,0 @@
-{
- id: '326dc4bf-29d9-45b4-889e-bdc33e84919b',
-
- name: 'Mi D Green',
- author: 'syuilo',
-
- base: 'dark',
-
- props: {
- accent: 'rgb(152, 196, 69)',
- bg: 'rgb(54, 54, 54)',
- fg: 'rgb(199, 209, 216)',
- fgHighlighted: '#fff',
- divider: 'rgba(255, 255, 255, 0.14)',
- panel: '@bg',
- panelShadow: '" 0 0 0 1px var(--divider)',
- panelHeaderBg: '@panel',
- panelHeaderDivider: '@divider',
- infoFg: '@accent',
- infoBg: 'rgb(0, 0, 0)',
- header: ':alpha<0.7<@bg',
- navBg: 'rgb(71, 71, 71)',
- renote: '@accent',
- mention: '#da6d35',
- mentionMe: '#d44c4c',
- hashtag: '#4cb8d4',
- link: '@accent',
- },
-}
diff --git a/src/client/themes/d-persimmon.json5 b/src/client/themes/d-persimmon.json5
index 2c32e0797b..067911ace6 100644
--- a/src/client/themes/d-persimmon.json5
+++ b/src/client/themes/d-persimmon.json5
@@ -1,23 +1,23 @@
{
id: 'c503d768-7c70-4db2-a4e6-08264304bc8d',
- name: 'Ai Persimmon',
+ name: 'Mi Persimmon',
author: 'syuilo',
base: 'dark',
props: {
accent: 'rgb(206, 102, 65)',
- bg: 'rgb(41, 43, 41)',
+ bg: 'rgb(31, 33, 31)',
fg: '#cdd8c7',
fgHighlighted: '#fff',
divider: 'rgba(255, 255, 255, 0.14)',
- panel: '@bg',
- panelShadow: '" 0 0 0 1px var(--divider)',
+ panel: 'rgb(41, 43, 41)',
+ panelShadow: '" 0 8px 24px rgb(0 0 0 / 25%)',
panelHeaderBg: '@panel',
panelHeaderDivider: '@divider',
- infoFg: '@accent',
- infoBg: 'rgb(0, 0, 0)',
+ infoFg: '@fg',
+ infoBg: '#333c3b',
header: ':alpha<0.7<@bg',
navBg: '#1f211f',
renote: '@accent',
diff --git a/src/client/themes/l-apricot.json5 b/src/client/themes/l-apricot.json5
index 7fbc2b47c7..4bdeea21a5 100644
--- a/src/client/themes/l-apricot.json5
+++ b/src/client/themes/l-apricot.json5
@@ -1,7 +1,7 @@
{
id: '0ff48d43-aab3-46e7-ab12-8492110d2e2b',
- name: 'Ai Apricot',
+ name: 'Mi Apricot',
author: 'syuilo',
base: 'light',
diff --git a/src/client/themes/l-blue.json5 b/src/client/themes/l-blue.json5
deleted file mode 100644
index 06c06da08b..0000000000
--- a/src/client/themes/l-blue.json5
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- id: 'ad18a23b-6af6-4af0-9ed4-600568250574',
-
- name: 'Mi L Blue',
- author: 'syuilo',
-
- base: 'light',
-
- props: {
- accent: '#4dbccc',
- bg: '#fff',
- fg: '#5d5d5d',
- divider: 'rgb(223, 223, 223)',
- header: ':alpha<0.7<@bg',
- navBg: '@bg',
- panel: '@bg',
- panelShadow: '" 0 0 0 1px var(--divider)',
- panelHeaderDivider: '@divider',
- messageBg: '#dedede',
- },
-}
diff --git a/src/client/themes/l-green.json5 b/src/client/themes/l-green.json5
deleted file mode 100644
index 5a9eb8e0a2..0000000000
--- a/src/client/themes/l-green.json5
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- id: 'a55af79a-12bf-4f8d-a0cc-718957ad59b4',
-
- name: 'Mi L Green',
- author: 'syuilo',
-
- base: 'light',
-
- props: {
- accent: '#8bcc4d',
- bg: '#fff',
- fg: '#5d5d5d',
- divider: 'rgb(223, 223, 223)',
- header: ':alpha<0.7<@bg',
- navBg: '@bg',
- panel: '@bg',
- panelShadow: '" 0 0 0 1px var(--divider)',
- panelHeaderDivider: '@divider',
- messageBg: '#dedede',
- },
-}
diff --git a/src/client/themes/l-white.json5 b/src/client/themes/l-light.json5
index 9daa60c119..f7ec85d01e 100644
--- a/src/client/themes/l-white.json5
+++ b/src/client/themes/l-light.json5
@@ -1,7 +1,7 @@
{
id: '4eea646f-7afa-4645-83e9-83af0333cd37',
- name: 'Mi White',
+ name: 'Mi Light',
author: 'syuilo',
desc: 'Default light theme',
diff --git a/src/client/themes/l-red.json5 b/src/client/themes/l-red.json5
deleted file mode 100644
index 22139c3aaa..0000000000
--- a/src/client/themes/l-red.json5
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- id: '957db7cb-30fb-4c80-bf0b-04198e7ae7e3',
-
- name: 'Mi L Red',
- author: 'syuilo',
-
- base: 'light',
-
- props: {
- accent: '#fb734d',
- bg: '#fff',
- fg: '#5d5d5d',
- divider: 'rgb(223, 223, 223)',
- header: ':alpha<0.7<@bg',
- navBg: '@bg',
- panel: '@bg',
- panelShadow: '" 0 0 0 1px var(--divider)',
- panelHeaderDivider: '@divider',
- messageBg: '#dedede',
- },
-}
diff --git a/src/client/ui/_common_/common.vue b/src/client/ui/_common_/common.vue
index d06cbb9869..469220806d 100644
--- a/src/client/ui/_common_/common.vue
+++ b/src/client/ui/_common_/common.vue
@@ -15,8 +15,9 @@
<script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue';
-import { stream, sound, popup, popups, uploads, pendingApiRequestsCount } from '@/os';
+import { stream, popup, popups, uploads, pendingApiRequestsCount } from '@/os';
import { store } from '@/store';
+import * as sound from '@/scripts/sound';
export default defineComponent({
components: {
@@ -38,7 +39,7 @@ export default defineComponent({
}, {}, 'closed');
}
- sound('notification');
+ sound.play('notification');
};
if (store.getters.isSignedIn) {
diff --git a/src/client/ui/visitor.vue b/src/client/ui/visitor.vue
index 56cc270be7..0d83088882 100644
--- a/src/client/ui/visitor.vue
+++ b/src/client/ui/visitor.vue
@@ -1,209 +1,19 @@
<template>
-<div class="mk-app">
- <header>
- <MkA class="link" to="/">{{ $t('home') }}</MkA>
- <MkA class="link" to="/announcements">{{ $t('announcements') }}</MkA>
- <MkA class="link" to="/channels">{{ $t('channel') }}</MkA>
- <MkA class="link" to="/about">{{ $t('aboutX', { x: instanceName }) }}</MkA>
- </header>
-
- <div class="banner" :class="{ asBg: $route.path === '/' }" :style="{ backgroundImage: `url(${ $store.state.instance.meta.bannerUrl })` }">
- <h1 v-if="$route.path !== '/'">{{ instanceName }}</h1>
- </div>
-
- <div class="contents" ref="contents" :class="{ wallpaper }">
- <header class="header" ref="header" v-show="$route.path !== '/'">
- <XHeader :info="pageInfo"/>
- </header>
- <main ref="main">
- <router-view v-slot="{ Component }">
- <transition :name="$store.state.device.animation ? 'page' : ''" mode="out-in" @enter="onTransition">
- <component :is="Component" :ref="changePage"/>
- </transition>
- </router-view>
- </main>
- <div class="powered-by">
- <b><MkA to="/">{{ host }}</MkA></b>
- <small>Powered by <a href="https://github.com/syuilo/misskey" target="_blank">Misskey</a></small>
- </div>
- </div>
-
- <XCommon/>
-</div>
+<DesignA/>
+<XCommon/>
</template>
<script lang="ts">
import { defineComponent, defineAsyncComponent } from 'vue';
-import { } from '@fortawesome/free-solid-svg-icons';
-import { host, instanceName } from '@/config';
-import { search } from '@/scripts/search';
-import * as os from '@/os';
-import XHeader from './_common_/header.vue';
+import DesignA from './visitor/a.vue';
+import DesignB from './visitor/b.vue';
import XCommon from './_common_/common.vue';
-const DESKTOP_THRESHOLD = 1100;
-
export default defineComponent({
components: {
XCommon,
- XHeader,
- },
-
- data() {
- return {
- host,
- instanceName,
- pageKey: 0,
- pageInfo: null,
- isDesktop: window.innerWidth >= DESKTOP_THRESHOLD,
- };
- },
-
- computed: {
- keymap(): any {
- return {
- 'd': () => {
- if (this.$store.state.device.syncDeviceDarkMode) return;
- this.$store.commit('device/set', { key: 'darkMode', value: !this.$store.state.device.darkMode });
- },
- 's': search,
- 'h|/': this.help
- };
- },
+ DesignA,
+ DesignB,
},
-
- watch: {
- $route(to, from) {
- this.pageKey++;
- },
- },
-
- created() {
- document.documentElement.style.overflowY = 'scroll';
- },
-
- mounted() {
- if (!this.isDesktop) {
- window.addEventListener('resize', () => {
- if (window.innerWidth >= DESKTOP_THRESHOLD) this.isDesktop = true;
- }, { passive: true });
- }
- },
-
- methods: {
- changePage(page) {
- if (page == null) return;
- if (page.INFO) {
- this.pageInfo = page.INFO;
- }
- },
-
- top() {
- window.scroll({ top: 0, behavior: 'smooth' });
- },
-
- help() {
- this.$router.push('/docs/keyboard-shortcut');
- },
-
- onTransition() {
- if (window._scroll) window._scroll();
- },
- }
});
</script>
-
-<style lang="scss" scoped>
-.mk-app {
- min-height: 100vh;
-
- > header {
- position: relative;
- z-index: 1;
- background: var(--panel);
- padding: 0 16px;
- text-align: center;
- overflow: auto;
- white-space: nowrap;
-
- > .link {
- display: inline-block;
- line-height: 60px;
- padding: 0 0.7em;
-
- &.MkA-active {
- box-shadow: 0 -2px 0 0 var(--accent) inset;
- }
- }
- }
-
- > .banner {
- position: relative;
- width: 100%;
- height: 200px;
- background-size: cover;
- background-position: center;
-
- &.asBg {
- position: absolute;
- left: 0;
- height: 320px;
- }
-
- &:after {
- content: "";
- display: block;
- position: absolute;
- bottom: 0;
- left: 0;
- width: 100%;
- height: 64px;
- background: linear-gradient(transparent, var(--bg));
- }
-
- > h1 {
- margin: 0;
- text-align: center;
- color: #fff;
- text-shadow: 0 0 8px #000;
- line-height: 200px;
- }
- }
-
- > .contents {
- position: relative;
- z-index: 1;
-
- > .header {
- position: sticky;
- top: 0;
- left: 0;
- z-index: 1000;
- height: 60px;
- width: 100%;
- line-height: 60px;
- text-align: center;
- -webkit-backdrop-filter: blur(32px);
- backdrop-filter: blur(32px);
- background-color: var(--header);
- border-bottom: 1px solid var(--divider);
- }
-
- > .powered-by {
- padding: 28px;
- font-size: 14px;
- text-align: center;
- border-top: 1px solid var(--divider);
-
- > small {
- display: block;
- margin-top: 8px;
- opacity: 0.5;
- }
- }
- }
-}
-</style>
-
-<style lang="scss">
-</style>
diff --git a/src/client/ui/visitor/a.vue b/src/client/ui/visitor/a.vue
new file mode 100644
index 0000000000..da09a9363b
--- /dev/null
+++ b/src/client/ui/visitor/a.vue
@@ -0,0 +1,357 @@
+<template>
+<div class="mk-app">
+ <div class="banner" v-if="$route.path === '/'" :style="{ backgroundImage: `url(${ $store.state.instance.meta.bannerUrl })` }">
+ <div>
+ <header>
+ <MkA class="link" to="/">{{ $t('home') }}</MkA>
+ <MkA class="link" to="/announcements">{{ $t('announcements') }}</MkA>
+ <MkA class="link" to="/channels">{{ $t('channel') }}</MkA>
+ <MkA class="link" to="/about">{{ $t('aboutX', { x: instanceName }) }}</MkA>
+ </header>
+ <h1 v-if="meta"><img class="logo" v-if="meta.logoImageUrl" :src="meta.logoImageUrl"><span v-else class="text">{{ instanceName }}</span></h1>
+ <div class="about" v-if="meta">
+ <div class="desc" v-html="meta.description || $t('introMisskey')"></div>
+ </div>
+ <div class="action">
+ <button class="_button primary" @click="signup()">{{ $t('signup') }}</button>
+ <button class="_button" @click="signin()">{{ $t('login') }}</button>
+ </div>
+ </div>
+ </div>
+ <div class="banner-mini" v-else :style="{ backgroundImage: `url(${ $store.state.instance.meta.bannerUrl })` }">
+ <div>
+ <header>
+ <MkA class="link" to="/">{{ $t('home') }}</MkA>
+ <MkA class="link" to="/announcements">{{ $t('announcements') }}</MkA>
+ <MkA class="link" to="/channels">{{ $t('channel') }}</MkA>
+ <MkA class="link" to="/about">{{ $t('aboutX', { x: instanceName }) }}</MkA>
+ <div class="action">
+ <button class="_button primary" @click="signup()">{{ $t('signup') }}</button>
+ <button class="_button" @click="signin()">{{ $t('login') }}</button>
+ </div>
+ </header>
+ <h1 v-if="meta"><img class="logo" v-if="meta.logoImageUrl" :src="meta.logoImageUrl"><span v-else class="text">{{ instanceName }}</span></h1>
+ </div>
+ </div>
+
+ <div class="main">
+ <div class="contents" ref="contents" :class="{ wallpaper }">
+ <header class="header" ref="header" v-show="$route.path !== '/'">
+ <XHeader :info="pageInfo"/>
+ </header>
+ <main ref="main">
+ <router-view v-slot="{ Component }">
+ <transition :name="$store.state.device.animation ? 'page' : ''" mode="out-in" @enter="onTransition">
+ <component :is="Component" :ref="changePage"/>
+ </transition>
+ </router-view>
+ </main>
+ <div class="powered-by">
+ <b><MkA to="/">{{ host }}</MkA></b>
+ <small>Powered by <a href="https://github.com/syuilo/misskey" target="_blank">Misskey</a></small>
+ </div>
+ </div>
+ </div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, defineAsyncComponent } from 'vue';
+import { } from '@fortawesome/free-solid-svg-icons';
+import { host, instanceName } from '@/config';
+import { search } from '@/scripts/search';
+import * as os from '@/os';
+import MkPagination from '@/components/ui/pagination.vue';
+import XSigninDialog from '@/components/signin-dialog.vue';
+import XSignupDialog from '@/components/signup-dialog.vue';
+import MkButton from '@/components/ui/button.vue';
+import XHeader from '../_common_/header.vue';
+
+const DESKTOP_THRESHOLD = 1100;
+
+export default defineComponent({
+ components: {
+ XHeader,
+ MkPagination,
+ MkButton,
+ },
+
+ data() {
+ return {
+ host,
+ instanceName,
+ pageKey: 0,
+ pageInfo: null,
+ meta: null,
+ narrow: window.innerWidth < 1280,
+ announcements: {
+ endpoint: 'announcements',
+ limit: 10,
+ },
+ isDesktop: window.innerWidth >= DESKTOP_THRESHOLD,
+ };
+ },
+
+ computed: {
+ keymap(): any {
+ return {
+ 'd': () => {
+ if (this.$store.state.device.syncDeviceDarkMode) return;
+ this.$store.commit('device/set', { key: 'darkMode', value: !this.$store.state.device.darkMode });
+ },
+ 's': search,
+ 'h|/': this.help
+ };
+ },
+ },
+
+ watch: {
+ $route(to, from) {
+ this.pageKey++;
+ },
+ },
+
+ created() {
+ document.documentElement.style.overflowY = 'scroll';
+
+ os.api('meta', { detail: true }).then(meta => {
+ this.meta = meta;
+ });
+ },
+
+ mounted() {
+ if (!this.isDesktop) {
+ window.addEventListener('resize', () => {
+ if (window.innerWidth >= DESKTOP_THRESHOLD) this.isDesktop = true;
+ }, { passive: true });
+ }
+ },
+
+ methods: {
+ setParallax(el) {
+ //new simpleParallax(el);
+ },
+
+ changePage(page) {
+ if (page == null) return;
+ if (page.INFO) {
+ this.pageInfo = page.INFO;
+ }
+ },
+
+ top() {
+ window.scroll({ top: 0, behavior: 'smooth' });
+ },
+
+ help() {
+ this.$router.push('/docs/keyboard-shortcut');
+ },
+
+ onTransition() {
+ if (window._scroll) window._scroll();
+ },
+
+ signin() {
+ os.popup(XSigninDialog, {
+ autoSet: true
+ }, {}, 'closed');
+ },
+
+ signup() {
+ os.popup(XSignupDialog, {
+ autoSet: true
+ }, {}, 'closed');
+ }
+ }
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-app {
+ min-height: 100vh;
+
+ > .banner {
+ position: relative;
+ width: 100%;
+ text-align: center;
+ background-position: center;
+ background-size: cover;
+
+ > div {
+ height: 100%;
+ background: rgba(0, 0, 0, 0.3);
+
+ * {
+ color: #fff;
+ }
+
+ > h1 {
+ margin: 0;
+ padding: 96px 32px 0 32px;
+ text-shadow: 0 0 8px black;
+
+ > .logo {
+ vertical-align: bottom;
+ max-height: 150px;
+ }
+ }
+
+ > .about {
+ padding: 32px;
+ max-width: 580px;
+ margin: 0 auto;
+ box-sizing: border-box;
+ text-shadow: 0 0 8px black;
+ }
+
+ > .action {
+ padding-bottom: 64px;
+
+ > button {
+ display: inline-block;
+ padding: 10px 20px;
+ box-sizing: border-box;
+ text-align: center;
+ border-radius: 999px;
+ background: var(--panel);
+ color: var(--fg);
+
+ &.primary {
+ background: var(--accent);
+ color: #fff;
+ }
+
+ &:first-child {
+ margin-right: 16px;
+ }
+ }
+ }
+ }
+ }
+
+ > .banner-mini {
+ position: relative;
+ width: 100%;
+ text-align: center;
+ background-position: center;
+ background-size: cover;
+
+ > div {
+ position: relative;
+ z-index: 1;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.3);
+
+ * {
+ color: #fff !important;
+ }
+
+ > header {
+
+ }
+
+ > h1 {
+ margin: 0;
+ padding: 32px;
+ text-shadow: 0 0 8px black;
+
+ > .logo {
+ vertical-align: bottom;
+ max-height: 100px;
+ }
+ }
+ }
+ }
+
+ > .main {
+ > header {
+ position: relative;
+ z-index: 1;
+ background: var(--panel);
+ padding: 0 32px;
+ text-align: left;
+ overflow: auto;
+ white-space: nowrap;
+
+ > .link {
+ display: inline-block;
+ line-height: 60px;
+ padding: 0 0.7em;
+
+ &.MkA-active {
+ box-shadow: 0 -2px 0 0 var(--accent) inset;
+ }
+ }
+ }
+
+ > .banner {
+ position: relative;
+ width: 100%;
+ height: 200px;
+ background-size: cover;
+ background-position: center;
+
+ &.asBg {
+ position: absolute;
+ left: 0;
+ height: 320px;
+ }
+
+ &:after {
+ content: "";
+ display: block;
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ height: 64px;
+ background: linear-gradient(transparent, var(--bg));
+ }
+
+ > h1 {
+ margin: 0;
+ text-align: center;
+ color: #fff;
+ text-shadow: 0 0 8px #000;
+ line-height: 200px;
+ }
+ }
+
+ > .contents {
+ position: relative;
+ z-index: 1;
+
+ > .header {
+ position: sticky;
+ top: 0;
+ left: 0;
+ z-index: 1000;
+ height: 60px;
+ width: 100%;
+ line-height: 60px;
+ text-align: center;
+ -webkit-backdrop-filter: blur(32px);
+ backdrop-filter: blur(32px);
+ background-color: var(--header);
+ border-bottom: 1px solid var(--divider);
+ }
+
+ > .powered-by {
+ padding: 28px;
+ font-size: 14px;
+ text-align: center;
+ border-top: 1px solid var(--divider);
+
+ > small {
+ display: block;
+ margin-top: 8px;
+ opacity: 0.5;
+ }
+ }
+ }
+ }
+}
+</style>
+
+<style lang="scss">
+</style>
diff --git a/src/client/ui/visitor/b.vue b/src/client/ui/visitor/b.vue
new file mode 100644
index 0000000000..13f93a522e
--- /dev/null
+++ b/src/client/ui/visitor/b.vue
@@ -0,0 +1,372 @@
+<template>
+<div class="mk-app">
+ <div class="side" v-if="!narrow">
+ <div :style="{ backgroundImage: `url(${ $store.state.instance.meta.backgroundImageUrl })` }">
+ <div class="fade"></div>
+ <h1 v-if="meta"><img class="logo" v-if="meta.logoImageUrl" :src="meta.logoImageUrl"><span v-else class="text">{{ instanceName }}</span></h1>
+ <div class="about _panel" v-if="meta">
+ <div class="desc" v-html="meta.description || $t('introMisskey')"></div>
+ </div>
+ <div class="action">
+ <button class="_button primary" @click="signup()">{{ $t('signup') }}</button>
+ <button class="_button" @click="signin()">{{ $t('login') }}</button>
+ </div>
+ <div class="announcements panel">
+ <header>{{ $t('announcements') }}</header>
+ <MkPagination :pagination="announcements" #default="{items}" class="list">
+ <section class="item" v-for="(announcement, i) in items" :key="announcement.id">
+ <div class="title">{{ announcement.title }}</div>
+ <div class="content">
+ <Mfm :text="announcement.text"/>
+ <img v-if="announcement.imageUrl" :src="announcement.imageUrl"/>
+ </div>
+ </section>
+ </MkPagination>
+ </div>
+ </div>
+ </div>
+
+ <div class="main">
+ <header>
+ <MkA class="link" to="/">{{ $t('home') }}</MkA>
+ <MkA class="link" to="/announcements">{{ $t('announcements') }}</MkA>
+ <MkA class="link" to="/channels">{{ $t('channel') }}</MkA>
+ <MkA class="link" to="/about">{{ $t('aboutX', { x: instanceName }) }}</MkA>
+ </header>
+
+ <div v-if="narrow" class="banner" :style="{ backgroundImage: `url(${ $store.state.instance.meta.bannerUrl })` }">
+ <h1 v-if="meta"><img class="logo" v-if="meta.logoImageUrl" :src="meta.logoImageUrl"><span v-else class="text">{{ instanceName }}</span></h1>
+ </div>
+
+ <div class="contents" ref="contents" :class="{ wallpaper }">
+ <header class="header" ref="header" v-show="$route.path !== '/'">
+ <XHeader :info="pageInfo"/>
+ </header>
+ <main ref="main">
+ <router-view v-slot="{ Component }">
+ <transition :name="$store.state.device.animation ? 'page' : ''" mode="out-in" @enter="onTransition">
+ <component :is="Component" :ref="changePage"/>
+ </transition>
+ </router-view>
+ </main>
+ <div class="powered-by">
+ <b><MkA to="/">{{ host }}</MkA></b>
+ <small>Powered by <a href="https://github.com/syuilo/misskey" target="_blank">Misskey</a></small>
+ </div>
+ </div>
+ </div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, defineAsyncComponent } from 'vue';
+import { } from '@fortawesome/free-solid-svg-icons';
+import { host, instanceName } from '@/config';
+import { search } from '@/scripts/search';
+import * as os from '@/os';
+import MkPagination from '@/components/ui/pagination.vue';
+import XSigninDialog from '@/components/signin-dialog.vue';
+import XSignupDialog from '@/components/signup-dialog.vue';
+import MkButton from '@/components/ui/button.vue';
+import XHeader from '../_common_/header.vue';
+
+const DESKTOP_THRESHOLD = 1100;
+
+export default defineComponent({
+ components: {
+ XHeader,
+ MkPagination,
+ MkButton,
+ },
+
+ data() {
+ return {
+ host,
+ instanceName,
+ pageKey: 0,
+ pageInfo: null,
+ meta: null,
+ narrow: window.innerWidth < 1280,
+ announcements: {
+ endpoint: 'announcements',
+ limit: 10,
+ },
+ isDesktop: window.innerWidth >= DESKTOP_THRESHOLD,
+ };
+ },
+
+ computed: {
+ keymap(): any {
+ return {
+ 'd': () => {
+ if (this.$store.state.device.syncDeviceDarkMode) return;
+ this.$store.commit('device/set', { key: 'darkMode', value: !this.$store.state.device.darkMode });
+ },
+ 's': search,
+ 'h|/': this.help
+ };
+ },
+ },
+
+ watch: {
+ $route(to, from) {
+ this.pageKey++;
+ },
+ },
+
+ created() {
+ document.documentElement.style.overflowY = 'scroll';
+
+ os.api('meta', { detail: true }).then(meta => {
+ this.meta = meta;
+ });
+ },
+
+ mounted() {
+ if (!this.isDesktop) {
+ window.addEventListener('resize', () => {
+ if (window.innerWidth >= DESKTOP_THRESHOLD) this.isDesktop = true;
+ }, { passive: true });
+ }
+ },
+
+ methods: {
+ changePage(page) {
+ if (page == null) return;
+ if (page.INFO) {
+ this.pageInfo = page.INFO;
+ }
+ },
+
+ top() {
+ window.scroll({ top: 0, behavior: 'smooth' });
+ },
+
+ help() {
+ this.$router.push('/docs/keyboard-shortcut');
+ },
+
+ onTransition() {
+ if (window._scroll) window._scroll();
+ },
+
+ signin() {
+ os.popup(XSigninDialog, {
+ autoSet: true
+ }, {}, 'closed');
+ },
+
+ signup() {
+ os.popup(XSignupDialog, {
+ autoSet: true
+ }, {}, 'closed');
+ }
+ }
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-app {
+ display: flex;
+ min-height: 100vh;
+
+ > .side {
+ width: 500px;
+ height: 100vh;
+ text-align: center;
+
+ > div {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 500px;
+ height: 100vh;
+ background-position: center;
+ background-size: cover;
+
+ > .panel {
+ -webkit-backdrop-filter: blur(8px);
+ backdrop-filter: blur(8px);
+ background: rgba(0, 0, 0, 0.5);
+ border-radius: var(--radius);
+
+ &, * {
+ color: #fff !important;
+ }
+ }
+
+ > .fade {
+ position: absolute;
+ z-index: -1;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 300px;
+ background: linear-gradient(rgba(#000, 0.5), transparent);
+ }
+
+ > h1 {
+ display: block;
+ margin: 0;
+ padding: 64px 32px 48px 32px;
+ color: #fff;
+
+ > .logo {
+ vertical-align: bottom;
+ max-height: 150px;
+ }
+ }
+
+ > .about {
+ display: block;
+ margin: 0 64px 16px 64px;
+ padding: 24px;
+ text-align: center;
+ box-sizing: border-box;
+ }
+
+ > .action {
+ padding: 0 64px;
+
+ > button {
+ display: block;
+ width: 100%;
+ padding: 10px;
+ box-sizing: border-box;
+ text-align: center;
+ border-radius: 999px;
+ background: var(--panel);
+
+ &.primary {
+ background: var(--accent);
+ color: #fff;
+ }
+
+ &:first-child {
+ margin-bottom: 16px;
+ }
+ }
+ }
+
+ > .announcements {
+ margin: 64px 64px 16px 64px;
+ text-align: left;
+
+ > header {
+ padding: 12px 16px;
+ border-bottom: solid 1px rgba(255, 255, 255, 0.5);
+ }
+
+ > .list {
+ max-height: 300px;
+ overflow: auto;
+
+ > .item {
+ padding: 12px 16px;
+
+ & + .item {
+ border-top: solid 1px rgba(255, 255, 255, 0.5);
+ }
+
+ > .title {
+ font-weight: bold;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ > .main {
+ flex: 1;
+
+ > header {
+ position: relative;
+ z-index: 1;
+ background: var(--panel);
+ padding: 0 32px;
+ text-align: left;
+ overflow: auto;
+ white-space: nowrap;
+
+ > .link {
+ display: inline-block;
+ line-height: 60px;
+ padding: 0 0.7em;
+
+ &.MkA-active {
+ box-shadow: 0 -2px 0 0 var(--accent) inset;
+ }
+ }
+ }
+
+ > .banner {
+ position: relative;
+ width: 100%;
+ height: 200px;
+ background-size: cover;
+ background-position: center;
+
+ &:after {
+ content: "";
+ display: block;
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ height: 64px;
+ background: linear-gradient(transparent, var(--bg));
+ }
+
+ > h1 {
+ margin: 0;
+ padding: 32px;
+ text-align: center;
+ color: #fff;
+ text-shadow: 0 0 8px #000;
+
+ > .logo {
+ vertical-align: bottom;
+ max-height: 150px;
+ }
+ }
+ }
+
+ > .contents {
+ position: relative;
+ z-index: 1;
+
+ > .header {
+ position: sticky;
+ top: 0;
+ left: 0;
+ z-index: 1000;
+ height: 60px;
+ width: 100%;
+ line-height: 60px;
+ text-align: center;
+ -webkit-backdrop-filter: blur(32px);
+ backdrop-filter: blur(32px);
+ background-color: var(--header);
+ border-bottom: 1px solid var(--divider);
+ }
+
+ > .powered-by {
+ padding: 28px;
+ font-size: 14px;
+ text-align: center;
+ border-top: 1px solid var(--divider);
+
+ > small {
+ display: block;
+ margin-top: 8px;
+ opacity: 0.5;
+ }
+ }
+ }
+ }
+}
+</style>
+
+<style lang="scss">
+</style>
diff --git a/src/client/widgets/digital-clock.vue b/src/client/widgets/digital-clock.vue
index 702f335c7f..9d32e8b9fe 100644
--- a/src/client/widgets/digital-clock.vue
+++ b/src/client/widgets/digital-clock.vue
@@ -1,5 +1,5 @@
<template>
-<div class="mkw-digitalClock" :class="{ _panel: !props.transparent }" :style="{ fontSize: `${props.fontSize}em` }">
+<div class="mkw-digitalClock _monospace" :class="{ _panel: !props.transparent }" :style="{ fontSize: `${props.fontSize}em` }">
<span>
<span v-text="hh"></span>
<span :style="{ visibility: showColon ? 'visible' : 'hidden' }">:</span>
@@ -74,7 +74,6 @@ export default defineComponent({
<style lang="scss" scoped>
.mkw-digitalClock {
padding: 16px 0;
- font-family: Lucida Console, Courier, monospace;
text-align: center;
}
</style>