Skip to content

Commit

Permalink
Merge pull request #4154 from traPtitech/Natsuki/theme-alter
Browse files Browse the repository at this point in the history
テーマ設定のUI改善
  • Loading branch information
mehm8128 committed May 10, 2024
2 parents b0dd54a + a53e653 commit 3cf4790
Show file tree
Hide file tree
Showing 6 changed files with 180 additions and 123 deletions.
3 changes: 3 additions & 0 deletions src/components/Modal/ModalContainer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import GroupCreateModal from './GroupCreateModal/GroupCreateModal.vue'
import GroupMemberEditModal from './GroupMemberEditModal/GroupMemberEditModal.vue'
import GroupAdminAddModal from './GroupAdminAddModal/GroupAdminAddModal.vue'
import GroupMemberAddModal from './GroupMemberAddModal/GroupMemberAddModal.vue'
import SettingsThemeEditModal from './SettingsThemeEditModal/SettingsThemeEditModal.vue'
const { shouldShowModal, currentState } = useModalStore()
Expand Down Expand Up @@ -96,6 +97,8 @@ const component = computed(() => {
return GroupAdminAddModal
case 'group-member-add':
return GroupMemberAddModal
case 'settings-theme-edit':
return SettingsThemeEditModal
}
// eslint-disable-next-line no-console
console.error('Unexpected modal type:', currentState.value)
Expand Down
133 changes: 133 additions & 0 deletions src/components/Modal/SettingsThemeEditModal/SettingsThemeEditModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
<template>
<modal-frame title="カスタムテーマ" icon-name="">
<div :class="$style.content">
<textarea-autosize v-model="editedTheme" :class="$style.jsonField" />
<div :class="$style.buttonContainer">
<form-button label="キャンセル" type="tertiary" @click="clearModal" />
<form-button
label="更新する"
:disabled="!isChanged"
type="primary"
@click="applyTheme"
/>
</div>
</div>
</modal-frame>
</template>

<script lang="ts">
import { computed, ref, watchEffect } from 'vue'
import type { Theme } from '/@/lib/theme/schema'
import { themeSchema } from '/@/lib/theme/schema'
import { dequal } from 'dequal'
import { useToastStore } from '/@/store/ui/toast'
import { reactive } from 'vue'
import { useThemeSettings } from '/@/store/app/themeSettings'
const useEditedThemes = (
props: { custom: Theme },
changeTheme: (theme: Theme) => void,
clearModal: () => void
) => {
const { addErrorToast } = useToastStore()
const appliedThemeStringified = computed(() => {
const theme = props.custom
return JSON.stringify(theme, null, '\t')
})
const editedTheme = ref(appliedThemeStringified.value)
watchEffect(() => {
editedTheme.value = appliedThemeStringified.value
})
const isChanged = computed(() => {
try {
return !dequal(JSON.parse(editedTheme.value), props.custom)
} catch {
return true
}
})
const failedUpdateTheme = (text: string) => {
addErrorToast(`テーマの更新に失敗しました: ${text}`)
}
const applyTheme = () => {
try {
const themeObj = JSON.parse(editedTheme.value)
const res = themeSchema.safeParse(themeObj)
if (res.success) {
changeTheme(res.data)
clearModal()
} else {
failedUpdateTheme(
`構文エラー: ${res.error.issues
.map(
issue =>
`[${issue.code}](${issue.path.join('.')}) ${issue.message}`
)
.join()}`
)
}
} catch {
failedUpdateTheme('無効なJSON')
}
}
return { editedTheme, isChanged, applyTheme }
}
</script>

<script lang="ts" setup>
import FormButton from '/@/components/UI/FormButton.vue'
import TextareaAutosize from '/@/components/UI/TextareaAutosize.vue'
import ModalFrame from '../Common/ModalFrame.vue'
import { useModalStore } from '/@/store/ui/modal'
const { clearModal } = useModalStore()
const state = reactive(useThemeSettings())
const changeTheme = (theme: Theme) => {
state.custom = theme
}
const { editedTheme, isChanged, applyTheme } = useEditedThemes(
state,
changeTheme,
clearModal
)
</script>

<style lang="scss" module>
.content {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 32px;
align-self: stretch;
}
.jsonField {
@include color-ui-primary;
@include background-secondary;
width: 100%;
display: flex;
max-height: 292px;
align-items: flex-start;
align-self: stretch;
border-radius: 4px;
border: solid 2px transparent;
padding: 4px;
&:focus-within {
border-color: $theme-accent-focus-default;
}
}
.buttonContainer {
display: flex;
justify-content: flex-end;
align-items: center;
align-content: center;
gap: 8px 16px;
align-self: stretch;
flex-wrap: wrap;
}
</style>
116 changes: 7 additions & 109 deletions src/components/Settings/ThemeTab/EditTheme.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,10 @@
/>
-->
<form-button
label="インポート/エクスポート"
:disabled="isImporterOpen"
type="tertiary"
label="import/export"
type="secondary"
:class="$style.element"
@click="onImportClick"
@click="openSettingsThemeEditModal"
/>
<!--
<form-button
Expand All @@ -26,100 +25,16 @@
/>
-->
</div>
<template v-if="isImporterOpen">
<textarea-autosize v-model="editedTheme" :class="$style.jsonField" />
<div :class="$style.import">
<form-button
label="保存"
:disabled="!isChanged"
type="primary"
@click="applyTheme"
/>
</div>
</template>
</template>

<script lang="ts">
import { computed, ref, watchEffect } from 'vue'
import type { Theme } from '/@/lib/theme/schema'
import { themeSchema } from '/@/lib/theme/schema'
import { dequal } from 'dequal'
import { useToastStore } from '/@/store/ui/toast'
const useEditedThemes = (
props: { custom: Theme },
emit: (name: 'changeTheme', theme: Theme) => void
) => {
const { addErrorToast } = useToastStore()
const appliedThemeStringified = computed(() => {
const theme = props.custom
return JSON.stringify(theme, null, '\t')
})
const editedTheme = ref(appliedThemeStringified.value)
watchEffect(() => {
editedTheme.value = appliedThemeStringified.value
})
const isChanged = computed(() => {
try {
return !dequal(JSON.parse(editedTheme.value), props.custom)
} catch {
return true
}
})
const failedUpdateTheme = (text: string) => {
addErrorToast(`テーマの更新に失敗しました: ${text}`)
}
const applyTheme = () => {
try {
const themeObj = JSON.parse(editedTheme.value)
const res = themeSchema.safeParse(themeObj)
if (res.success) {
emit('changeTheme', res.data)
} else {
failedUpdateTheme(
`構文エラー: ${res.error.issues
.map(
issue =>
`[${issue.code}](${issue.path.join('.')}) ${issue.message}`
)
.join()}`
)
}
} catch {
failedUpdateTheme('無効なJSON')
}
}
return { editedTheme, isChanged, applyTheme }
}
const useImporter = () => {
const isImporterOpen = ref(false)
const onImportClick = () => {
isImporterOpen.value = true
}
return { isImporterOpen, onImportClick }
}
</script>

<script lang="ts" setup>
import FormButton from '/@/components/UI/FormButton.vue'
import TextareaAutosize from '/@/components/UI/TextareaAutosize.vue'
const props = defineProps<{
custom: Theme
}>()
import { useModalStore } from '/@/store/ui/modal'
const emit = defineEmits<{
(e: 'changeTheme', _theme: Theme): void
}>()
const { pushModal } = useModalStore()
const { editedTheme, isChanged, applyTheme } = useEditedThemes(props, emit)
const { isImporterOpen, onImportClick } = useImporter()
const openSettingsThemeEditModal = () =>
pushModal({ type: 'settings-theme-edit' })
</script>

<style lang="scss" module>
Expand All @@ -131,21 +46,4 @@ const { isImporterOpen, onImportClick } = useImporter()
.element {
align-self: flex-end;
}
.jsonField {
@include color-ui-primary;
@include background-secondary;
width: 100%;
margin-top: 12px;
border-radius: 4px;
border: solid 2px transparent;
padding: 4px;
&:focus-within {
border-color: $theme-accent-focus-default;
}
}
.import {
display: flex;
justify-content: center;
margin: 12px;
}
</style>
11 changes: 8 additions & 3 deletions src/components/UI/FormRadio.vue
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,21 @@ const isChecked = computed(() => props.inputValue === value.value)

<style lang="scss" module>
.label {
display: flex;
align-items: center;
gap: 8px;
align-self: stretch;
font-weight: bold;
cursor: pointer;
border: solid 2px transparent;
border-radius: 4px;
&:focus-within {
border-color: $theme-accent-focus-default;
}
&:has(.radio:not(:checked)) {
opacity: 0.5;
}
}
.radio {
Expand All @@ -67,9 +75,6 @@ const isChecked = computed(() => props.inputValue === value.value)
width: 13px;
border: solid 2px $theme-ui-primary-default;
border-radius: 50%;
&[aria-checked='false'] {
opacity: 0.5;
}
}
.pseudoRadioInner {
display: inline-block;
Expand Down
6 changes: 6 additions & 0 deletions src/store/ui/modal/states.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type ModalStateType =
| 'group-member-edit'
| 'group-admin-add'
| 'group-member-add'
| 'settings-theme-edit'

export type ModalState =
| UserModalState
Expand All @@ -40,6 +41,7 @@ export type ModalState =
| GroupMemberEditModalState
| GroupAdminAddModalState
| GroupMemberAddModalState
| SettingsThemeEditState

interface BaseModalState {
/** モーダル種別 */
Expand Down Expand Up @@ -123,3 +125,7 @@ interface GroupMemberAddModalState extends BaseModalState {
type: 'group-member-add'
id: UserGroupId
}

interface SettingsThemeEditState extends BaseModalState {
type: 'settings-theme-edit'
}
Loading

0 comments on commit 3cf4790

Please sign in to comment.