diff options
| author | syuilo <syuilotan@yahoo.co.jp> | 2020-03-22 10:59:27 +0900 |
|---|---|---|
| committer | syuilo <syuilotan@yahoo.co.jp> | 2020-03-22 10:59:27 +0900 |
| commit | 7f0084a7ede7a3950ea6bdaeadd744f602d010cf (patch) | |
| tree | e339d8002b094f18dccabf364479aaab056ee0dd /src | |
| parent | Merge branch 'develop' (diff) | |
| parent | 12.23.0 (diff) | |
| download | misskey-7f0084a7ede7a3950ea6bdaeadd744f602d010cf.tar.gz misskey-7f0084a7ede7a3950ea6bdaeadd744f602d010cf.tar.bz2 misskey-7f0084a7ede7a3950ea6bdaeadd744f602d010cf.zip | |
Merge branch 'develop'
Diffstat (limited to 'src')
30 files changed, 485 insertions, 103 deletions
diff --git a/src/client/app.vue b/src/client/app.vue index 4e5dfbd18a..1e9fd6c0a9 100644 --- a/src/client/app.vue +++ b/src/client/app.vue @@ -164,6 +164,7 @@ import { v4 as uuid } from 'uuid'; import i18n from './i18n'; import { host, instanceName } from './config'; import { search } from './scripts/search'; +import { isDeviceDarkmode } from './scripts/is-device-darkmode'; import MkToast from './components/toast.vue'; const DESKTOP_THRESHOLD = 1100; @@ -224,6 +225,10 @@ export default Vue.extend({ }, created() { + if (this.$store.state.device.syncDeviceDarkMode) { + this.$store.commit('device/set', { key: 'darkMode', value: isDeviceDarkmode() }); + } + if (this.$store.getters.isSignedIn) { this.connection = this.$root.stream.useSharedConnection('main'); this.connection.on('notification', this.onNotification); diff --git a/src/client/components/drive.file.vue b/src/client/components/drive.file.vue index 2ef3fd38e4..014e1a6ca5 100644 --- a/src/client/components/drive.file.vue +++ b/src/client/components/drive.file.vue @@ -139,9 +139,9 @@ export default Vue.extend({ rename() { this.$root.dialog({ - title: this.$t('contextmenu.rename-file'), + title: this.$t('renameFile'), input: { - placeholder: this.$t('contextmenu.input-new-file-name'), + placeholder: this.$t('inputNewFileName'), default: this.file.name, allowEmpty: false } diff --git a/src/client/components/drive.folder.vue b/src/client/components/drive.folder.vue index 658aced3e8..2e861e2480 100644 --- a/src/client/components/drive.folder.vue +++ b/src/client/components/drive.folder.vue @@ -137,14 +137,14 @@ export default Vue.extend({ switch (err) { case 'detected-circular-definition': this.$root.dialog({ - title: this.$t('unable-to-process'), - text: this.$t('circular-reference-detected') + title: this.$t('unableToProcess'), + text: this.$t('circularReferenceFolder') }); break; default: this.$root.dialog({ type: 'error', - text: this.$t('unhandled-error') + text: this.$t('error') }); } }); @@ -177,9 +177,9 @@ export default Vue.extend({ rename() { this.$root.dialog({ - title: this.$t('contextmenu.rename-folder'), + title: this.$t('renameFolder'), input: { - placeholder: this.$t('contextmenu.input-new-folder-name'), + placeholder: this.$t('inputNewFolderName'), default: this.folder.name } }).then(({ canceled, result: name }) => { @@ -206,14 +206,14 @@ export default Vue.extend({ case 'b0fc8a17-963c-405d-bfbc-859a487295e1': this.$root.dialog({ type: 'error', - title: this.$t('unable-to-delete'), - text: this.$t('has-child-files-or-folders') + title: this.$t('unableToDelete'), + text: this.$t('hasChildFilesOrFolders') }); break; default: this.$root.dialog({ type: 'error', - text: this.$t('unable-to-delete') + text: this.$t('unableToDelete') }); } }); diff --git a/src/client/components/drive.vue b/src/client/components/drive.vue index bb35b156aa..fddac5b9aa 100644 --- a/src/client/components/drive.vue +++ b/src/client/components/drive.vue @@ -263,14 +263,14 @@ export default Vue.extend({ switch (err) { case 'detected-circular-definition': this.$root.dialog({ - title: this.$t('unable-to-process'), - text: this.$t('circular-reference-detected') + title: this.$t('unableToProcess'), + text: this.$t('circularReferenceFolder') }); break; default: this.$root.dialog({ type: 'error', - text: this.$t('unhandled-error') + text: this.$t('error') }); } }); @@ -284,9 +284,9 @@ export default Vue.extend({ urlUpload() { this.$root.dialog({ - title: this.$t('url-upload'), + title: this.$t('uploadFromUrl'), input: { - placeholder: this.$t('url-of-file') + placeholder: this.$t('uploadFromUrlDescription') } }).then(({ canceled, result: url }) => { if (canceled) return; @@ -296,17 +296,17 @@ export default Vue.extend({ }); this.$root.dialog({ - title: this.$t('url-upload-requested'), - text: this.$t('may-take-time') + title: this.$t('uploadFromUrlRequested'), + text: this.$t('uploadFromUrlMayTakeTime') }); }); }, createFolder() { this.$root.dialog({ - title: this.$t('create-folder'), + title: this.$t('createFolder'), input: { - placeholder: this.$t('folder-name') + placeholder: this.$t('folderName') } }).then(({ canceled, result: name }) => { if (canceled) return; @@ -321,9 +321,9 @@ export default Vue.extend({ renameFolder(folder) { this.$root.dialog({ - title: this.$t('contextmenu.rename-folder'), + title: this.$t('renameFolder'), input: { - placeholder: this.$t('contextmenu.input-new-folder-name'), + placeholder: this.$t('inputNewFolderName'), default: folder.name } }).then(({ canceled, result: name }) => { @@ -349,14 +349,14 @@ export default Vue.extend({ case 'b0fc8a17-963c-405d-bfbc-859a487295e1': this.$root.dialog({ type: 'error', - title: this.$t('unable-to-delete'), - text: this.$t('has-child-files-or-folders') + title: this.$t('unableToDelete'), + text: this.$t('hasChildFilesOrFolders') }); break; default: this.$root.dialog({ type: 'error', - text: this.$t('unable-to-delete') + text: this.$t('unableToDelete') }); } }); diff --git a/src/client/components/emoji-picker.vue b/src/client/components/emoji-picker.vue index a647b0ea04..e346023f29 100644 --- a/src/client/components/emoji-picker.vue +++ b/src/client/components/emoji-picker.vue @@ -2,12 +2,11 @@ <x-popup :source="source" ref="popup" @closed="() => { $emit('closed'); destroyDom(); }"> <div class="omfetrab"> <header> - <button v-for="category in categories" + <button v-for="(category, i) in categories" class="_button" - :title="category.text" @click="go(category)" :class="{ active: category.isActive }" - :key="category.text" + :key="i" > <fa :icon="category.icon" fixed-width/> </button> @@ -15,7 +14,7 @@ <div class="emojis"> <template v-if="categories[0].isActive"> - <header class="category"><fa :icon="faHistory" fixed-width/> {{ $t('recentUsedEmojis') }}</header> + <header class="category"><fa :icon="faHistory" fixed-width/> {{ $t('recentUsed') }}</header> <div class="list"> <button v-for="(emoji, i) in ($store.state.device.recentEmojis || [])" class="_button" @@ -27,9 +26,10 @@ <img v-else :src="$store.state.device.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/> </button> </div> + + <header class="category"><fa :icon="faAsterisk" fixed-width/> {{ $t('customEmojis') }}</header> </template> - <header class="category"><fa :icon="categories.find(x => x.isActive).icon" fixed-width/> {{ categories.find(x => x.isActive).text }}</header> <template v-if="categories.find(x => x.isActive).name"> <div class="list"> <button v-for="emoji in emojilist.filter(e => e.category === categories.find(x => x.isActive).name)" @@ -92,47 +92,38 @@ export default Vue.extend({ customEmojis: {}, faGlobe, faHistory, categories: [{ - text: this.$t('customEmoji'), icon: faAsterisk, isActive: true }, { name: 'people', - text: this.$t('people'), icon: faLaugh, isActive: false }, { name: 'animals_and_nature', - text: this.$t('animals-and-nature'), icon: faLeaf, isActive: false }, { name: 'food_and_drink', - text: this.$t('food-and-drink'), icon: faUtensils, isActive: false }, { name: 'activity', - text: this.$t('activity'), icon: faFutbol, isActive: false }, { name: 'travel_and_places', - text: this.$t('travel-and-places'), icon: faCity, isActive: false }, { name: 'objects', - text: this.$t('objects'), icon: faDice, isActive: false }, { name: 'symbols', - text: this.$t('symbols'), icon: faHeart, isActive: false }, { name: 'flags', - text: this.$t('flags'), icon: faFlag, isActive: false }] diff --git a/src/client/components/note.vue b/src/client/components/note.vue index db669309d3..b3126e0673 100644 --- a/src/client/components/note.vue +++ b/src/client/components/note.vue @@ -86,7 +86,7 @@ <script lang="ts"> import Vue from 'vue'; import { faBolt, faTimes, faBullhorn, faStar, faLink, faExternalLinkSquareAlt, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faQuoteRight, faInfoCircle } from '@fortawesome/free-solid-svg-icons'; -import { faCopy, faTrashAlt, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons'; +import { faCopy, faTrashAlt, faEdit, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons'; import { parse } from '../../mfm/parse'; import { sum, unique } from '../../prelude/array'; import i18n from '../i18n'; @@ -142,7 +142,7 @@ export default Vue.extend({ replies: [], showContent: false, hideThisNote: false, - faBolt, faTimes, faBullhorn, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan + faEdit, faBolt, faTimes, faBullhorn, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan }; }, @@ -460,6 +460,22 @@ export default Vue.extend({ }); }, + delEdit() { + this.$root.dialog({ + type: 'warning', + text: this.$t('deleteAndEditConfirm'), + showCancelButton: true + }).then(({ canceled }) => { + if (canceled) return; + + this.$root.api('notes/delete', { + noteId: this.appearNote.id + }); + + this.$root.post({ initialNote: this.appearNote, renote: this.appearNote.renote, reply: this.appearNote.reply }); + }); + }, + toggleFavorite(favorite: boolean) { this.$root.api(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', { noteId: this.appearNote.id @@ -548,6 +564,11 @@ export default Vue.extend({ ...(this.appearNote.userId == this.$store.state.i.id ? [ null, { + icon: faEdit, + text: this.$t('deleteAndEdit'), + action: this.delEdit + }, + { icon: faTrashAlt, text: this.$t('delete'), action: this.del diff --git a/src/client/components/post-form.vue b/src/client/components/post-form.vue index 7b84938d5a..d7bd105d80 100644 --- a/src/client/components/post-form.vue +++ b/src/client/components/post-form.vue @@ -300,6 +300,7 @@ export default Vue.extend({ }); } this.visibility = init.visibility; + this.localOnly = init.localOnly; this.quoteId = init.renote ? init.renote.id : null; } diff --git a/src/client/components/signin.vue b/src/client/components/signin.vue index 758bc59107..3d68289390 100755 --- a/src/client/components/signin.vue +++ b/src/client/components/signin.vue @@ -155,7 +155,7 @@ export default Vue.extend({ if (err === null) return; this.$root.dialog({ type: 'error', - text: this.$t('login-failed') + text: this.$t('signinFailed') }); this.signing = false; }); @@ -176,7 +176,7 @@ export default Vue.extend({ }).catch(() => { this.$root.dialog({ type: 'error', - text: this.$t('login-failed') + text: this.$t('signinFailed') }); this.challengeData = null; this.totpLogin = false; diff --git a/src/client/init.ts b/src/client/init.ts index 29eabfee4e..f1790ac4d9 100644 --- a/src/client/init.ts +++ b/src/client/init.ts @@ -18,7 +18,7 @@ import PostFormDialog from './components/post-form-dialog.vue'; import Dialog from './components/dialog.vue'; import Menu from './components/menu.vue'; import { router } from './router'; -import { applyTheme, lightTheme } from './theme'; +import { applyTheme, lightTheme, builtinThemes } from './theme'; Vue.use(Vuex); Vue.use(VueHotkey); @@ -163,6 +163,12 @@ os.init(async () => { isMobile: isMobile }; }, + watch: { + '$store.state.device.darkMode'() { + const themes = builtinThemes.concat(this.$store.state.device.themes); + applyTheme(themes.find(x => x.id === (this.$store.state.device.darkMode ? this.$store.state.device.darkTheme : this.$store.state.device.lightTheme))); + } + }, methods: { api: os.api, signout: os.signout, diff --git a/src/client/pages/about-misskey.vue b/src/client/pages/about-misskey.vue index 08821522b6..6b853d9e4a 100644 --- a/src/client/pages/about-misskey.vue +++ b/src/client/pages/about-misskey.vue @@ -14,6 +14,8 @@ <li><mk-link url="https://github.com/acid-chicken" class="at">@acid-chicken</mk-link></li> <li><mk-link url="https://github.com/tamaina" class="at">@tamaina</mk-link></li> <li><mk-link url="https://github.com/rinsuki" class="at">@rinsuki</mk-link></li> + <li><mk-link url="https://github.com/Xeltica" class="at">@Xeltica</mk-link></li> + <li><mk-link url="https://github.com/u1-liquid" class="at">@u1-liquid</mk-link></li> </ul> <div style="margin-top: 1em;">📦 {{ $t('misskeySource') }}</div> <mk-url url="https://github.com/syuilo/misskey"/> diff --git a/src/client/pages/index.welcome.setup.vue b/src/client/pages/index.welcome.setup.vue index a339ac0a28..6d08f5b5d4 100644 --- a/src/client/pages/index.welcome.setup.vue +++ b/src/client/pages/index.welcome.setup.vue @@ -61,7 +61,7 @@ export default Vue.extend({ this.$root.dialog({ type: 'error', - text: this.$t('some-error') + text: this.$t('error') }); }); } diff --git a/src/client/pages/instance/settings.vue b/src/client/pages/instance/settings.vue index d11f840d8f..f0a123f279 100644 --- a/src/client/pages/instance/settings.vue +++ b/src/client/pages/instance/settings.vue @@ -345,6 +345,20 @@ export default Vue.extend({ }, methods: { + invite() { + this.$root.api('admin/invite').then(x => { + this.$root.dialog({ + type: 'info', + text: x.code + }); + }).catch(e => { + this.$root.dialog({ + type: 'error', + text: e + }); + }); + }, + addPinUser() { this.$root.new(MkUserSelect, {}).$once('selected', user => { this.pinnedUsers = this.pinnedUsers.trim(); diff --git a/src/client/pages/messaging-room.form.vue b/src/client/pages/messaging-room.form.vue index 14659fb1c4..72e2632772 100644 --- a/src/client/pages/messaging-room.form.vue +++ b/src/client/pages/messaging-room.form.vue @@ -113,7 +113,7 @@ export default Vue.extend({ if (items[0].kind == 'file') { this.$root.dialog({ type: 'error', - text: this.$t('only-one-file-attached') + text: this.$t('onlyOneFileCanBeAttached') }); } } @@ -138,7 +138,7 @@ export default Vue.extend({ e.preventDefault(); this.$root.dialog({ type: 'error', - text: this.$t('only-one-file-attached') + text: this.$t('onlyOneFileCanBeAttached') }); return; } diff --git a/src/client/pages/messaging.vue b/src/client/pages/messaging.vue index 2179115dea..47d761d895 100644 --- a/src/client/pages/messaging.vue +++ b/src/client/pages/messaging.vue @@ -145,7 +145,7 @@ export default Vue.extend({ if (groups1.length === 0 && groups2.length === 0) { this.$root.dialog({ type: 'warning', - title: this.$t('noGroups'), + title: this.$t('youHaveNoGroups'), text: this.$t('joinOrCreateGroup'), }); return; diff --git a/src/client/pages/page-editor/els/page-editor.el.if.vue b/src/client/pages/page-editor/els/page-editor.el.if.vue index 3c545a7ddc..5e531a7ab5 100644 --- a/src/client/pages/page-editor/els/page-editor.el.if.vue +++ b/src/client/pages/page-editor/els/page-editor.el.if.vue @@ -69,7 +69,7 @@ export default Vue.extend({ async add() { const { canceled, result: type } = await this.$root.dialog({ type: null, - title: this.$t('choose-block'), + title: this.$t('_pages.chooseBlock'), select: { groupedItems: this.getPageBlockList() }, diff --git a/src/client/pages/page-editor/els/page-editor.el.section.vue b/src/client/pages/page-editor/els/page-editor.el.section.vue index d405ee1965..8de796e6d6 100644 --- a/src/client/pages/page-editor/els/page-editor.el.section.vue +++ b/src/client/pages/page-editor/els/page-editor.el.section.vue @@ -80,7 +80,7 @@ export default Vue.extend({ async add() { const { canceled, result: type } = await this.$root.dialog({ type: null, - title: this.$t('choose-block'), + title: this.$t('_pages.chooseBlock'), select: { groupedItems: this.getPageBlockList() }, diff --git a/src/client/pages/page-editor/page-editor.script-block.vue b/src/client/pages/page-editor/page-editor.script-block.vue index ae56803a39..f56e848c39 100644 --- a/src/client/pages/page-editor/page-editor.script-block.vue +++ b/src/client/pages/page-editor/page-editor.script-block.vue @@ -212,7 +212,7 @@ export default Vue.extend({ async changeType() { const { canceled, result: type } = await this.$root.dialog({ type: null, - title: this.$t('select-type'), + title: this.$t('_pages.selectType'), select: { groupedItems: this.getScriptBlockList(this.getExpectedType ? this.getExpectedType() : null) }, diff --git a/src/client/pages/page-editor/page-editor.vue b/src/client/pages/page-editor/page-editor.vue index 8b357584a5..6144b0bd9c 100644 --- a/src/client/pages/page-editor/page-editor.vue +++ b/src/client/pages/page-editor/page-editor.vue @@ -2,7 +2,7 @@ <div> <div class="gwbmwxkm _panel"> <header> - <div class="title"><fa :icon="faStickyNote"/> {{ readonly ? $t('readPage') : pageId ? $t('editPage') : $t('newPage') }}</div> + <div class="title"><fa :icon="faStickyNote"/> {{ readonly ? $t('_pages.readPage') : pageId ? $t('_pages.editPage') : $t('_pages.newPage') }}</div> <div class="buttons"> <button class="_button" @click="del()" v-if="!readonly"><fa :icon="faTrashAlt"/></button> <button class="_button" @click="() => showOptions = !showOptions"><fa :icon="faCog"/></button> @@ -11,37 +11,37 @@ </header> <section> - <router-link class="view" v-if="pageId" :to="`/@${ author.username }/pages/${ currentName }`"><fa :icon="faExternalLinkSquareAlt"/> {{ $t('view-page') }}</router-link> + <router-link class="view" v-if="pageId" :to="`/@${ author.username }/pages/${ currentName }`"><fa :icon="faExternalLinkSquareAlt"/> {{ $t('_pages.viewPage') }}</router-link> <mk-input v-model="title"> - <span>{{ $t('title') }}</span> + <span>{{ $t('_pages.title') }}</span> </mk-input> <template v-if="showOptions"> <mk-input v-model="summary"> - <span>{{ $t('summary') }}</span> + <span>{{ $t('_pages.summary') }}</span> </mk-input> <mk-input v-model="name"> <template #prefix>{{ url }}/@{{ author.username }}/pages/</template> - <span>{{ $t('url') }}</span> + <span>{{ $t('_pages.url') }}</span> </mk-input> - <mk-switch v-model="alignCenter">{{ $t('alignCenter') }}</mk-switch> + <mk-switch v-model="alignCenter">{{ $t('_pages.alignCenter') }}</mk-switch> <mk-select v-model="font"> - <template #label>{{ $t('font') }}</template> - <option value="serif">{{ $t('fontSerif') }}</option> - <option value="sans-serif">{{ $t('fontSansSerif') }}</option> + <template #label>{{ $t('_pages.font') }}</template> + <option value="serif">{{ $t('_pages.fontSerif') }}</option> + <option value="sans-serif">{{ $t('_pages.fontSansSerif') }}</option> </mk-select> - <mk-switch v-model="hideTitleWhenPinned">{{ $t('hide-title-when-pinned') }}</mk-switch> + <mk-switch v-model="hideTitleWhenPinned">{{ $t('_pages.hideTitleWhenPinned') }}</mk-switch> <div class="eyeCatch"> - <mk-button v-if="eyeCatchingImageId == null && !readonly" @click="setEyeCatchingImage()"><fa :icon="faPlus"/> {{ $t('set-eye-catching-image') }}</mk-button> + <mk-button v-if="eyeCatchingImageId == null && !readonly" @click="setEyeCatchingImage()"><fa :icon="faPlus"/> {{ $t('_pages.eyeCatchingImageSet') }}</mk-button> <div v-else-if="eyeCatchingImage"> <img :src="eyeCatchingImage.url" :alt="eyeCatchingImage.name"/> - <mk-button @click="removeEyeCatchingImage()" v-if="!readonly"><fa :icon="faTrashAlt"/> {{ $t('remove-eye-catching-image') }}</mk-button> + <mk-button @click="removeEyeCatchingImage()" v-if="!readonly"><fa :icon="faTrashAlt"/> {{ $t('_pages.eyeCatchingImageRemove') }}</mk-button> </div> </div> </template> @@ -53,7 +53,7 @@ </div> <mk-container :body-togglable="true"> - <template #header><fa :icon="faMagic"/> {{ $t('variables') }}</template> + <template #header><fa :icon="faMagic"/> {{ $t('_pages.variables') }}</template> <div class="qmuvgica"> <x-draggable tag="div" class="variables" v-show="variables.length > 0" :list="variables" handle=".drag-handle" :group="{ name: 'variables' }" animation="150" swap-threshold="0.5"> <x-variable v-for="variable in variables" @@ -70,22 +70,14 @@ </x-draggable> <mk-button @click="addVariable()" class="add" v-if="!readonly"><fa :icon="faPlus"/></mk-button> - - <x-info><span v-html="$t('variables-info')"></span><a @click="() => moreDetails = true" style="display:block;">{{ $t('more-details') }}</a></x-info> - - <template v-if="moreDetails"> - <x-info><span v-html="$t('variables-info2')"></span></x-info> - <x-info><span v-html="$t('variables-info3')"></span></x-info> - <x-info><span v-html="$t('variables-info4')"></span></x-info> - </template> </div> </mk-container> <mk-container :body-togglable="true" :expanded="false"> - <template #header><fa :icon="faCode"/> {{ $t('inspector') }}</template> + <template #header><fa :icon="faCode"/> {{ $t('_pages.inspector') }}</template> <div style="padding:0 32px 32px 32px;"> - <mk-textarea :value="JSON.stringify(content, null, 2)" readonly tall>{{ $t('content') }}</mk-textarea> - <mk-textarea :value="JSON.stringify(variables, null, 2)" readonly tall>{{ $t('variables') }}</mk-textarea> + <mk-textarea :value="JSON.stringify(content, null, 2)" readonly tall>{{ $t('_pages.content') }}</mk-textarea> + <mk-textarea :value="JSON.stringify(variables, null, 2)" readonly tall>{{ $t('_pages.variables') }}</mk-textarea> </div> </mk-container> </div> @@ -152,7 +144,6 @@ export default Vue.extend({ variables: [], aiScript: null, showOptions: false, - moreDetails: false, url, faPlus, faICursor, faSave, faStickyNote, faMagic, faCog, faTrashAlt, faExternalLinkSquareAlt, faCode }; @@ -243,14 +234,14 @@ export default Vue.extend({ if (err.info.param == 'name') { this.$root.dialog({ type: 'error', - title: this.$t('title-invalid-name'), - text: this.$t('text-invalid-name') + title: this.$t('_pages.invalidNameTitle'), + text: this.$t('_pages.invalidNameText') }); } } else if (err.code == 'NAME_ALREADY_EXISTS') { this.$root.dialog({ type: 'error', - text: this.$t('name-already-exists') + text: this.$t('_pages.nameAlreadyExists') }); } }; @@ -262,7 +253,7 @@ export default Vue.extend({ this.currentName = this.name.trim(); this.$root.dialog({ type: 'success', - text: this.$t('page-updated') + text: this.$t('_pages.updated') }); }).catch(onError); } else { @@ -272,7 +263,7 @@ export default Vue.extend({ this.currentName = this.name.trim(); this.$root.dialog({ type: 'success', - text: this.$t('page-created') + text: this.$t('_pages.created') }); this.$router.push(`/my/pages/edit/${this.pageId}`); }).catch(onError); @@ -282,7 +273,7 @@ export default Vue.extend({ del() { this.$root.dialog({ type: 'warning', - text: this.$t('are-you-sure-delete'), + text: this.$t('removeAreYouSure', { x: this.title.trim() }), showCancelButton: true }).then(({ canceled }) => { if (canceled) return; @@ -291,7 +282,7 @@ export default Vue.extend({ }).then(() => { this.$root.dialog({ type: 'success', - text: this.$t('page-deleted') + text: this.$t('_pages.deleted') }); this.$router.push(`/my/pages`); }); @@ -301,7 +292,7 @@ export default Vue.extend({ async add() { const { canceled, result: type } = await this.$root.dialog({ type: null, - title: this.$t('chooseBlock'), + title: this.$t('_pages.chooseBlock'), select: { groupedItems: this.getPageBlockList() }, @@ -315,7 +306,7 @@ export default Vue.extend({ async addVariable() { let { canceled, result: name } = await this.$root.dialog({ - title: this.$t('enterVariableName'), + title: this.$t('_pages.enterVariableName'), input: { type: 'text', }, @@ -328,7 +319,7 @@ export default Vue.extend({ if (this.aiScript.isUsedName(name)) { this.$root.dialog({ type: 'error', - text: this.$t('the-variable-name-is-already-used') + text: this.$t('_pages.variableNameIsAlreadyUsed') }); return; } @@ -348,7 +339,7 @@ export default Vue.extend({ getPageBlockList() { return [{ - label: this.$t('content-blocks'), + label: this.$t('_pages.contentBlocks'), items: [ { value: 'section', text: this.$t('_pages.blocks.section') }, { value: 'text', text: this.$t('_pages.blocks.text') }, @@ -356,7 +347,7 @@ export default Vue.extend({ { value: 'textarea', text: this.$t('_pages.blocks.textarea') }, ] }, { - label: this.$t('input-blocks'), + label: this.$t('_pages.inputBlocks'), items: [ { value: 'button', text: this.$t('_pages.blocks.button') }, { value: 'radioButton', text: this.$t('_pages.blocks.radioButton') }, @@ -367,7 +358,7 @@ export default Vue.extend({ { value: 'counter', text: this.$t('_pages.blocks.counter') } ] }, { - label: this.$t('special-blocks'), + label: this.$t('_pages.specialBlocks'), items: [ { value: 'if', text: this.$t('_pages.blocks.if') }, { value: 'post', text: this.$t('_pages.blocks.post') } diff --git a/src/client/pages/pages.vue b/src/client/pages/pages.vue index bee7d30a61..d993d0196e 100644 --- a/src/client/pages/pages.vue +++ b/src/client/pages/pages.vue @@ -1,7 +1,7 @@ <template> <div> <mk-container :body-togglable="true"> - <template #header><fa :icon="faEdit" fixed-width/>{{ $t('my-pages') }}</template> + <template #header><fa :icon="faEdit" fixed-width/>{{ $t('_pages.my') }}</template> <div class="rknalgpo my"> <mk-button class="new" @click="create()"><fa :icon="faPlus"/></mk-button> <mk-pagination :pagination="myPagesPagination" #default="{items}"> @@ -11,7 +11,7 @@ </mk-container> <mk-container :body-togglable="true"> - <template #header><fa :icon="faHeart" fixed-width/>{{ $t('liked-pages') }}</template> + <template #header><fa :icon="faHeart" fixed-width/>{{ $t('_pages.liked') }}</template> <div class="rknalgpo"> <mk-pagination :pagination="likedPagesPagination" #default="{items}"> <mk-page-preview v-for="like in items" class="ckltabjg" :page="like.page" :key="like.page.id"/> diff --git a/src/client/pages/preferences/index.vue b/src/client/pages/preferences/index.vue index 8ffb481c99..7cf6f4b4b2 100644 --- a/src/client/pages/preferences/index.vue +++ b/src/client/pages/preferences/index.vue @@ -8,7 +8,7 @@ <section class="_card"> <div class="_title"><fa :icon="faMusic"/> {{ $t('sounds') }}</div> <div class="_content"> - <mk-range v-model="sfxVolume" min="0" max="1" step="0.1"> + <mk-range v-model="sfxVolume" :min="0" :max="1" :step="0.1"> <fa slot="icon" :icon="volumeIcon"/> <span slot="title">{{ $t('volume') }}</span> </mk-range> diff --git a/src/client/pages/preferences/theme.vue b/src/client/pages/preferences/theme.vue index d406cadb01..488935a0cd 100644 --- a/src/client/pages/preferences/theme.vue +++ b/src/client/pages/preferences/theme.vue @@ -2,8 +2,30 @@ <section class="rfqxtzch _card"> <div class="_title"><fa :icon="faPalette"/> {{ $t('theme') }}</div> <div class="_content"> - <mk-select v-model="theme" :placeholder="$t('theme')"> - <template #label>{{ $t('theme') }}</template> + <div class="darkMode" :class="{ disabled: syncDeviceDarkMode }"> + <div class="toggleWrapper"> + <input type="checkbox" class="dn" id="dn" v-model="darkMode" :disabled="syncDeviceDarkMode"/> + <label for="dn" class="toggle"> + <span class="before">{{ $t('light') }}</span> + <span class="after">{{ $t('dark') }}</span> + <span class="toggle__handler"> + <span class="crater crater--1"></span> + <span class="crater crater--2"></span> + <span class="crater crater--3"></span> + </span> + <span class="star star--1"></span> + <span class="star star--2"></span> + <span class="star star--3"></span> + <span class="star star--4"></span> + <span class="star star--5"></span> + <span class="star star--6"></span> + </label> + </div> + </div> + </div> + <div class="_content"> + <mk-select v-model="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> @@ -11,6 +33,18 @@ <option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option> </optgroup> </mk-select> + <mk-select v-model="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> + </mk-select> + </div> + <div class="_content"> + <mk-switch v-model="syncDeviceDarkMode">{{ $t('syncDeviceDarkMode') }}</mk-switch> </div> <div class="_content"> <mk-button primary v-if="wallpaper == null" @click="setWallpaper">{{ $t('setWallpaper') }}</mk-button> @@ -25,9 +59,11 @@ import { faPalette } from '@fortawesome/free-solid-svg-icons'; import MkInput from '../../components/ui/input.vue'; import MkButton from '../../components/ui/button.vue'; import MkSelect from '../../components/ui/select.vue'; +import MkSwitch from '../../components/ui/switch.vue'; import i18n from '../../i18n'; import { Theme, builtinThemes, applyTheme } from '../../theme'; import { selectFile } from '../../scripts/select-file'; +import { isDeviceDarkmode } from '../../scripts/is-device-darkmode'; export default Vue.extend({ i18n, @@ -36,6 +72,7 @@ export default Vue.extend({ MkInput, MkButton, MkSelect, + MkSwitch, }, data() { @@ -62,15 +99,44 @@ export default Vue.extend({ return this.themes.filter(t => t.base == 'light' || t.kind == 'light'); }, - theme: { - get() { return this.$store.state.device.theme; }, - set(value) { this.$store.commit('device/set', { key: 'theme', value }); } + darkTheme: { + get() { return this.$store.state.device.darkTheme; }, + set(value) { this.$store.commit('device/set', { key: 'darkTheme', value }); } + }, + + lightTheme: { + get() { return this.$store.state.device.lightTheme; }, + set(value) { this.$store.commit('device/set', { key: 'lightTheme', value }); } + }, + + darkMode: { + get() { return this.$store.state.device.darkMode; }, + set(value) { this.$store.commit('device/set', { key: 'darkMode', value }); } + }, + + syncDeviceDarkMode: { + get() { return this.$store.state.device.syncDeviceDarkMode; }, + set(value) { this.$store.commit('device/set', { key: 'syncDeviceDarkMode', value }); } }, }, watch: { - theme() { - applyTheme(this.themes.find(x => x.id === this.theme)); + darkTheme() { + if (this.$store.state.device.darkMode) { + applyTheme(this.themes.find(x => x.id === this.darkTheme)); + } + }, + + lightTheme() { + if (!this.$store.state.device.darkMode) { + applyTheme(this.themes.find(x => x.id === this.lightTheme)); + } + }, + + syncDeviceDarkMode() { + if (this.$store.state.device.syncDeviceDarkMode) { + this.$store.commit('device/set', { key: 'darkMode', value: isDeviceDarkmode() }); + } }, wallpaper() { @@ -92,3 +158,230 @@ export default Vue.extend({ } }); </script> + +<style lang="scss" scoped> +.rfqxtzch { + > ._content { + > .darkMode { + position: relative; + padding: 32px 0; + + &.disabled { + opacity: 0.7; + + &, * { + cursor: not-allowed !important; + } + } + + .toggleWrapper { + position: absolute; + top: 50%; + left: 50%; + overflow: hidden; + padding: 0 200px; + transform: translate3d(-50%, -50%, 0); + + input { + position: absolute; + left: -99em; + } + } + + .toggle { + cursor: pointer; + display: inline-block; + position: relative; + width: 90px; + height: 50px; + background-color: #83D8FF; + border-radius: 90px - 6; + transition: background-color 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important; + + > .before, > .after { + position: absolute; + top: 15px; + font-size: 18px; + transition: color 1s ease; + } + + > .before { + left: -70px; + color: var(--accent); + } + + > .after { + right: -68px; + color: var(--fg); + } + } + + .toggle__handler { + display: inline-block; + position: relative; + z-index: 1; + top: 3px; + left: 3px; + width: 50px - 6; + height: 50px - 6; + background-color: #FFCF96; + border-radius: 50px; + box-shadow: 0 2px 6px rgba(0,0,0,.3); + transition: all 400ms cubic-bezier(0.68, -0.55, 0.265, 1.55) !important; + transform: rotate(-45deg); + + .crater { + position: absolute; + background-color: #E8CDA5; + opacity: 0; + transition: opacity 200ms ease-in-out !important; + border-radius: 100%; + } + + .crater--1 { + top: 18px; + left: 10px; + width: 4px; + height: 4px; + } + + .crater--2 { + top: 28px; + left: 22px; + width: 6px; + height: 6px; + } + + .crater--3 { + top: 10px; + left: 25px; + width: 8px; + height: 8px; + } + } + + .star { + position: absolute; + background-color: #ffffff; + transition: all 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important; + border-radius: 50%; + } + + .star--1 { + top: 10px; + left: 35px; + z-index: 0; + width: 30px; + height: 3px; + } + + .star--2 { + top: 18px; + left: 28px; + z-index: 1; + width: 30px; + height: 3px; + } + + .star--3 { + top: 27px; + left: 40px; + z-index: 0; + width: 30px; + height: 3px; + } + + .star--4, + .star--5, + .star--6 { + opacity: 0; + transition: all 300ms 0 cubic-bezier(0.445, 0.05, 0.55, 0.95) !important; + } + + .star--4 { + top: 16px; + left: 11px; + z-index: 0; + width: 2px; + height: 2px; + transform: translate3d(3px,0,0); + } + + .star--5 { + top: 32px; + left: 17px; + z-index: 0; + width: 3px; + height: 3px; + transform: translate3d(3px,0,0); + } + + .star--6 { + top: 36px; + left: 28px; + z-index: 0; + width: 2px; + height: 2px; + transform: translate3d(3px,0,0); + } + + input:checked { + + .toggle { + background-color: #749DD6; + + > .before { + color: var(--fg); + } + + > .after { + color: var(--accent); + } + + .toggle__handler { + background-color: #FFE5B5; + transform: translate3d(40px, 0, 0) rotate(0); + + .crater { opacity: 1; } + } + + .star--1 { + width: 2px; + height: 2px; + } + + .star--2 { + width: 4px; + height: 4px; + transform: translate3d(-5px, 0, 0); + } + + .star--3 { + width: 2px; + height: 2px; + transform: translate3d(-7px, 0, 0); + } + + .star--4, + .star--5, + .star--6 { + opacity: 1; + transform: translate3d(0,0,0); + } + + .star--4 { + transition: all 300ms 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important; + } + + .star--5 { + transition: all 300ms 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important; + } + + .star--6 { + transition: all 300ms 400ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important; + } + } + } + } + } +} +</style> diff --git a/src/client/scripts/is-device-darkmode.ts b/src/client/scripts/is-device-darkmode.ts new file mode 100644 index 0000000000..21c26823c7 --- /dev/null +++ b/src/client/scripts/is-device-darkmode.ts @@ -0,0 +1,3 @@ +export function isDeviceDarkmode() { + return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; +} diff --git a/src/client/store.ts b/src/client/store.ts index 29709096ee..35b932d624 100644 --- a/src/client/store.ts +++ b/src/client/store.ts @@ -35,7 +35,10 @@ const defaultDeviceSettings = { accounts: [], recentEmojis: [], themes: [], - theme: 'light', + darkTheme: '8c539dc1-0fab-4d47-9194-39c508e9bfe1', + lightTheme: '4eea646f-7afa-4645-83e9-83af0333cd37', + darkMode: false, + syncDeviceDarkMode: true, animation: true, animatedMfm: true, imageNewTab: false, diff --git a/src/client/theme.ts b/src/client/theme.ts index 576096cf70..2a6adbffcc 100644 --- a/src/client/theme.ts +++ b/src/client/theme.ts @@ -13,8 +13,8 @@ export const lightTheme: Theme = require('./themes/_light.json5'); export const darkTheme: Theme = require('./themes/_dark.json5'); export const builtinThemes = [ - lightTheme, - darkTheme, + require('./themes/white.json5'), + require('./themes/black.json5'), require('./themes/lavender.json5'), require('./themes/halloween.json5'), require('./themes/garden.json5'), @@ -44,7 +44,7 @@ export function applyTheme(theme: Theme, persist = true) { const _theme = JSON.parse(JSON.stringify(theme)); if (_theme.base) { - const base = [lightTheme, darkTheme].find(x => x.id == _theme.base); + const base = [lightTheme, darkTheme].find(x => x.id === _theme.base); _theme.props = Object.assign({}, base.props, _theme.props); } diff --git a/src/client/themes/black.json5 b/src/client/themes/black.json5 new file mode 100644 index 0000000000..22dfeda5a0 --- /dev/null +++ b/src/client/themes/black.json5 @@ -0,0 +1,14 @@ +{ + id: '8c539dc1-0fab-4d47-9194-39c508e9bfe1', + + name: 'Black', + author: 'syuilo', + desc: 'Basic dark theme', + + base: 'dark', + + props: { + divider: '#2d2d2d', + messageBg: '#1d1d1d', + }, +} diff --git a/src/client/themes/white.json5 b/src/client/themes/white.json5 new file mode 100644 index 0000000000..0e35a8232e --- /dev/null +++ b/src/client/themes/white.json5 @@ -0,0 +1,13 @@ +{ + id: '4eea646f-7afa-4645-83e9-83af0333cd37', + + name: 'White', + author: 'syuilo', + desc: 'Basic light theme', + + base: 'light', + + props: { + messageBg: '#dedede', + }, +} diff --git a/src/docs/pages.ja-JP.md b/src/docs/pages.ja-JP.md new file mode 100644 index 0000000000..3804c5a5c2 --- /dev/null +++ b/src/docs/pages.ja-JP.md @@ -0,0 +1,10 @@ +# Pages + +## 変数 +変数を使うことで動的なページを作成できます。テキスト内で <b>{ 変数名 }</b> と書くとそこに変数の値を埋め込めます。例えば <b>Hello { thing } world!</b> というテキストで、変数(thing)の値が <b>ai</b> だった場合、テキストは <b>Hello ai world!</b> になります。 + +変数の評価(値を算出すること)は上から下に行われるので、ある変数の中で自分より下の変数を参照することはできません。例えば上から <b>A、B、C</b> と3つの変数を定義したとき、<b>C</b>の中で<b>A</b>や<b>B</b>を参照することはできますが、<b>A</b>の中で<b>B</b>や<b>C</b>を参照することはできません。 + +ユーザーからの入力を受け取るには、ページに「ユーザー入力」ブロックを設置し、「変数名」に入力を格納したい変数名を設定します(変数は自動で作成されます)。その変数を使ってユーザー入力に応じた動作を行えます。 + +関数を使うと、値の算出処理を再利用可能な形にまとめることができます。関数を作るには、「関数」タイプの変数を作成します。関数にはスロット(引数)を設定することができ、スロットの値は関数内で変数として利用可能です。また、AiScript標準で関数を引数に取る関数(高階関数と呼ばれます)も存在します。関数は予め定義しておくほかに、このような高階関数のスロットに即席でセットすることもできます。 diff --git a/src/prelude/time.ts b/src/prelude/time.ts index a65366d74a..34e8b6b17c 100644 --- a/src/prelude/time.ts +++ b/src/prelude/time.ts @@ -5,7 +5,17 @@ const dateTimeIntervals = { }; export function dateUTC(time: number[]): Date { - return new Date(Date.UTC(...time)); + const d = time.length === 2 ? Date.UTC(time[0], time[1]) + : time.length === 3 ? Date.UTC(time[0], time[1], time[2]) + : time.length === 4 ? Date.UTC(time[0], time[1], time[2], time[3]) + : time.length === 5 ? Date.UTC(time[0], time[1], time[2], time[3], time[4]) + : time.length === 6 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5]) + : time.length === 7 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5], time[6]) + : null; + + if (!d) throw 'wrong number of arguments'; + + return new Date(d); } export function isTimeSame(a: Date, b: Date): boolean { diff --git a/src/server/api/endpoints/users/search-by-username-and-host.ts b/src/server/api/endpoints/users/search-by-username-and-host.ts index bc68f44094..b6ba09d0c0 100644 --- a/src/server/api/endpoints/users/search-by-username-and-host.ts +++ b/src/server/api/endpoints/users/search-by-username-and-host.ts @@ -72,6 +72,7 @@ export default define(meta, async (ps, me) => { q.andWhere('user.usernameLower like :username', { username: ps.username.toLowerCase() + '%' }) } + q.andWhere('user.updatedAt IS NOT NULL'); q.orderBy('user.updatedAt', 'DESC'); const users = await q.take(ps.limit!).skip(ps.offset).getMany(); @@ -82,6 +83,7 @@ export default define(meta, async (ps, me) => { .where('user.host IS NULL') .andWhere('user.isSuspended = FALSE') .andWhere('user.usernameLower like :username', { username: ps.username.toLowerCase() + '%' }) + .andWhere('user.updatedAt IS NOT NULL') .orderBy('user.updatedAt', 'DESC') .take(ps.limit!) .skip(ps.offset) @@ -92,6 +94,7 @@ export default define(meta, async (ps, me) => { .where('user.host IS NOT NULL') .andWhere('user.isSuspended = FALSE') .andWhere('user.usernameLower like :username', { username: ps.username.toLowerCase() + '%' }) + .andWhere('user.updatedAt IS NOT NULL') .orderBy('user.updatedAt', 'DESC') .take(ps.limit! - users.length) .getMany(); diff --git a/src/server/api/endpoints/users/search.ts b/src/server/api/endpoints/users/search.ts index c01f355d8c..7733b1a6bf 100644 --- a/src/server/api/endpoints/users/search.ts +++ b/src/server/api/endpoints/users/search.ts @@ -74,6 +74,7 @@ export default define(meta, async (ps, me) => { .where('user.host IS NULL') .andWhere('user.isSuspended = FALSE') .andWhere('user.usernameLower like :username', { username: ps.query.replace('@', '').toLowerCase() + '%' }) + .andWhere('user.updatedAt IS NOT NULL') .orderBy('user.updatedAt', 'DESC') .take(ps.limit!) .skip(ps.offset) @@ -84,6 +85,7 @@ export default define(meta, async (ps, me) => { .where('user.host IS NOT NULL') .andWhere('user.isSuspended = FALSE') .andWhere('user.usernameLower like :username', { username: ps.query.replace('@', '').toLowerCase() + '%' }) + .andWhere('user.updatedAt IS NOT NULL') .orderBy('user.updatedAt', 'DESC') .take(ps.limit! - users.length) .getMany(); |