* resolve #3023 * fix * fix * better description * widget * fix text * Update post-form.vue * Fix enter-file-name dialog title text * Fix type * On messaging room * Replace moment.js to original one * Fix formatDateTimeString
This commit is contained in:
parent
eb783f827c
commit
5343b005df
10 changed files with 163 additions and 18 deletions
|
@ -129,6 +129,7 @@ common:
|
|||
add-visible-user: "ユーザーを追加"
|
||||
cw-placeholder: "内容への注釈 (オプション)"
|
||||
username-prompt: "ユーザー名を入力してください"
|
||||
enter-file-name: "ファイル名を編集"
|
||||
|
||||
weekday-short:
|
||||
sunday: "日"
|
||||
|
@ -201,6 +202,11 @@ common:
|
|||
remember-note-visibility: "投稿の公開範囲を記憶する"
|
||||
web-search-engine: "ウェブ検索エンジン"
|
||||
web-search-engine-desc: "例: https://www.google.com/?#q={{query}}"
|
||||
paste: "ペースト"
|
||||
pasted-file-name: "ペーストされたファイル名のテンプレート"
|
||||
pasted-file-name-desc: "例: \"yyyy-MM-dd HH-mm-ss [{{number}}]\" → \"2018-03-20 21-30-24 1\""
|
||||
paste-dialog: "ペースト時にファイル名を編集"
|
||||
paste-dialog-desc: "ペースト時にファイル名を編集するダイアログを表示するようにします。"
|
||||
keep-cw: "CW保持"
|
||||
keep-cw-desc: "投稿にリプライする際、リプライ元の投稿にCWが設定されていたとき、デフォルトで同じCWを設定するようにします。"
|
||||
i-like-sushi: "私は(プリンよりむしろ)寿司が好き"
|
||||
|
|
|
@ -8,6 +8,7 @@ import { host, url } from '../../config';
|
|||
import i18n from '../../i18n';
|
||||
import { erase, unique } from '../../../../prelude/array';
|
||||
import extractMentions from '../../../../misc/extract-mentions';
|
||||
import { formatTimeString } from '../../../../misc/format-time-string';
|
||||
|
||||
export default (opts) => ({
|
||||
i18n: i18n(),
|
||||
|
@ -244,8 +245,8 @@ export default (opts) => ({
|
|||
for (const x of Array.from((this.$refs.file as any).files)) this.upload(x);
|
||||
},
|
||||
|
||||
upload(file) {
|
||||
(this.$refs.uploader as any).upload(file, this.$store.state.settings.uploadFolder);
|
||||
upload(file: File, name?: string) {
|
||||
(this.$refs.uploader as any).upload(file, this.$store.state.settings.uploadFolder, name);
|
||||
},
|
||||
|
||||
onChangeUploadings(uploads) {
|
||||
|
@ -334,10 +335,23 @@ export default (opts) => ({
|
|||
if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey) && this.canPost) this.post();
|
||||
},
|
||||
|
||||
async onPaste(e) {
|
||||
for (const item of Array.from(e.clipboardData.items)) {
|
||||
async onPaste(e: ClipboardEvent) {
|
||||
for (const { item, i } of Array.from(e.clipboardData.items).map((item, i) => ({item, i}))) {
|
||||
if (item.kind == 'file') {
|
||||
this.upload(item.getAsFile());
|
||||
const file = item.getAsFile();
|
||||
const lio = file.name.lastIndexOf('.');
|
||||
const ext = lio >= 0 ? file.name.slice(lio) : '';
|
||||
const formatted = `${formatTimeString(new Date(file.lastModified), this.$store.state.settings.pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`;
|
||||
const name = this.$store.state.settings.pasteDialog
|
||||
? await this.$root.dialog({
|
||||
title: this.$t('@.post-form.enter-file-name'),
|
||||
input: {
|
||||
default: formatted
|
||||
},
|
||||
allowEmpty: false
|
||||
}).then(({ canceled, result }) => canceled ? false : result)
|
||||
: formatted;
|
||||
if (name) this.upload(file, name);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
import Vue from 'vue';
|
||||
import i18n from '../../../i18n';
|
||||
import * as autosize from 'autosize';
|
||||
import { formatTimeString } from '../../../../../misc/format-time-string';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('common/views/components/messaging-room.form.vue'),
|
||||
|
@ -84,13 +85,26 @@ export default Vue.extend({
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
onPaste(e) {
|
||||
async onPaste(e: ClipboardEvent) {
|
||||
const data = e.clipboardData;
|
||||
const items = data.items;
|
||||
|
||||
if (items.length == 1) {
|
||||
if (items[0].kind == 'file') {
|
||||
this.upload(items[0].getAsFile());
|
||||
const file = items[0].getAsFile();
|
||||
const lio = file.name.lastIndexOf('.');
|
||||
const ext = lio >= 0 ? file.name.slice(lio) : '';
|
||||
const formatted = `${formatTimeString(new Date(file.lastModified), this.$store.state.settings.pastedFileName).replace(/{{number}}/g, '1')}${ext}`;
|
||||
const name = this.$store.state.settings.pasteDialog
|
||||
? await this.$root.dialog({
|
||||
title: this.$t('@.post-form.enter-file-name'),
|
||||
input: {
|
||||
default: formatted
|
||||
},
|
||||
allowEmpty: false
|
||||
}).then(({ canceled, result }) => canceled ? false : result)
|
||||
: formatted;
|
||||
if (name) this.upload(file, name);
|
||||
}
|
||||
} else {
|
||||
if (items[0].kind == 'file') {
|
||||
|
@ -157,8 +171,8 @@ export default Vue.extend({
|
|||
this.upload((this.$refs.file as any).files[0]);
|
||||
},
|
||||
|
||||
upload(file) {
|
||||
(this.$refs.uploader as any).upload(file, this.$store.state.settings.uploadFolder);
|
||||
upload(file: File, name?: string) {
|
||||
(this.$refs.uploader as any).upload(file, this.$store.state.settings.uploadFolder, name);
|
||||
},
|
||||
|
||||
onUploaded(file) {
|
||||
|
|
|
@ -140,7 +140,19 @@
|
|||
|
||||
<section>
|
||||
<header>{{ $t('@._settings.web-search-engine') }}</header>
|
||||
<ui-input v-model="webSearchEngine">{{ $t('@._settings.web-search-engine') }}<template #desc>{{ $t('@._settings.web-search-engine-desc') }}</template></ui-input>
|
||||
<ui-input v-model="webSearchEngine">{{ $t('@._settings.web-search-engine') }}
|
||||
<template #desc>{{ $t('@._settings.web-search-engine-desc') }}</template>
|
||||
</ui-input>
|
||||
</section>
|
||||
|
||||
<section v-if="!$root.isMobile">
|
||||
<header>{{ $t('@._settings.paste') }}</header>
|
||||
<ui-input v-model="pastedFileName">{{ $t('@._settings.pasted-file-name') }}
|
||||
<template #desc>{{ $t('@._settings.pasted-file-name-desc') }}</template>
|
||||
</ui-input>
|
||||
<ui-switch v-model="pasteDialog">{{ $t('@._settings.paste-dialog') }}
|
||||
<template #desc>{{ $t('@._settings.paste-dialog-desc') }}</template>
|
||||
</ui-switch>
|
||||
</section>
|
||||
</ui-card>
|
||||
|
||||
|
@ -412,6 +424,16 @@ export default Vue.extend({
|
|||
set(value) { this.$store.dispatch('settings/set', { key: 'webSearchEngine', value }); }
|
||||
},
|
||||
|
||||
pastedFileName: {
|
||||
get() { return this.$store.state.settings.pastedFileName; },
|
||||
set(value) { this.$store.dispatch('settings/set', { key: 'pastedFileName', value }); }
|
||||
},
|
||||
|
||||
pasteDialog: {
|
||||
get() { return this.$store.state.settings.pasteDialog; },
|
||||
set(value) { this.$store.dispatch('settings/set', { key: 'pasteDialog', value }); }
|
||||
},
|
||||
|
||||
showReplyTarget: {
|
||||
get() { return this.$store.state.settings.showReplyTarget; },
|
||||
set(value) { this.$store.dispatch('settings/set', { key: 'showReplyTarget', value }); }
|
||||
|
|
|
@ -46,7 +46,7 @@ export default Vue.extend({
|
|||
});
|
||||
},
|
||||
|
||||
upload(file: File, folder: any) {
|
||||
upload(file: File, folder: any, name?: string) {
|
||||
if (folder && typeof folder == 'object') folder = folder.id;
|
||||
|
||||
const id = Math.random();
|
||||
|
@ -61,7 +61,7 @@ export default Vue.extend({
|
|||
|
||||
const ctx = {
|
||||
id: id,
|
||||
name: file.name || 'untitled',
|
||||
name: name || file.name || 'untitled',
|
||||
progress: undefined,
|
||||
img: window.URL.createObjectURL(file)
|
||||
};
|
||||
|
@ -75,6 +75,7 @@ export default Vue.extend({
|
|||
data.append('file', file);
|
||||
|
||||
if (folder) data.append('folderId', folder);
|
||||
if (name) data.append('name', name);
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', apiUrl + '/drive/files/create', true);
|
||||
|
|
|
@ -38,6 +38,7 @@
|
|||
import define from '../../../common/define-widget';
|
||||
import i18n from '../../../i18n';
|
||||
import insertTextAtCursor from 'insert-text-at-cursor';
|
||||
import { formatTimeString } from '../../../../../misc/format-time-string';
|
||||
|
||||
export default define({
|
||||
name: 'post-form',
|
||||
|
@ -109,10 +110,23 @@ export default define({
|
|||
if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey) && !this.posting && this.text) this.post();
|
||||
},
|
||||
|
||||
onPaste(e) {
|
||||
for (const item of Array.from(e.clipboardData.items)) {
|
||||
async onPaste(e: ClipboardEvent) {
|
||||
for (const { item, i } of Array.from(e.clipboardData.items).map((item, i) => ({item, i}))) {
|
||||
if (item.kind == 'file') {
|
||||
this.upload(item.getAsFile());
|
||||
const file = item.getAsFile();
|
||||
const lio = file.name.lastIndexOf('.');
|
||||
const ext = lio >= 0 ? file.name.slice(lio) : '';
|
||||
const formatted = `${formatTimeString(new Date(file.lastModified), this.$store.state.settings.pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`;
|
||||
const name = this.$store.state.settings.pasteDialog
|
||||
? await this.$root.dialog({
|
||||
title: this.$t('@.post-form.enter-file-name'),
|
||||
input: {
|
||||
default: formatted
|
||||
},
|
||||
allowEmpty: false
|
||||
}).then(({ canceled, result }) => canceled ? false : result)
|
||||
: formatted;
|
||||
if (name) this.upload(file, name);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -121,8 +135,8 @@ export default define({
|
|||
for (const x of Array.from((this.$refs.file as any).files)) this.upload(x);
|
||||
},
|
||||
|
||||
upload(file) {
|
||||
(this.$refs.uploader as any).upload(file, this.$store.state.settings.uploadFolder);
|
||||
upload(file: File, name?: string) {
|
||||
(this.$refs.uploader as any).upload(file, this.$store.state.settings.uploadFolder, name);
|
||||
},
|
||||
|
||||
onDragover(e) {
|
||||
|
|
|
@ -39,6 +39,8 @@ const defaultSettings = {
|
|||
mobileHomeProfiles: {},
|
||||
deckProfiles: {},
|
||||
uploadFolder: null,
|
||||
pastedFileName: 'yyyy-MM-dd HH-mm-ss [{{number}}]',
|
||||
pasteDialog: false,
|
||||
};
|
||||
|
||||
const defaultDeviceSettings = {
|
||||
|
|
50
src/misc/format-time-string.ts
Normal file
50
src/misc/format-time-string.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
const defaultLocaleStringFormats: {[index: string]: string} = {
|
||||
'weekday': 'narrow',
|
||||
'era': 'narrow',
|
||||
'year': 'numeric',
|
||||
'month': 'numeric',
|
||||
'day': 'numeric',
|
||||
'hour': 'numeric',
|
||||
'minute': 'numeric',
|
||||
'second': 'numeric',
|
||||
'timeZoneName': 'short'
|
||||
};
|
||||
|
||||
function formatLocaleString(date: Date, format: string): string {
|
||||
return format.replace(/\{\{(\w+)(:(\w+))?\}\}/g, (match: string, kind: string, unused?, option?: string) => {
|
||||
if (['weekday', 'era', 'year', 'month', 'day', 'hour', 'minute', 'second', 'timeZoneName'].includes(kind)) {
|
||||
return date.toLocaleString(window.navigator.language, {[kind]: option ? option : defaultLocaleStringFormats[kind]});
|
||||
} else {
|
||||
return match;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function formatDateTimeString(date: Date, format: string): string {
|
||||
return format
|
||||
.replace(/yyyy/g, date.getFullYear().toString())
|
||||
.replace(/yy/g, date.getFullYear().toString().slice(-2))
|
||||
.replace(/MMMM/g, date.toLocaleString(window.navigator.language, { month: 'long'}))
|
||||
.replace(/MMM/g, date.toLocaleString(window.navigator.language, { month: 'short'}))
|
||||
.replace(/MM/g, (`0${date.getMonth() + 1}`).slice(-2))
|
||||
.replace(/M/g, (date.getMonth() + 1).toString())
|
||||
.replace(/dd/g, (`0${date.getDate()}`).slice(-2))
|
||||
.replace(/d/g, date.getDate().toString())
|
||||
.replace(/HH/g, (`0${date.getHours()}`).slice(-2))
|
||||
.replace(/H/g, date.getHours().toString())
|
||||
.replace(/hh/g, (`0${(date.getHours() % 12) || 12}`).slice(-2))
|
||||
.replace(/h/g, ((date.getHours() % 12) || 12).toString())
|
||||
.replace(/mm/g, (`0${date.getMinutes()}`).slice(-2))
|
||||
.replace(/m/g, date.getMinutes().toString())
|
||||
.replace(/ss/g, (`0${date.getSeconds()}`).slice(-2))
|
||||
.replace(/s/g, date.getSeconds().toString())
|
||||
.replace(/tt/g, date.getHours() >= 12 ? 'PM' : 'AM');
|
||||
}
|
||||
|
||||
export function formatTimeString(date: Date, format: string): string {
|
||||
return format.replace(/\[(([^\[]|\[\])*)\]|([yMdHhmst]{1,4})/g, (match: string, localeformat?: string, unused?, datetimeformat?: string) => {
|
||||
if (localeformat) return formatLocaleString(date, localeformat);
|
||||
if (datetimeformat) return formatDateTimeString(date, datetimeformat);
|
||||
return match;
|
||||
});
|
||||
}
|
|
@ -35,6 +35,14 @@ export const meta = {
|
|||
}
|
||||
},
|
||||
|
||||
name: {
|
||||
validator: $.optional.nullable.str,
|
||||
default: null as any,
|
||||
desc: {
|
||||
'ja-JP': 'ファイル名(拡張子があるなら含めて)'
|
||||
}
|
||||
},
|
||||
|
||||
isSensitive: {
|
||||
validator: $.optional.either($.bool, $.str),
|
||||
default: false,
|
||||
|
@ -72,7 +80,7 @@ export const meta = {
|
|||
|
||||
export default define(meta, async (ps, user, app, file, cleanup) => {
|
||||
// Get 'name' parameter
|
||||
let name = file.originalname;
|
||||
let name = ps.name || file.originalname;
|
||||
if (name !== undefined && name !== null) {
|
||||
name = name.trim();
|
||||
if (name.length === 0) {
|
||||
|
|
14
test/api.ts
14
test/api.ts
|
@ -474,6 +474,20 @@ describe('API', () => {
|
|||
assert.strictEqual(res.body.name, 'Lenna.png');
|
||||
}));
|
||||
|
||||
it('ファイルに名前を付けられる', async(async () => {
|
||||
const alice = await signup({ username: 'alice' });
|
||||
|
||||
const res = await assert.request(server)
|
||||
.post('/drive/files/create')
|
||||
.field('i', alice.token)
|
||||
.field('name', 'Belmond.png')
|
||||
.attach('file', fs.readFileSync(__dirname + '/resources/Lenna.png'), 'Lenna.png');
|
||||
|
||||
expect(res).have.status(200);
|
||||
expect(res.body).be.a('object');
|
||||
expect(res.body).have.property('name').eql('Belmond.png');
|
||||
}));
|
||||
|
||||
it('ファイル無しで怒られる', async(async () => {
|
||||
const alice = await signup({ username: 'alice' });
|
||||
|
||||
|
|
Loading…
Reference in a new issue