refactor: Composition APIへ移行 (#8138)

* components/drive-file-thumbnail.vue

* components/drive-select-dialog.vue

* components/drive-window.vue

* wip

* wip drive.file.vue, drive.vue

* fix prop

* wip(

* components/drive.folder.vue

* maybe ok

* ✌️

* fix variable

* FIX FOLDER VARIABLE

* components/emoji-picker-dialog.vue

* Hate `$emit`

* hate global property

* components/emoji-picker-window.vue

* components/emoji-picker.section.vue

* fix

* fixx

* wip components/emoji-picker.vue

* fix

* defineExpose

* ユニコード絵文字の型をもっといい感じに

* components/featured-photos.vue

* components/follow-button.vue

* forgot-password.vue

* forgot-password.vue

* 🎨

* fix
This commit is contained in:
tamaina 2022-01-18 23:06:16 +09:00 committed by GitHub
parent efb0ffc4ec
commit 7be09a4af9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 1608 additions and 1763 deletions

View File

@ -1,5 +1,5 @@
<template> <template>
<MkModal ref="modal" :prefer-type="'dialog'" :z-priority="'high'" @click="done(true)" @closed="$emit('closed')"> <MkModal ref="modal" :prefer-type="'dialog'" :z-priority="'high'" @click="done(true)" @closed="emit('closed')">
<div class="mk-dialog"> <div class="mk-dialog">
<div v-if="icon" class="icon"> <div v-if="icon" class="icon">
<i :class="icon"></i> <i :class="icon"></i>
@ -28,8 +28,8 @@
</template> </template>
</MkSelect> </MkSelect>
<div v-if="(showOkButton || showCancelButton) && !actions" class="buttons"> <div v-if="(showOkButton || showCancelButton) && !actions" class="buttons">
<MkButton v-if="showOkButton" inline primary :autofocus="!input && !select" @click="ok">{{ (showCancelButton || input || select) ? $ts.ok : $ts.gotIt }}</MkButton> <MkButton v-if="showOkButton" inline primary :autofocus="!input && !select" @click="ok">{{ (showCancelButton || input || select) ? i18n.locale.ok : i18n.locale.gotIt }}</MkButton>
<MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ $ts.cancel }}</MkButton> <MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ i18n.locale.cancel }}</MkButton>
</div> </div>
<div v-if="actions" class="buttons"> <div v-if="actions" class="buttons">
<MkButton v-for="action in actions" :key="action.text" inline :primary="action.primary" @click="() => { action.callback(); close(); }">{{ action.text }}</MkButton> <MkButton v-for="action in actions" :key="action.text" inline :primary="action.primary" @click="() => { action.callback(); close(); }">{{ action.text }}</MkButton>
@ -44,6 +44,7 @@ import MkModal from '@/components/ui/modal.vue';
import MkButton from '@/components/ui/button.vue'; import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue'; import MkInput from '@/components/form/input.vue';
import MkSelect from '@/components/form/select.vue'; import MkSelect from '@/components/form/select.vue';
import { i18n } from '@/i18n';
type Input = { type Input = {
type: HTMLInputElement['type']; type: HTMLInputElement['type'];

View File

@ -14,42 +14,24 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent } from 'vue'; import { computed } from 'vue';
import * as Misskey from 'misskey-js';
import ImgWithBlurhash from '@/components/img-with-blurhash.vue'; import ImgWithBlurhash from '@/components/img-with-blurhash.vue';
import { ColdDeviceStorage } from '@/store';
export default defineComponent({ const props = defineProps<{
components: { file: Misskey.entities.DriveFile;
ImgWithBlurhash fit: string;
}, }>();
props: {
file: {
type: Object,
required: true
},
fit: {
type: String,
required: false,
default: 'cover'
},
},
data() {
return {
isContextmenuShowing: false,
isDragging: false,
}; const is = computed(() => {
}, if (props.file.type.startsWith('image/')) return 'image';
computed: { if (props.file.type.startsWith('video/')) return 'video';
is(): 'image' | 'video' | 'midi' | 'audio' | 'csv' | 'pdf' | 'textfile' | 'archive' | 'unknown' { if (props.file.type === 'audio/midi') return 'midi';
if (this.file.type.startsWith('image/')) return 'image'; if (props.file.type.startsWith('audio/')) return 'audio';
if (this.file.type.startsWith('video/')) return 'video'; if (props.file.type.endsWith('/csv')) return 'csv';
if (this.file.type === 'audio/midi') return 'midi'; if (props.file.type.endsWith('/pdf')) return 'pdf';
if (this.file.type.startsWith('audio/')) return 'audio'; if (props.file.type.startsWith('text/')) return 'textfile';
if (this.file.type.endsWith('/csv')) return 'csv';
if (this.file.type.endsWith('/pdf')) return 'pdf';
if (this.file.type.startsWith('text/')) return 'textfile';
if ([ if ([
"application/zip", "application/zip",
"application/x-cpio", "application/x-cpio",
@ -60,25 +42,14 @@ export default defineComponent({
"application/x-tar", "application/x-tar",
"application/gzip", "application/gzip",
"application/x-7z-compressed" "application/x-7z-compressed"
].some(e => e === this.file.type)) return 'archive'; ].some(e => e === props.file.type)) return 'archive';
return 'unknown'; return 'unknown';
}, });
isThumbnailAvailable(): boolean {
return this.file.thumbnailUrl const isThumbnailAvailable = computed(() => {
? (this.is === 'image' || this.is === 'video') return props.file.thumbnailUrl
? (is.value === 'image' as const || is.value === 'video')
: false; : false;
},
},
mounted() {
const audioTag = this.$refs.volumectrl as HTMLAudioElement;
if (audioTag) audioTag.volume = ColdDeviceStorage.get('mediaVolume');
},
methods: {
volumechange() {
const audioTag = this.$refs.volumectrl as HTMLAudioElement;
ColdDeviceStorage.set('mediaVolume', audioTag.volume);
}
}
}); });
</script> </script>

View File

@ -7,64 +7,51 @@
@click="cancel()" @click="cancel()"
@close="cancel()" @close="cancel()"
@ok="ok()" @ok="ok()"
@closed="$emit('closed')" @closed="emit('closed')"
> >
<template #header> <template #header>
{{ multiple ? ((type === 'file') ? $ts.selectFiles : $ts.selectFolders) : ((type === 'file') ? $ts.selectFile : $ts.selectFolder) }} {{ multiple ? ((type === 'file') ? i18n.locale.selectFiles : i18n.locale.selectFolders) : ((type === 'file') ? i18n.locale.selectFile : i18n.locale.selectFolder) }}
<span v-if="selected.length > 0" style="margin-left: 8px; opacity: 0.5;">({{ number(selected.length) }})</span> <span v-if="selected.length > 0" style="margin-left: 8px; opacity: 0.5;">({{ number(selected.length) }})</span>
</template> </template>
<XDrive :multiple="multiple" :select="type" @changeSelection="onChangeSelection" @selected="ok()"/> <XDrive :multiple="multiple" :select="type" @changeSelection="onChangeSelection" @selected="ok()"/>
</XModalWindow> </XModalWindow>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent } from 'vue'; import { ref } from 'vue';
import * as Misskey from 'misskey-js';
import XDrive from './drive.vue'; import XDrive from './drive.vue';
import XModalWindow from '@/components/ui/modal-window.vue'; import XModalWindow from '@/components/ui/modal-window.vue';
import number from '@/filters/number'; import number from '@/filters/number';
import { i18n } from '@/i18n';
export default defineComponent({ withDefaults(defineProps<{
components: { type?: 'file' | 'folder';
XDrive, multiple: boolean;
XModalWindow, }>(), {
}, type: 'file',
props: {
type: {
type: String,
required: false,
default: 'file'
},
multiple: {
type: Boolean,
default: false
}
},
emits: ['done', 'closed'],
data() {
return {
selected: []
};
},
methods: {
ok() {
this.$emit('done', this.selected);
this.$refs.dialog.close();
},
cancel() {
this.$emit('done');
this.$refs.dialog.close();
},
onChangeSelection(xs) {
this.selected = xs;
},
number
}
}); });
const emit = defineEmits<{
(e: 'done', r?: Misskey.entities.DriveFile[]): void;
(e: 'closed'): void;
}>();
const dialog = ref<InstanceType<typeof XModalWindow>>();
const selected = ref<Misskey.entities.DriveFile[]>([]);
function ok() {
emit('done', selected.value);
dialog.value?.close();
}
function cancel() {
emit('done');
dialog.value?.close();
}
function onChangeSelection(files: Misskey.entities.DriveFile[]) {
selected.value = files;
}
</script> </script>

View File

@ -3,42 +3,27 @@
:initial-width="800" :initial-width="800"
:initial-height="500" :initial-height="500"
:can-resize="true" :can-resize="true"
@closed="$emit('closed')" @closed="emit('closed')"
> >
<template #header> <template #header>
{{ $ts.drive }} {{ i18n.locale.drive }}
</template> </template>
<XDrive :initial-folder="initialFolder"/> <XDrive :initial-folder="initialFolder"/>
</XWindow> </XWindow>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent } from 'vue'; import { } from 'vue';
import * as Misskey from 'misskey-js';
import XDrive from './drive.vue'; import XDrive from './drive.vue';
import XWindow from '@/components/ui/window.vue'; import XWindow from '@/components/ui/window.vue';
import { i18n } from '@/i18n';
export default defineComponent({ defineProps<{
components: { initialFolder?: Misskey.entities.DriveFolder;
XDrive, }>();
XWindow,
},
props: { const emit = defineEmits<{
initialFolder: { (e: 'closed'): void;
type: Object, }>();
required: false
},
},
emits: ['closed'],
data() {
return {
};
},
methods: {
}
});
</script> </script>

View File

@ -8,17 +8,17 @@
@dragstart="onDragstart" @dragstart="onDragstart"
@dragend="onDragend" @dragend="onDragend"
> >
<div v-if="$i.avatarId == file.id" class="label"> <div v-if="$i?.avatarId == file.id" class="label">
<img src="/client-assets/label.svg"/> <img src="/client-assets/label.svg"/>
<p>{{ $ts.avatar }}</p> <p>{{ i18n.locale.avatar }}</p>
</div> </div>
<div v-if="$i.bannerId == file.id" class="label"> <div v-if="$i?.bannerId == file.id" class="label">
<img src="/client-assets/label.svg"/> <img src="/client-assets/label.svg"/>
<p>{{ $ts.banner }}</p> <p>{{ i18n.locale.banner }}</p>
</div> </div>
<div v-if="file.isSensitive" class="label red"> <div v-if="file.isSensitive" class="label red">
<img src="/client-assets/label-red.svg"/> <img src="/client-assets/label-red.svg"/>
<p>{{ $ts.nsfw }}</p> <p>{{ i18n.locale.nsfw }}</p>
</div> </div>
<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/> <MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
@ -30,179 +30,155 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent } from 'vue'; import { computed, ref } from 'vue';
import * as Misskey from 'misskey-js';
import copyToClipboard from '@/scripts/copy-to-clipboard'; import copyToClipboard from '@/scripts/copy-to-clipboard';
import MkDriveFileThumbnail from './drive-file-thumbnail.vue'; import MkDriveFileThumbnail from './drive-file-thumbnail.vue';
import bytes from '@/filters/bytes'; import bytes from '@/filters/bytes';
import * as os from '@/os'; import * as os from '@/os';
import { i18n } from '@/i18n';
import { $i } from '@/account';
export default defineComponent({ const props = withDefaults(defineProps<{
components: { file: Misskey.entities.DriveFile;
MkDriveFileThumbnail isSelected?: boolean;
}, selectMode?: boolean;
}>(), {
isSelected: false,
selectMode: false,
});
props: { const emit = defineEmits<{
file: { (e: 'chosen', r: Misskey.entities.DriveFile): void;
type: Object, (e: 'dragstart'): void;
required: true, (e: 'dragend'): void;
}, }>();
isSelected: {
type: Boolean,
required: false,
default: false,
},
selectMode: {
type: Boolean,
required: false,
default: false,
}
},
emits: ['chosen'], const isDragging = ref(false);
data() { const title = computed(() => `${props.file.name}\n${props.file.type} ${bytes(props.file.size)}`);
return {
isDragging: false
};
},
computed: { function getMenu() {
// TODO: parent
browser(): any {
return this.$parent;
},
title(): string {
return `${this.file.name}\n${this.file.type} ${bytes(this.file.size)}`;
}
},
methods: {
getMenu() {
return [{ return [{
text: this.$ts.rename, text: i18n.locale.rename,
icon: 'fas fa-i-cursor', icon: 'fas fa-i-cursor',
action: this.rename action: rename
}, { }, {
text: this.file.isSensitive ? this.$ts.unmarkAsSensitive : this.$ts.markAsSensitive, text: props.file.isSensitive ? i18n.locale.unmarkAsSensitive : i18n.locale.markAsSensitive,
icon: this.file.isSensitive ? 'fas fa-eye' : 'fas fa-eye-slash', icon: props.file.isSensitive ? 'fas fa-eye' : 'fas fa-eye-slash',
action: this.toggleSensitive action: toggleSensitive
}, { }, {
text: this.$ts.describeFile, text: i18n.locale.describeFile,
icon: 'fas fa-i-cursor', icon: 'fas fa-i-cursor',
action: this.describe action: describe
}, null, { }, null, {
text: this.$ts.copyUrl, text: i18n.locale.copyUrl,
icon: 'fas fa-link', icon: 'fas fa-link',
action: this.copyUrl action: copyUrl
}, { }, {
type: 'a', type: 'a',
href: this.file.url, href: props.file.url,
target: '_blank', target: '_blank',
text: this.$ts.download, text: i18n.locale.download,
icon: 'fas fa-download', icon: 'fas fa-download',
download: this.file.name download: props.file.name
}, null, { }, null, {
text: this.$ts.delete, text: i18n.locale.delete,
icon: 'fas fa-trash-alt', icon: 'fas fa-trash-alt',
danger: true, danger: true,
action: this.deleteFile action: deleteFile
}]; }];
}, }
onClick(ev) { function onClick(ev: MouseEvent) {
if (this.selectMode) { if (props.selectMode) {
this.$emit('chosen', this.file); emit('chosen', props.file);
} else { } else {
os.popupMenu(this.getMenu(), ev.currentTarget || ev.target); os.popupMenu(getMenu(), (ev.currentTarget || ev.target || undefined) as HTMLElement | undefined);
} }
}, }
onContextmenu(e) { function onContextmenu(e: MouseEvent) {
os.contextMenu(this.getMenu(), e); os.contextMenu(getMenu(), e);
}, }
onDragstart(e) { function onDragstart(e: DragEvent) {
if (e.dataTransfer) {
e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FILE_, JSON.stringify(this.file)); e.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FILE_, JSON.stringify(props.file));
this.isDragging = true; }
isDragging.value = true;
// emit('dragstart');
// (=) }
this.browser.isDragSource = true;
},
onDragend(e) { function onDragend() {
this.isDragging = false; isDragging.value = false;
this.browser.isDragSource = false; emit('dragend');
}, }
rename() { function rename() {
os.inputText({ os.inputText({
title: this.$ts.renameFile, title: i18n.locale.renameFile,
placeholder: this.$ts.inputNewFileName, placeholder: i18n.locale.inputNewFileName,
default: this.file.name, default: props.file.name,
allowEmpty: false
}).then(({ canceled, result: name }) => { }).then(({ canceled, result: name }) => {
if (canceled) return; if (canceled) return;
os.api('drive/files/update', { os.api('drive/files/update', {
fileId: this.file.id, fileId: props.file.id,
name: name name: name
}); });
}); });
}, }
describe() { function describe() {
os.popup(import('@/components/media-caption.vue'), { os.popup(import('@/components/media-caption.vue'), {
title: this.$ts.describeFile, title: i18n.locale.describeFile,
input: { input: {
placeholder: this.$ts.inputNewDescription, placeholder: i18n.locale.inputNewDescription,
default: this.file.comment !== null ? this.file.comment : '', default: props.file.comment !== null ? props.file.comment : '',
}, },
image: this.file image: props.file
}, { }, {
done: result => { done: result => {
if (!result || result.canceled) return; if (!result || result.canceled) return;
let comment = result.result; let comment = result.result;
os.api('drive/files/update', { os.api('drive/files/update', {
fileId: this.file.id, fileId: props.file.id,
comment: comment.length == 0 ? null : comment comment: comment.length == 0 ? null : comment
}); });
} }
}, 'closed'); }, 'closed');
}, }
toggleSensitive() { function toggleSensitive() {
os.api('drive/files/update', { os.api('drive/files/update', {
fileId: this.file.id, fileId: props.file.id,
isSensitive: !this.file.isSensitive isSensitive: !props.file.isSensitive
}); });
}, }
copyUrl() { function copyUrl() {
copyToClipboard(this.file.url); copyToClipboard(props.file.url);
os.success(); os.success();
}, }
/*
addApp() { function addApp() {
alert('not implemented yet'); alert('not implemented yet');
}, }
*/
async deleteFile() { async function deleteFile() {
const { canceled } = await os.confirm({ const { canceled } = await os.confirm({
type: 'warning', type: 'warning',
text: this.$t('driveFileDeleteConfirm', { name: this.file.name }), text: i18n.t('driveFileDeleteConfirm', { name: props.file.name }),
}); });
if (canceled) return; if (canceled) return;
os.api('drive/files/delete', { os.api('drive/files/delete', {
fileId: this.file.id fileId: props.file.id
}); });
}, }
bytes
}
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -19,74 +19,66 @@
<template v-if="!hover"><i class="fas fa-folder fa-fw"></i></template> <template v-if="!hover"><i class="fas fa-folder fa-fw"></i></template>
{{ folder.name }} {{ folder.name }}
</p> </p>
<p v-if="$store.state.uploadFolder == folder.id" class="upload"> <p v-if="defaultStore.state.uploadFolder == folder.id" class="upload">
{{ $ts.uploadFolder }} {{ i18n.locale.uploadFolder }}
</p> </p>
<button v-if="selectMode" class="checkbox _button" :class="{ checked: isSelected }" @click.prevent.stop="checkboxClicked"></button> <button v-if="selectMode" class="checkbox _button" :class="{ checked: isSelected }" @click.prevent.stop="checkboxClicked"></button>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent } from 'vue'; import { computed, ref } from 'vue';
import * as Misskey from 'misskey-js';
import * as os from '@/os'; import * as os from '@/os';
import { i18n } from '@/i18n';
import { defaultStore } from '@/store';
export default defineComponent({ const props = withDefaults(defineProps<{
props: { folder: Misskey.entities.DriveFolder;
folder: { isSelected?: boolean;
type: Object, selectMode?: boolean;
required: true, }>(), {
}, isSelected: false,
isSelected: { selectMode: false,
type: Boolean, });
required: false,
default: false,
},
selectMode: {
type: Boolean,
required: false,
default: false,
}
},
emits: ['chosen'], const emit = defineEmits<{
(e: 'chosen', v: Misskey.entities.DriveFolder): void;
(e: 'move', v: Misskey.entities.DriveFolder): void;
(e: 'upload', file: File, folder: Misskey.entities.DriveFolder);
(e: 'removeFile', v: Misskey.entities.DriveFile['id']): void;
(e: 'removeFolder', v: Misskey.entities.DriveFolder['id']): void;
(e: 'dragstart'): void;
(e: 'dragend'): void;
}>();
data() { const hover = ref(false);
return { const draghover = ref(false);
hover: false, const isDragging = ref(false);
draghover: false,
isDragging: false,
};
},
computed: { const title = computed(() => props.folder.name);
browser(): any {
return this.$parent;
},
title(): string {
return this.folder.name;
}
},
methods: { function checkboxClicked(e) {
checkboxClicked(e) { emit('chosen', props.folder);
this.$emit('chosen', this.folder); }
},
onClick() { function onClick() {
this.browser.move(this.folder); emit('move', props.folder);
}, }
onMouseover() { function onMouseover() {
this.hover = true; hover.value = true;
}, }
onMouseout() { function onMouseout() {
this.hover = false hover.value = false
}, }
function onDragover(e: DragEvent) {
if (!e.dataTransfer) return;
onDragover(e) {
// //
if (this.isDragging) { if (isDragging.value) {
// //
e.dataTransfer.dropEffect = 'none'; e.dataTransfer.dropEffect = 'none';
return; return;
@ -101,23 +93,25 @@ export default defineComponent({
} else { } else {
e.dataTransfer.dropEffect = 'none'; e.dataTransfer.dropEffect = 'none';
} }
}, }
onDragenter() { function onDragenter() {
if (!this.isDragging) this.draghover = true; if (!isDragging.value) draghover.value = true;
}, }
onDragleave() { function onDragleave() {
this.draghover = false; draghover.value = false;
}, }
onDrop(e) { function onDrop(e: DragEvent) {
this.draghover = false; draghover.value = false;
if (!e.dataTransfer) return;
// //
if (e.dataTransfer.files.length > 0) { if (e.dataTransfer.files.length > 0) {
for (const file of Array.from(e.dataTransfer.files)) { for (const file of Array.from(e.dataTransfer.files)) {
this.browser.upload(file, this.folder); emit('upload', file, props.folder);
} }
return; return;
} }
@ -126,10 +120,10 @@ export default defineComponent({
const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_); const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
if (driveFile != null && driveFile != '') { if (driveFile != null && driveFile != '') {
const file = JSON.parse(driveFile); const file = JSON.parse(driveFile);
this.browser.removeFile(file.id); emit('removeFile', file.id);
os.api('drive/files/update', { os.api('drive/files/update', {
fileId: file.id, fileId: file.id,
folderId: this.folder.id folderId: props.folder.id
}); });
} }
//#endregion //#endregion
@ -140,122 +134,118 @@ export default defineComponent({
const folder = JSON.parse(driveFolder); const folder = JSON.parse(driveFolder);
// reject // reject
if (folder.id == this.folder.id) return; if (folder.id == props.folder.id) return;
this.browser.removeFolder(folder.id); emit('removeFolder', folder.id);
os.api('drive/folders/update', { os.api('drive/folders/update', {
folderId: folder.id, folderId: folder.id,
parentId: this.folder.id parentId: props.folder.id
}).then(() => { }).then(() => {
// noop // noop
}).catch(err => { }).catch(err => {
switch (err) { switch (err) {
case 'detected-circular-definition': case 'detected-circular-definition':
os.alert({ os.alert({
title: this.$ts.unableToProcess, title: i18n.locale.unableToProcess,
text: this.$ts.circularReferenceFolder text: i18n.locale.circularReferenceFolder
}); });
break; break;
default: default:
os.alert({ os.alert({
type: 'error', type: 'error',
text: this.$ts.somethingHappened text: i18n.locale.somethingHappened
}); });
} }
}); });
} }
//#endregion //#endregion
}, }
function onDragstart(e: DragEvent) {
if (!e.dataTransfer) return;
onDragstart(e) {
e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FOLDER_, JSON.stringify(this.folder)); e.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FOLDER_, JSON.stringify(props.folder));
this.isDragging = true; isDragging.value = true;
// //
// (=) // (=)
this.browser.isDragSource = true; emit('dragstart');
}, }
onDragend() { function onDragend() {
this.isDragging = false; isDragging.value = false;
this.browser.isDragSource = false; emit('dragend');
}, }
go() { function go() {
this.browser.move(this.folder.id); emit('move', props.folder.id);
}, }
newWindow() { function rename() {
this.browser.newWindow(this.folder);
},
rename() {
os.inputText({ os.inputText({
title: this.$ts.renameFolder, title: i18n.locale.renameFolder,
placeholder: this.$ts.inputNewFolderName, placeholder: i18n.locale.inputNewFolderName,
default: this.folder.name default: props.folder.name
}).then(({ canceled, result: name }) => { }).then(({ canceled, result: name }) => {
if (canceled) return; if (canceled) return;
os.api('drive/folders/update', { os.api('drive/folders/update', {
folderId: this.folder.id, folderId: props.folder.id,
name: name name: name
}); });
}); });
}, }
deleteFolder() { function deleteFolder() {
os.api('drive/folders/delete', { os.api('drive/folders/delete', {
folderId: this.folder.id folderId: props.folder.id
}).then(() => { }).then(() => {
if (this.$store.state.uploadFolder === this.folder.id) { if (defaultStore.state.uploadFolder === props.folder.id) {
this.$store.set('uploadFolder', null); defaultStore.set('uploadFolder', null);
} }
}).catch(err => { }).catch(err => {
switch(err.id) { switch(err.id) {
case 'b0fc8a17-963c-405d-bfbc-859a487295e1': case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
os.alert({ os.alert({
type: 'error', type: 'error',
title: this.$ts.unableToDelete, title: i18n.locale.unableToDelete,
text: this.$ts.hasChildFilesOrFolders text: i18n.locale.hasChildFilesOrFolders
}); });
break; break;
default: default:
os.alert({ os.alert({
type: 'error', type: 'error',
text: this.$ts.unableToDelete text: i18n.locale.unableToDelete
}); });
} }
}); });
}, }
setAsUploadFolder() { function setAsUploadFolder() {
this.$store.set('uploadFolder', this.folder.id); defaultStore.set('uploadFolder', props.folder.id);
}, }
onContextmenu(e) { function onContextmenu(e) {
os.contextMenu([{ os.contextMenu([{
text: this.$ts.openInWindow, text: i18n.locale.openInWindow,
icon: 'fas fa-window-restore', icon: 'fas fa-window-restore',
action: () => { action: () => {
os.popup(import('./drive-window.vue'), { os.popup(import('./drive-window.vue'), {
initialFolder: this.folder initialFolder: props.folder
}, { }, {
}, 'closed'); }, 'closed');
} }
}, null, { }, null, {
text: this.$ts.rename, text: i18n.locale.rename,
icon: 'fas fa-i-cursor', icon: 'fas fa-i-cursor',
action: this.rename action: rename,
}, null, { }, null, {
text: this.$ts.delete, text: i18n.locale.delete,
icon: 'fas fa-trash-alt', icon: 'fas fa-trash-alt',
danger: true, danger: true,
action: this.deleteFolder action: deleteFolder,
}], e); }], e);
}, }
}
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -8,51 +8,48 @@
@drop.stop="onDrop" @drop.stop="onDrop"
> >
<i v-if="folder == null" class="fas fa-cloud"></i> <i v-if="folder == null" class="fas fa-cloud"></i>
<span>{{ folder == null ? $ts.drive : folder.name }}</span> <span>{{ folder == null ? i18n.locale.drive : folder.name }}</span>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent } from 'vue'; import { ref } from 'vue';
import * as Misskey from 'misskey-js';
import * as os from '@/os'; import * as os from '@/os';
import { i18n } from '@/i18n';
export default defineComponent({ const props = defineProps<{
props: { folder?: Misskey.entities.DriveFolder;
folder: { parentFolder: Misskey.entities.DriveFolder | null;
type: Object, }>();
required: false,
}
},
data() { const emit = defineEmits<{
return { (e: 'move', v?: Misskey.entities.DriveFolder): void;
hover: false, (e: 'upload', file: File, folder?: Misskey.entities.DriveFolder | null): void;
draghover: false, (e: 'removeFile', v: Misskey.entities.DriveFile['id']): void;
}; (e: 'removeFolder', v: Misskey.entities.DriveFolder['id']): void;
}, }>();
computed: { const hover = ref(false);
browser(): any { const draghover = ref(false);
return this.$parent;
}
},
methods: { function onClick() {
onClick() { emit('move', props.folder);
this.browser.move(this.folder); }
},
onMouseover() { function onMouseover() {
this.hover = true; hover.value = true;
}, }
onMouseout() { function onMouseout() {
this.hover = false; hover.value = false;
}, }
function onDragover(e: DragEvent) {
if (!e.dataTransfer) return;
onDragover(e) {
// //
if (this.folder == null && this.browser.folder == null) { if (props.folder == null && props.parentFolder == null) {
e.dataTransfer.dropEffect = 'none'; e.dataTransfer.dropEffect = 'none';
} }
@ -67,23 +64,25 @@ export default defineComponent({
} }
return false; return false;
}, }
onDragenter() { function onDragenter() {
if (this.folder || this.browser.folder) this.draghover = true; if (props.folder || props.parentFolder) draghover.value = true;
}, }
onDragleave() { function onDragleave() {
if (this.folder || this.browser.folder) this.draghover = false; if (props.folder || props.parentFolder) draghover.value = false;
}, }
onDrop(e) { function onDrop(e: DragEvent) {
this.draghover = false; draghover.value = false;
if (!e.dataTransfer) return;
// //
if (e.dataTransfer.files.length > 0) { if (e.dataTransfer.files.length > 0) {
for (const file of Array.from(e.dataTransfer.files)) { for (const file of Array.from(e.dataTransfer.files)) {
this.browser.upload(file, this.folder); emit('upload', file, props.folder);
} }
return; return;
} }
@ -92,10 +91,10 @@ export default defineComponent({
const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_); const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
if (driveFile != null && driveFile != '') { if (driveFile != null && driveFile != '') {
const file = JSON.parse(driveFile); const file = JSON.parse(driveFile);
this.browser.removeFile(file.id); emit('removeFile', file.id);
os.api('drive/files/update', { os.api('drive/files/update', {
fileId: file.id, fileId: file.id,
folderId: this.folder ? this.folder.id : null folderId: props.folder ? props.folder.id : null
}); });
} }
//#endregion //#endregion
@ -105,17 +104,15 @@ export default defineComponent({
if (driveFolder != null && driveFolder != '') { if (driveFolder != null && driveFolder != '') {
const folder = JSON.parse(driveFolder); const folder = JSON.parse(driveFolder);
// reject // reject
if (this.folder && folder.id == this.folder.id) return; if (props.folder && folder.id == props.folder.id) return;
this.browser.removeFolder(folder.id); emit('removeFolder', folder.id);
os.api('drive/folders/update', { os.api('drive/folders/update', {
folderId: folder.id, folderId: folder.id,
parentId: this.folder ? this.folder.id : null parentId: props.folder ? props.folder.id : null
}); });
} }
//#endregion //#endregion
} }
}
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -2,10 +2,24 @@
<div class="yfudmmck"> <div class="yfudmmck">
<nav> <nav>
<div class="path" @contextmenu.prevent.stop="() => {}"> <div class="path" @contextmenu.prevent.stop="() => {}">
<XNavFolder :class="{ current: folder == null }"/> <XNavFolder
:class="{ current: folder == null }"
:parent-folder="folder"
@move="move"
@upload="upload"
@removeFile="removeFile"
@removeFolder="removeFolder"
/>
<template v-for="f in hierarchyFolders"> <template v-for="f in hierarchyFolders">
<span class="separator"><i class="fas fa-angle-right"></i></span> <span class="separator"><i class="fas fa-angle-right"></i></span>
<XNavFolder :folder="f"/> <XNavFolder
:folder="f"
:parent-folder="folder"
@move="move"
@upload="upload"
@removeFile="removeFile"
@removeFolder="removeFolder"
/>
</template> </template>
<span v-if="folder != null" class="separator"><i class="fas fa-angle-right"></i></span> <span v-if="folder != null" class="separator"><i class="fas fa-angle-right"></i></span>
<span v-if="folder != null" class="folder current">{{ folder.name }}</span> <span v-if="folder != null" class="folder current">{{ folder.name }}</span>
@ -22,192 +36,154 @@
> >
<div ref="contents" class="contents"> <div ref="contents" class="contents">
<div v-show="folders.length > 0" ref="foldersContainer" class="folders"> <div v-show="folders.length > 0" ref="foldersContainer" class="folders">
<XFolder v-for="(f, i) in folders" :key="f.id" v-anim="i" class="folder" :folder="f" :select-mode="select === 'folder'" :is-selected="selectedFolders.some(x => x.id === f.id)" @chosen="chooseFolder"/> <XFolder
v-for="(f, i) in folders"
:key="f.id"
v-anim="i"
class="folder"
:folder="f"
:select-mode="select === 'folder'"
:is-selected="selectedFolders.some(x => x.id === f.id)"
@chosen="chooseFolder"
@move="move"
@upload="upload"
@removeFile="removeFile"
@removeFolder="removeFolder"
@dragstart="isDragSource = true"
@dragend="isDragSource = false"
/>
<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid --> <!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
<div v-for="(n, i) in 16" :key="i" class="padding"></div> <div v-for="(n, i) in 16" :key="i" class="padding"></div>
<MkButton v-if="moreFolders" ref="moreFolders">{{ $ts.loadMore }}</MkButton> <MkButton v-if="moreFolders" ref="moreFolders">{{ i18n.locale.loadMore }}</MkButton>
</div> </div>
<div v-show="files.length > 0" ref="filesContainer" class="files"> <div v-show="files.length > 0" ref="filesContainer" class="files">
<XFile v-for="(file, i) in files" :key="file.id" v-anim="i" class="file" :file="file" :select-mode="select === 'file'" :is-selected="selectedFiles.some(x => x.id === file.id)" @chosen="chooseFile"/> <XFile
v-for="(file, i) in files"
:key="file.id"
v-anim="i"
class="file"
:file="file"
:select-mode="select === 'file'"
:is-selected="selectedFiles.some(x => x.id === file.id)"
@chosen="chooseFile"
@dragstart="isDragSource = true"
@dragend="isDragSource = false"
/>
<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid --> <!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
<div v-for="(n, i) in 16" :key="i" class="padding"></div> <div v-for="(n, i) in 16" :key="i" class="padding"></div>
<MkButton v-show="moreFiles" ref="loadMoreFiles" @click="fetchMoreFiles">{{ $ts.loadMore }}</MkButton> <MkButton v-show="moreFiles" ref="loadMoreFiles" @click="fetchMoreFiles">{{ i18n.locale.loadMore }}</MkButton>
</div> </div>
<div v-if="files.length == 0 && folders.length == 0 && !fetching" class="empty"> <div v-if="files.length == 0 && folders.length == 0 && !fetching" class="empty">
<p v-if="draghover">{{ $t('empty-draghover') }}</p> <p v-if="draghover">{{ i18n.t('empty-draghover') }}</p>
<p v-if="!draghover && folder == null"><strong>{{ $ts.emptyDrive }}</strong><br/>{{ $t('empty-drive-description') }}</p> <p v-if="!draghover && folder == null"><strong>{{ i18n.locale.emptyDrive }}</strong><br/>{{ i18n.t('empty-drive-description') }}</p>
<p v-if="!draghover && folder != null">{{ $ts.emptyFolder }}</p> <p v-if="!draghover && folder != null">{{ i18n.locale.emptyFolder }}</p>
</div> </div>
</div> </div>
<MkLoading v-if="fetching"/> <MkLoading v-if="fetching"/>
</div> </div>
<div v-if="draghover" class="dropzone"></div> <div v-if="draghover" class="dropzone"></div>
<input ref="fileInput" type="file" accept="*/*" multiple="multiple" tabindex="-1" @change="onChangeFileInput"/> <input ref="fileInput" type="file" accept="*/*" multiple tabindex="-1" @change="onChangeFileInput"/>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent, markRaw } from 'vue'; import { markRaw, nextTick, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import * as Misskey from 'misskey-js';
import XNavFolder from './drive.nav-folder.vue'; import XNavFolder from './drive.nav-folder.vue';
import XFolder from './drive.folder.vue'; import XFolder from './drive.folder.vue';
import XFile from './drive.file.vue'; import XFile from './drive.file.vue';
import MkButton from './ui/button.vue'; import MkButton from './ui/button.vue';
import * as os from '@/os'; import * as os from '@/os';
import { stream } from '@/stream'; import { stream } from '@/stream';
import { defaultStore } from '@/store';
import { i18n } from '@/i18n';
export default defineComponent({ const props = withDefaults(defineProps<{
components: { initialFolder?: Misskey.entities.DriveFolder;
XNavFolder, type?: string;
XFolder, multiple?: boolean;
XFile, select?: 'file' | 'folder' | null;
MkButton, }>(), {
}, multiple: false,
select: null,
});
props: { const emit = defineEmits<{
initialFolder: { (e: 'selected', v: Misskey.entities.DriveFile | Misskey.entities.DriveFolder): void;
type: Object, (e: 'change-selection', v: Misskey.entities.DriveFile[] | Misskey.entities.DriveFolder[]): void;
required: false (e: 'move-root'): void;
}, (e: 'cd', v: Misskey.entities.DriveFolder | null): void;
type: { (e: 'open-folder', v: Misskey.entities.DriveFolder): void;
type: String, }>();
required: false,
default: undefined
},
multiple: {
type: Boolean,
required: false,
default: false
},
select: {
type: String,
required: false,
default: null
}
},
emits: ['selected', 'change-selection', 'move-root', 'cd', 'open-folder'], const loadMoreFiles = ref<InstanceType<typeof MkButton>>();
const fileInput = ref<HTMLInputElement>();
data() { const folder = ref<Misskey.entities.DriveFolder | null>(null);
return { const files = ref<Misskey.entities.DriveFile[]>([]);
/** const folders = ref<Misskey.entities.DriveFolder[]>([]);
* 現在の階層(フォルダ) const moreFiles = ref(false);
* * null でルートを表す const moreFolders = ref(false);
*/ const hierarchyFolders = ref<Misskey.entities.DriveFolder[]>([]);
folder: null, const selectedFiles = ref<Misskey.entities.DriveFile[]>([]);
const selectedFolders = ref<Misskey.entities.DriveFolder[]>([]);
const uploadings = os.uploads;
const connection = stream.useChannel('drive');
files: [], //
folders: [], const draghover = ref(false);
moreFiles: false,
moreFolders: false,
hierarchyFolders: [],
selectedFiles: [],
selectedFolders: [],
uploadings: os.uploads,
connection: null,
/** //
* ドロップされようとしているか // ()
*/ const isDragSource = ref(false);
draghover: false,
/** const fetching = ref(true);
* 自信の所有するアイテムがドラッグをスタートさせたか
* (自分自身の階層にドロップできないようにするためのフラグ)
*/
isDragSource: false,
fetching: true, const ilFilesObserver = new IntersectionObserver(
(entries) => entries.some((entry) => entry.isIntersecting) && !fetching.value && moreFiles.value && fetchMoreFiles()
)
ilFilesObserver: new IntersectionObserver( watch(folder, () => emit('cd', folder.value));
(entries) => entries.some((entry) => entry.isIntersecting)
&& !this.fetching && this.moreFiles &&
this.fetchMoreFiles()
),
moreFilesElement: null as Element,
}; function onStreamDriveFileCreated(file: Misskey.entities.DriveFile) {
}, addFile(file, true);
}
watch: { function onStreamDriveFileUpdated(file: Misskey.entities.DriveFile) {
folder() { const current = folder.value ? folder.value.id : null;
this.$emit('cd', this.folder);
}
},
mounted() {
if (this.$store.state.enableInfiniteScroll && this.$refs.loadMoreFiles) {
this.$nextTick(() => {
this.ilFilesObserver.observe((this.$refs.loadMoreFiles as Vue).$el)
});
}
this.connection = markRaw(stream.useChannel('drive'));
this.connection.on('fileCreated', this.onStreamDriveFileCreated);
this.connection.on('fileUpdated', this.onStreamDriveFileUpdated);
this.connection.on('fileDeleted', this.onStreamDriveFileDeleted);
this.connection.on('folderCreated', this.onStreamDriveFolderCreated);
this.connection.on('folderUpdated', this.onStreamDriveFolderUpdated);
this.connection.on('folderDeleted', this.onStreamDriveFolderDeleted);
if (this.initialFolder) {
this.move(this.initialFolder);
} else {
this.fetch();
}
},
activated() {
if (this.$store.state.enableInfiniteScroll) {
this.$nextTick(() => {
this.ilFilesObserver.observe((this.$refs.loadMoreFiles as Vue).$el)
});
}
},
beforeUnmount() {
this.connection.dispose();
this.ilFilesObserver.disconnect();
},
methods: {
onStreamDriveFileCreated(file) {
this.addFile(file, true);
},
onStreamDriveFileUpdated(file) {
const current = this.folder ? this.folder.id : null;
if (current != file.folderId) { if (current != file.folderId) {
this.removeFile(file); removeFile(file);
} else { } else {
this.addFile(file, true); addFile(file, true);
} }
}, }
onStreamDriveFileDeleted(fileId) { function onStreamDriveFileDeleted(fileId: string) {
this.removeFile(fileId); removeFile(fileId);
}, }
onStreamDriveFolderCreated(folder) { function onStreamDriveFolderCreated(createdFolder: Misskey.entities.DriveFolder) {
this.addFolder(folder, true); addFolder(createdFolder, true);
}, }
onStreamDriveFolderUpdated(folder) { function onStreamDriveFolderUpdated(updatedFolder: Misskey.entities.DriveFolder) {
const current = this.folder ? this.folder.id : null; const current = folder.value ? folder.value.id : null;
if (current != folder.parentId) { if (current != updatedFolder.parentId) {
this.removeFolder(folder); removeFolder(updatedFolder);
} else { } else {
this.addFolder(folder, true); addFolder(updatedFolder, true);
} }
}, }
onStreamDriveFolderDeleted(folderId) { function onStreamDriveFolderDeleted(folderId: string) {
this.removeFolder(folderId); removeFolder(folderId);
}, }
function onDragover(e: DragEvent): any {
if (!e.dataTransfer) return;
onDragover(e): any {
// //
if (this.isDragSource) { if (isDragSource.value) {
// //
e.dataTransfer.dropEffect = 'none'; e.dataTransfer.dropEffect = 'none';
return; return;
@ -216,7 +192,6 @@ export default defineComponent({
const isFile = e.dataTransfer.items[0].kind == 'file'; const isFile = e.dataTransfer.items[0].kind == 'file';
const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_; const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_; const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;
if (isFile || isDriveFile || isDriveFolder) { if (isFile || isDriveFile || isDriveFolder) {
e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move'; e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
} else { } else {
@ -224,23 +199,25 @@ export default defineComponent({
} }
return false; return false;
}, }
onDragenter(e) { function onDragenter() {
if (!this.isDragSource) this.draghover = true; if (!isDragSource.value) draghover.value = true;
}, }
onDragleave(e) { function onDragleave() {
this.draghover = false; draghover.value = false;
}, }
onDrop(e): any { function onDrop(e: DragEvent): any {
this.draghover = false; draghover.value = false;
if (!e.dataTransfer) return;
// //
if (e.dataTransfer.files.length > 0) { if (e.dataTransfer.files.length > 0) {
for (const file of Array.from(e.dataTransfer.files)) { for (const file of Array.from(e.dataTransfer.files)) {
this.upload(file, this.folder); upload(file, folder.value);
} }
return; return;
} }
@ -249,11 +226,11 @@ export default defineComponent({
const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_); const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
if (driveFile != null && driveFile != '') { if (driveFile != null && driveFile != '') {
const file = JSON.parse(driveFile); const file = JSON.parse(driveFile);
if (this.files.some(f => f.id == file.id)) return; if (files.value.some(f => f.id == file.id)) return;
this.removeFile(file.id); removeFile(file.id);
os.api('drive/files/update', { os.api('drive/files/update', {
fileId: file.id, fileId: file.id,
folderId: this.folder ? this.folder.id : null folderId: folder.value ? folder.value.id : null
}); });
} }
//#endregion //#endregion
@ -261,377 +238,398 @@ export default defineComponent({
//#region //#region
const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_); const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
if (driveFolder != null && driveFolder != '') { if (driveFolder != null && driveFolder != '') {
const folder = JSON.parse(driveFolder); const droppedFolder = JSON.parse(driveFolder);
// reject // reject
if (this.folder && folder.id == this.folder.id) return false; if (folder.value && droppedFolder.id == folder.value.id) return false;
if (this.folders.some(f => f.id == folder.id)) return false; if (folders.value.some(f => f.id == droppedFolder.id)) return false;
this.removeFolder(folder.id); removeFolder(droppedFolder.id);
os.api('drive/folders/update', { os.api('drive/folders/update', {
folderId: folder.id, folderId: droppedFolder.id,
parentId: this.folder ? this.folder.id : null parentId: folder.value ? folder.value.id : null
}).then(() => { }).then(() => {
// noop // noop
}).catch(err => { }).catch(err => {
switch (err) { switch (err) {
case 'detected-circular-definition': case 'detected-circular-definition':
os.alert({ os.alert({
title: this.$ts.unableToProcess, title: i18n.locale.unableToProcess,
text: this.$ts.circularReferenceFolder text: i18n.locale.circularReferenceFolder
}); });
break; break;
default: default:
os.alert({ os.alert({
type: 'error', type: 'error',
text: this.$ts.somethingHappened text: i18n.locale.somethingHappened
}); });
} }
}); });
} }
//#endregion //#endregion
}, }
selectLocalFile() { function selectLocalFile() {
(this.$refs.fileInput as any).click(); fileInput.value?.click();
}, }
urlUpload() { function urlUpload() {
os.inputText({ os.inputText({
title: this.$ts.uploadFromUrl, title: i18n.locale.uploadFromUrl,
type: 'url', type: 'url',
placeholder: this.$ts.uploadFromUrlDescription placeholder: i18n.locale.uploadFromUrlDescription
}).then(({ canceled, result: url }) => { }).then(({ canceled, result: url }) => {
if (canceled) return; if (canceled || !url) return;
os.api('drive/files/upload-from-url', { os.api('drive/files/upload-from-url', {
url: url, url: url,
folderId: this.folder ? this.folder.id : undefined folderId: folder.value ? folder.value.id : undefined
}); });
os.alert({ os.alert({
title: this.$ts.uploadFromUrlRequested, title: i18n.locale.uploadFromUrlRequested,
text: this.$ts.uploadFromUrlMayTakeTime text: i18n.locale.uploadFromUrlMayTakeTime
}); });
}); });
}, }
createFolder() { function createFolder() {
os.inputText({ os.inputText({
title: this.$ts.createFolder, title: i18n.locale.createFolder,
placeholder: this.$ts.folderName placeholder: i18n.locale.folderName
}).then(({ canceled, result: name }) => { }).then(({ canceled, result: name }) => {
if (canceled) return; if (canceled) return;
os.api('drive/folders/create', { os.api('drive/folders/create', {
name: name, name: name,
parentId: this.folder ? this.folder.id : undefined parentId: folder.value ? folder.value.id : undefined
}).then(folder => { }).then(createdFolder => {
this.addFolder(folder, true); addFolder(createdFolder, true);
}); });
}); });
}, }
renameFolder(folder) { function renameFolder(folderToRename: Misskey.entities.DriveFolder) {
os.inputText({ os.inputText({
title: this.$ts.renameFolder, title: i18n.locale.renameFolder,
placeholder: this.$ts.inputNewFolderName, placeholder: i18n.locale.inputNewFolderName,
default: folder.name default: folderToRename.name
}).then(({ canceled, result: name }) => { }).then(({ canceled, result: name }) => {
if (canceled) return; if (canceled) return;
os.api('drive/folders/update', { os.api('drive/folders/update', {
folderId: folder.id, folderId: folderToRename.id,
name: name name: name
}).then(folder => { }).then(updatedFolder => {
// FIXME: // FIXME:
this.move(folder); move(updatedFolder);
}); });
}); });
}, }
deleteFolder(folder) { function deleteFolder(folderToDelete: Misskey.entities.DriveFolder) {
os.api('drive/folders/delete', { os.api('drive/folders/delete', {
folderId: folder.id folderId: folderToDelete.id
}).then(() => { }).then(() => {
// //
this.move(folder.parentId); move(folderToDelete.parentId);
}).catch(err => { }).catch(err => {
switch(err.id) { switch(err.id) {
case 'b0fc8a17-963c-405d-bfbc-859a487295e1': case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
os.alert({ os.alert({
type: 'error', type: 'error',
title: this.$ts.unableToDelete, title: i18n.locale.unableToDelete,
text: this.$ts.hasChildFilesOrFolders text: i18n.locale.hasChildFilesOrFolders
}); });
break; break;
default: default:
os.alert({ os.alert({
type: 'error', type: 'error',
text: this.$ts.unableToDelete text: i18n.locale.unableToDelete
}); });
} }
}); });
}, }
onChangeFileInput() { function onChangeFileInput() {
for (const file of Array.from((this.$refs.fileInput as any).files)) { if (!fileInput.value?.files) return;
this.upload(file, this.folder); for (const file of Array.from(fileInput.value.files)) {
upload(file, folder.value);
} }
}, }
upload(file, folder) { function upload(file: File, folderToUpload?: Misskey.entities.DriveFolder | null) {
if (folder && typeof folder == 'object') folder = folder.id; os.upload(file, (folderToUpload && typeof folderToUpload == 'object') ? folderToUpload.id : null).then(res => {
os.upload(file, folder).then(res => { addFile(res, true);
this.addFile(res, true);
}); });
}, }
chooseFile(file) { function chooseFile(file: Misskey.entities.DriveFile) {
const isAlreadySelected = this.selectedFiles.some(f => f.id == file.id); const isAlreadySelected = selectedFiles.value.some(f => f.id == file.id);
if (this.multiple) { if (props.multiple) {
if (isAlreadySelected) { if (isAlreadySelected) {
this.selectedFiles = this.selectedFiles.filter(f => f.id != file.id); selectedFiles.value = selectedFiles.value.filter(f => f.id != file.id);
} else { } else {
this.selectedFiles.push(file); selectedFiles.value.push(file);
} }
this.$emit('change-selection', this.selectedFiles); emit('change-selection', selectedFiles.value);
} else { } else {
if (isAlreadySelected) { if (isAlreadySelected) {
this.$emit('selected', file); emit('selected', file);
} else { } else {
this.selectedFiles = [file]; selectedFiles.value = [file];
this.$emit('change-selection', [file]); emit('change-selection', [file]);
} }
} }
}, }
chooseFolder(folder) { function chooseFolder(folderToChoose: Misskey.entities.DriveFolder) {
const isAlreadySelected = this.selectedFolders.some(f => f.id == folder.id); const isAlreadySelected = selectedFolders.value.some(f => f.id == folderToChoose.id);
if (this.multiple) { if (props.multiple) {
if (isAlreadySelected) { if (isAlreadySelected) {
this.selectedFolders = this.selectedFolders.filter(f => f.id != folder.id); selectedFolders.value = selectedFolders.value.filter(f => f.id != folderToChoose.id);
} else { } else {
this.selectedFolders.push(folder); selectedFolders.value.push(folderToChoose);
} }
this.$emit('change-selection', this.selectedFolders); emit('change-selection', selectedFolders.value);
} else { } else {
if (isAlreadySelected) { if (isAlreadySelected) {
this.$emit('selected', folder); emit('selected', folderToChoose);
} else { } else {
this.selectedFolders = [folder]; selectedFolders.value = [folderToChoose];
this.$emit('change-selection', [folder]); emit('change-selection', [folderToChoose]);
} }
} }
}, }
move(target) { function move(target?: Misskey.entities.DriveFolder) {
if (target == null) { if (!target) {
this.goRoot(); goRoot();
return; return;
} else if (typeof target == 'object') { } else if (typeof target == 'object') {
target = target.id; target = target.id;
} }
this.fetching = true; fetching.value = true;
os.api('drive/folders/show', { os.api('drive/folders/show', {
folderId: target folderId: target
}).then(folder => { }).then(folderToMove => {
this.folder = folder; folder.value = folderToMove;
this.hierarchyFolders = []; hierarchyFolders.value = [];
const dive = folder => { const dive = folderToDive => {
this.hierarchyFolders.unshift(folder); hierarchyFolders.value.unshift(folderToDive);
if (folder.parent) dive(folder.parent); if (folderToDive.parent) dive(folderToDive.parent);
}; };
if (folder.parent) dive(folder.parent); if (folderToMove.parent) dive(folderToMove.parent);
this.$emit('open-folder', folder); emit('open-folder', folderToMove);
this.fetch(); fetch();
}); });
}, }
addFolder(folder, unshift = false) { function addFolder(folderToAdd: Misskey.entities.DriveFolder, unshift = false) {
const current = this.folder ? this.folder.id : null; const current = folder.value ? folder.value.id : null;
if (current != folder.parentId) return; if (current != folderToAdd.parentId) return;
if (this.folders.some(f => f.id == folder.id)) { if (folders.value.some(f => f.id == folderToAdd.id)) {
const exist = this.folders.map(f => f.id).indexOf(folder.id); const exist = folders.value.map(f => f.id).indexOf(folderToAdd.id);
this.folders[exist] = folder; folders.value[exist] = folderToAdd;
return; return;
} }
if (unshift) { if (unshift) {
this.folders.unshift(folder); folders.value.unshift(folderToAdd);
} else { } else {
this.folders.push(folder); folders.value.push(folderToAdd);
} }
}, }
addFile(file, unshift = false) { function addFile(fileToAdd: Misskey.entities.DriveFile, unshift = false) {
const current = this.folder ? this.folder.id : null; const current = folder.value ? folder.value.id : null;
if (current != file.folderId) return; if (current != fileToAdd.folderId) return;
if (this.files.some(f => f.id == file.id)) { if (files.value.some(f => f.id == fileToAdd.id)) {
const exist = this.files.map(f => f.id).indexOf(file.id); const exist = files.value.map(f => f.id).indexOf(fileToAdd.id);
this.files[exist] = file; files.value[exist] = fileToAdd;
return; return;
} }
if (unshift) { if (unshift) {
this.files.unshift(file); files.value.unshift(fileToAdd);
} else { } else {
this.files.push(file); files.value.push(fileToAdd);
} }
}, }
removeFolder(folder) { function removeFolder(folderToRemove: Misskey.entities.DriveFolder | string) {
if (typeof folder == 'object') folder = folder.id; const folderIdToRemove = typeof folderToRemove === 'object' ? folderToRemove.id : folderToRemove;
this.folders = this.folders.filter(f => f.id != folder); folders.value = folders.value.filter(f => f.id != folderIdToRemove);
}, }
removeFile(file) { function removeFile(file: Misskey.entities.DriveFile | string) {
if (typeof file == 'object') file = file.id; const fileId = typeof file === 'object' ? file.id : file;
this.files = this.files.filter(f => f.id != file); files.value = files.value.filter(f => f.id != fileId);
}, }
appendFile(file) { function appendFile(file: Misskey.entities.DriveFile) {
this.addFile(file); addFile(file);
}, }
appendFolder(folder) { function appendFolder(folderToAppend: Misskey.entities.DriveFolder) {
this.addFolder(folder); addFolder(folderToAppend);
}, }
/*
function prependFile(file: Misskey.entities.DriveFile) {
addFile(file, true);
}
prependFile(file) { function prependFolder(folderToPrepend: Misskey.entities.DriveFolder) {
this.addFile(file, true); addFolder(folderToPrepend, true);
}, }
*/
prependFolder(folder) { function goRoot() {
this.addFolder(folder, true);
},
goRoot() {
// root // root
if (this.folder == null) return; if (folder.value == null) return;
this.folder = null; folder.value = null;
this.hierarchyFolders = []; hierarchyFolders.value = [];
this.$emit('move-root'); emit('move-root');
this.fetch(); fetch();
}, }
fetch() { async function fetch() {
this.folders = []; folders.value = [];
this.files = []; files.value = [];
this.moreFolders = false; moreFolders.value = false;
this.moreFiles = false; moreFiles.value = false;
this.fetching = true; fetching.value = true;
let fetchedFolders = null;
let fetchedFiles = null;
const foldersMax = 30; const foldersMax = 30;
const filesMax = 30; const filesMax = 30;
// const foldersPromise = os.api('drive/folders', {
os.api('drive/folders', { folderId: folder.value ? folder.value.id : null,
folderId: this.folder ? this.folder.id : null,
limit: foldersMax + 1 limit: foldersMax + 1
}).then(folders => { }).then(fetchedFolders => {
if (folders.length == foldersMax + 1) { if (fetchedFolders.length == foldersMax + 1) {
this.moreFolders = true; moreFolders.value = true;
folders.pop(); fetchedFolders.pop();
} }
fetchedFolders = folders; return fetchedFolders;
complete();
}); });
// const filesPromise = os.api('drive/files', {
os.api('drive/files', { folderId: folder.value ? folder.value.id : null,
folderId: this.folder ? this.folder.id : null, type: props.type,
type: this.type,
limit: filesMax + 1 limit: filesMax + 1
}).then(files => { }).then(fetchedFiles => {
if (files.length == filesMax + 1) { if (fetchedFiles.length == filesMax + 1) {
this.moreFiles = true; moreFiles.value = true;
files.pop(); fetchedFiles.pop();
} }
fetchedFiles = files; return fetchedFiles;
complete();
}); });
let flag = false; const [fetchedFolders, fetchedFiles] = await Promise.all([foldersPromise, filesPromise]);
const complete = () => {
if (flag) {
for (const x of fetchedFolders) this.appendFolder(x);
for (const x of fetchedFiles) this.appendFile(x);
this.fetching = false;
} else {
flag = true;
}
};
},
fetchMoreFiles() { for (const x of fetchedFolders) appendFolder(x);
this.fetching = true; for (const x of fetchedFiles) appendFile(x);
fetching.value = false;
}
function fetchMoreFiles() {
fetching.value = true;
const max = 30; const max = 30;
// //
os.api('drive/files', { os.api('drive/files', {
folderId: this.folder ? this.folder.id : null, folderId: folder.value ? folder.value.id : null,
type: this.type, type: props.type,
untilId: this.files[this.files.length - 1].id, untilId: files.value[files.value.length - 1].id,
limit: max + 1 limit: max + 1
}).then(files => { }).then(files => {
if (files.length == max + 1) { if (files.length == max + 1) {
this.moreFiles = true; moreFiles.value = true;
files.pop(); files.pop();
} else { } else {
this.moreFiles = false; moreFiles.value = false;
} }
for (const x of files) this.appendFile(x); for (const x of files) appendFile(x);
this.fetching = false; fetching.value = false;
}); });
}, }
getMenu() { function getMenu() {
return [{ return [{
text: this.$ts.addFile, text: i18n.locale.addFile,
type: 'label' type: 'label'
}, { }, {
text: this.$ts.upload, text: i18n.locale.upload,
icon: 'fas fa-upload', icon: 'fas fa-upload',
action: () => { this.selectLocalFile(); } action: () => { selectLocalFile(); }
}, { }, {
text: this.$ts.fromUrl, text: i18n.locale.fromUrl,
icon: 'fas fa-link', icon: 'fas fa-link',
action: () => { this.urlUpload(); } action: () => { urlUpload(); }
}, null, { }, null, {
text: this.folder ? this.folder.name : this.$ts.drive, text: folder.value ? folder.value.name : i18n.locale.drive,
type: 'label' type: 'label'
}, this.folder ? { }, folder.value ? {
text: this.$ts.renameFolder, text: i18n.locale.renameFolder,
icon: 'fas fa-i-cursor', icon: 'fas fa-i-cursor',
action: () => { this.renameFolder(this.folder); } action: () => { renameFolder(folder.value); }
} : undefined, this.folder ? { } : undefined, folder.value ? {
text: this.$ts.deleteFolder, text: i18n.locale.deleteFolder,
icon: 'fas fa-trash-alt', icon: 'fas fa-trash-alt',
action: () => { this.deleteFolder(this.folder); } action: () => { deleteFolder(folder.value as Misskey.entities.DriveFolder); }
} : undefined, { } : undefined, {
text: this.$ts.createFolder, text: i18n.locale.createFolder,
icon: 'fas fa-folder-plus', icon: 'fas fa-folder-plus',
action: () => { this.createFolder(); } action: () => { createFolder(); }
}]; }];
}, }
showMenu(ev) { function showMenu(ev: MouseEvent) {
os.popupMenu(this.getMenu(), ev.currentTarget || ev.target); os.popupMenu(getMenu(), (ev.currentTarget || ev.target || undefined) as HTMLElement | undefined);
}, }
onContextmenu(ev) { function onContextmenu(ev: MouseEvent) {
os.contextMenu(this.getMenu(), ev); os.contextMenu(getMenu(), ev);
}, }
onMounted(() => {
if (defaultStore.state.enableInfiniteScroll && loadMoreFiles.value) {
nextTick(() => {
ilFilesObserver.observe(loadMoreFiles.value?.$el)
});
} }
connection.on('fileCreated', onStreamDriveFileCreated);
connection.on('fileUpdated', onStreamDriveFileUpdated);
connection.on('fileDeleted', onStreamDriveFileDeleted);
connection.on('folderCreated', onStreamDriveFolderCreated);
connection.on('folderUpdated', onStreamDriveFolderUpdated);
connection.on('folderDeleted', onStreamDriveFolderDeleted);
if (props.initialFolder) {
move(props.initialFolder);
} else {
fetch();
}
});
onActivated(() => {
if (defaultStore.state.enableInfiniteScroll) {
nextTick(() => {
ilFilesObserver.observe(loadMoreFiles.value?.$el)
});
}
});
onBeforeUnmount(() => {
connection.dispose();
ilFilesObserver.disconnect();
}); });
</script> </script>

View File

@ -1,58 +1,65 @@
<template> <template>
<MkModal ref="modal" v-slot="{ type, maxHeight }" :z-priority="'middle'" :prefer-type="asReactionPicker && $store.state.reactionPickerUseDrawerForMobile === false ? 'popup' : 'auto'" :transparent-bg="true" :manual-showing="manualShowing" :src="src" @click="$refs.modal.close()" @opening="opening" @close="$emit('close')" @closed="$emit('closed')"> <MkModal
<MkEmojiPicker ref="picker" class="ryghynhb _popup _shadow" :class="{ drawer: type === 'drawer' }" :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" :as-drawer="type === 'drawer'" :max-height="maxHeight" @chosen="chosen"/> ref="modal"
v-slot="{ type, maxHeight }"
:z-priority="'middle'"
:prefer-type="asReactionPicker && defaultStore.state.reactionPickerUseDrawerForMobile === false ? 'popup' : 'auto'"
:transparent-bg="true"
:manual-showing="manualShowing"
:src="src"
@click="modal?.close()"
@opening="opening"
@close="emit('close')"
@closed="emit('closed')"
>
<MkEmojiPicker
ref="picker"
class="ryghynhb _popup _shadow"
:class="{ drawer: type === 'drawer' }"
:show-pinned="showPinned"
:as-reaction-picker="asReactionPicker"
:as-drawer="type === 'drawer'"
:max-height="maxHeight"
@chosen="chosen"
/>
</MkModal> </MkModal>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent, markRaw } from 'vue'; import { ref } from 'vue';
import MkModal from '@/components/ui/modal.vue'; import MkModal from '@/components/ui/modal.vue';
import MkEmojiPicker from '@/components/emoji-picker.vue'; import MkEmojiPicker from '@/components/emoji-picker.vue';
import { defaultStore } from '@/store';
export default defineComponent({ withDefaults(defineProps<{
components: { manualShowing?: boolean;
MkModal, src?: HTMLElement;
MkEmojiPicker, showPinned?: boolean;
}, asReactionPicker?: boolean;
}>(), {
props: { manualShowing: false,
manualShowing: { showPinned: true,
type: Boolean, asReactionPicker: false,
required: false,
default: null,
},
src: {
required: false
},
showPinned: {
required: false,
default: true
},
asReactionPicker: {
required: false
},
},
emits: ['done', 'close', 'closed'],
data() {
return {
};
},
methods: {
chosen(emoji: any) {
this.$emit('done', emoji);
this.$refs.modal.close();
},
opening() {
this.$refs.picker.reset();
this.$refs.picker.focus();
}
}
}); });
const emit = defineEmits<{
(e: 'done', v: any): void;
(e: 'close'): void;
(e: 'closed'): void;
}>();
const modal = ref<InstanceType<typeof MkModal>>();
const picker = ref<InstanceType<typeof MkEmojiPicker>>();
function chosen(emoji: any) {
emit('done', emoji);
modal.value?.close();
}
function opening() {
picker.value?.reset();
picker.value?.focus();
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -5,50 +5,33 @@
:can-resize="false" :can-resize="false"
:mini="true" :mini="true"
:front="true" :front="true"
@closed="$emit('closed')" @closed="emit('closed')"
> >
<MkEmojiPicker :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" @chosen="chosen"/> <MkEmojiPicker :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" @chosen="chosen"/>
</MkWindow> </MkWindow>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent, markRaw } from 'vue'; import { } from 'vue';
import MkWindow from '@/components/ui/window.vue'; import MkWindow from '@/components/ui/window.vue';
import MkEmojiPicker from '@/components/emoji-picker.vue'; import MkEmojiPicker from '@/components/emoji-picker.vue';
export default defineComponent({ withDefaults(defineProps<{
components: { src?: HTMLElement;
MkWindow, showPinned?: boolean;
MkEmojiPicker, asReactionPicker?: boolean;
}, }>(), {
showPinned: true,
props: {
src: {
required: false
},
showPinned: {
required: false,
default: true
},
asReactionPicker: {
required: false
},
},
emits: ['chosen', 'closed'],
data() {
return {
};
},
methods: {
chosen(emoji: any) {
this.$emit('chosen', emoji);
},
}
}); });
const emit = defineEmits<{
(e: 'chosen', v: any): void;
(e: 'closed'): void;
}>();
function chosen(emoji: any) {
emit('chosen', emoji);
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -7,7 +7,7 @@
<button v-for="emoji in emojis" <button v-for="emoji in emojis"
:key="emoji" :key="emoji"
class="_button" class="_button"
@click="chosen(emoji, $event)" @click="emit('chosen', emoji, $event)"
> >
<MkEmoji :emoji="emoji" :normal="true"/> <MkEmoji :emoji="emoji" :normal="true"/>
</button> </button>
@ -15,35 +15,19 @@
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent, markRaw } from 'vue'; import { ref } from 'vue';
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
export default defineComponent({ const props = defineProps<{
props: { emojis: string[];
emojis: { initialShown?: boolean;
required: true, }>();
},
initialShown: {
required: false
}
},
emits: ['chosen'], const emit = defineEmits<{
(e: 'chosen', v: string, ev: MouseEvent): void;
}>();
data() { const shown = ref(!!props.initialShown);
return {
getStaticImageUrl,
shown: this.initialShown,
};
},
methods: {
chosen(emoji: any, ev) {
this.$parent.chosen(emoji, ev);
},
}
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -1,18 +1,18 @@
<template> <template>
<div class="omfetrab" :class="['w' + width, 'h' + height, { big, asDrawer }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : null }"> <div class="omfetrab" :class="['w' + width, 'h' + height, { big, asDrawer }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }">
<input ref="search" v-model.trim="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="$ts.search" @paste.stop="paste" @keyup.enter="done()"> <input ref="search" v-model.trim="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.locale.search" @paste.stop="paste" @keyup.enter="done()">
<div ref="emojis" class="emojis"> <div ref="emojis" class="emojis">
<section class="result"> <section class="result">
<div v-if="searchResultCustom.length > 0"> <div v-if="searchResultCustom.length > 0">
<button v-for="emoji in searchResultCustom" <button v-for="emoji in searchResultCustom"
:key="emoji" :key="emoji.id"
class="_button" class="_button"
:title="emoji.name" :title="emoji.name"
tabindex="0" tabindex="0"
@click="chosen(emoji, $event)" @click="chosen(emoji, $event)"
> >
<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/> <!--<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>-->
<img v-else :src="$store.state.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/> <img :src="disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/>
</button> </button>
</div> </div>
<div v-if="searchResultUnicode.length > 0"> <div v-if="searchResultUnicode.length > 0">
@ -43,9 +43,9 @@
</section> </section>
<section> <section>
<header class="_acrylic"><i class="far fa-clock fa-fw"></i> {{ $ts.recentUsed }}</header> <header class="_acrylic"><i class="far fa-clock fa-fw"></i> {{ i18n.locale.recentUsed }}</header>
<div> <div>
<button v-for="emoji in $store.state.recentlyUsedEmojis" <button v-for="emoji in recentlyUsedEmojis"
:key="emoji" :key="emoji"
class="_button" class="_button"
@click="chosen(emoji, $event)" @click="chosen(emoji, $event)"
@ -56,12 +56,12 @@
</section> </section>
</div> </div>
<div> <div>
<header class="_acrylic">{{ $ts.customEmojis }}</header> <header class="_acrylic">{{ i18n.locale.customEmojis }}</header>
<XSection v-for="category in customEmojiCategories" :key="'custom:' + category" :initial-shown="false" :emojis="customEmojis.filter(e => e.category === category).map(e => ':' + e.name + ':')">{{ category || $ts.other }}</XSection> <XSection v-for="category in customEmojiCategories" :key="'custom:' + category" :initial-shown="false" :emojis="customEmojis.filter(e => e.category === category).map(e => ':' + e.name + ':')" @chosen="chosen">{{ category || i18n.locale.other }}</XSection>
</div> </div>
<div> <div>
<header class="_acrylic">{{ $ts.emoji }}</header> <header class="_acrylic">{{ i18n.locale.emoji }}</header>
<XSection v-for="category in categories" :emojis="emojilist.filter(e => e.category === category).map(e => e.char)">{{ category }}</XSection> <XSection v-for="category in categories" :emojis="emojilist.filter(e => e.category === category).map(e => e.char)" @chosen="chosen">{{ category }}</XSection>
</div> </div>
</div> </div>
<div class="tabs"> <div class="tabs">
@ -73,82 +73,75 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent, markRaw } from 'vue'; import { ref, computed, watch, onMounted } from 'vue';
import { emojilist } from '@/scripts/emojilist'; import * as Misskey from 'misskey-js';
import { emojilist, UnicodeEmojiDef, unicodeEmojiCategories as categories } from '@/scripts/emojilist';
import { getStaticImageUrl } from '@/scripts/get-static-image-url'; import { getStaticImageUrl } from '@/scripts/get-static-image-url';
import Ripple from '@/components/ripple.vue'; import Ripple from '@/components/ripple.vue';
import * as os from '@/os'; import * as os from '@/os';
import { isTouchUsing } from '@/scripts/touch'; import { isTouchUsing } from '@/scripts/touch';
import { isMobile } from '@/scripts/is-mobile'; import { isMobile } from '@/scripts/is-mobile';
import { emojiCategories } from '@/instance'; import { emojiCategories, instance } from '@/instance';
import XSection from './emoji-picker.section.vue'; import XSection from './emoji-picker.section.vue';
import { i18n } from '@/i18n';
import { defaultStore } from '@/store';
export default defineComponent({ const props = withDefaults(defineProps<{
components: { showPinned?: boolean;
XSection asReactionPicker?: boolean;
}, maxHeight?: number;
asDrawer?: boolean;
}>(), {
showPinned: true,
});
props: { const emit = defineEmits<{
showPinned: { (e: 'chosen', v: string): void;
required: false, }>();
default: true,
},
asReactionPicker: {
required: false,
},
maxHeight: {
type: Number,
required: false,
},
asDrawer: {
type: Boolean,
required: false
},
},
emits: ['chosen'], const search = ref<HTMLInputElement>();
const emojis = ref<HTMLDivElement>();
data() { const {
return { reactions: pinned,
emojilist: markRaw(emojilist), reactionPickerWidth,
getStaticImageUrl, reactionPickerHeight,
pinned: this.$store.reactiveState.reactions, disableShowingAnimatedImages,
width: this.asReactionPicker ? this.$store.state.reactionPickerWidth : 3, recentlyUsedEmojis,
height: this.asReactionPicker ? this.$store.state.reactionPickerHeight : 2, } = defaultStore.reactiveState;
big: this.asReactionPicker ? isTouchUsing : false,
customEmojiCategories: emojiCategories,
customEmojis: this.$instance.emojis,
q: null,
searchResultCustom: [],
searchResultUnicode: [],
tab: 'index',
categories: ['face', 'people', 'animals_and_nature', 'food_and_drink', 'activity', 'travel_and_places', 'objects', 'symbols', 'flags'],
};
},
watch: { const width = computed(() => props.asReactionPicker ? reactionPickerWidth.value : 3);
q() { const height = computed(() => props.asReactionPicker ? reactionPickerHeight.value : 2);
this.$refs.emojis.scrollTop = 0; const big = props.asReactionPicker ? isTouchUsing : false;
const customEmojiCategories = emojiCategories;
const customEmojis = instance.emojis;
const q = ref<string | null>(null);
const searchResultCustom = ref<Misskey.entities.CustomEmoji[]>([]);
const searchResultUnicode = ref<UnicodeEmojiDef[]>([]);
const tab = ref<'index' | 'custom' | 'unicode' | 'tags'>('index');
if (this.q == null || this.q === '') { watch(q, () => {
this.searchResultCustom = []; if (emojis.value) emojis.value.scrollTop = 0;
this.searchResultUnicode = [];
if (q.value == null || q.value === '') {
searchResultCustom.value = [];
searchResultUnicode.value = [];
return; return;
} }
const q = this.q.replace(/:/g, ''); const newQ = q.value.replace(/:/g, '');
const searchCustom = () => { const searchCustom = () => {
const max = 8; const max = 8;
const emojis = this.customEmojis; const emojis = customEmojis;
const matches = new Set(); const matches = new Set<Misskey.entities.CustomEmoji>();
const exactMatch = emojis.find(e => e.name === q); const exactMatch = emojis.find(e => e.name === newQ);
if (exactMatch) matches.add(exactMatch); if (exactMatch) matches.add(exactMatch);
if (q.includes(' ')) { // AND if (newQ.includes(' ')) { // AND
const keywords = q.split(' '); const keywords = newQ.split(' ');
// //
for (const emoji of emojis) { for (const emoji of emojis) {
@ -168,7 +161,7 @@ export default defineComponent({
} }
} else { } else {
for (const emoji of emojis) { for (const emoji of emojis) {
if (emoji.name.startsWith(q)) { if (emoji.name.startsWith(newQ)) {
matches.add(emoji); matches.add(emoji);
if (matches.size >= max) break; if (matches.size >= max) break;
} }
@ -176,7 +169,7 @@ export default defineComponent({
if (matches.size >= max) return matches; if (matches.size >= max) return matches;
for (const emoji of emojis) { for (const emoji of emojis) {
if (emoji.aliases.some(alias => alias.startsWith(q))) { if (emoji.aliases.some(alias => alias.startsWith(newQ))) {
matches.add(emoji); matches.add(emoji);
if (matches.size >= max) break; if (matches.size >= max) break;
} }
@ -184,7 +177,7 @@ export default defineComponent({
if (matches.size >= max) return matches; if (matches.size >= max) return matches;
for (const emoji of emojis) { for (const emoji of emojis) {
if (emoji.name.includes(q)) { if (emoji.name.includes(newQ)) {
matches.add(emoji); matches.add(emoji);
if (matches.size >= max) break; if (matches.size >= max) break;
} }
@ -192,7 +185,7 @@ export default defineComponent({
if (matches.size >= max) return matches; if (matches.size >= max) return matches;
for (const emoji of emojis) { for (const emoji of emojis) {
if (emoji.aliases.some(alias => alias.includes(q))) { if (emoji.aliases.some(alias => alias.includes(newQ))) {
matches.add(emoji); matches.add(emoji);
if (matches.size >= max) break; if (matches.size >= max) break;
} }
@ -204,14 +197,14 @@ export default defineComponent({
const searchUnicode = () => { const searchUnicode = () => {
const max = 8; const max = 8;
const emojis = this.emojilist; const emojis = emojilist;
const matches = new Set(); const matches = new Set<UnicodeEmojiDef>();
const exactMatch = emojis.find(e => e.name === q); const exactMatch = emojis.find(e => e.name === newQ);
if (exactMatch) matches.add(exactMatch); if (exactMatch) matches.add(exactMatch);
if (q.includes(' ')) { // AND if (newQ.includes(' ')) { // AND
const keywords = q.split(' '); const keywords = newQ.split(' ');
// //
for (const emoji of emojis) { for (const emoji of emojis) {
@ -231,7 +224,7 @@ export default defineComponent({
} }
} else { } else {
for (const emoji of emojis) { for (const emoji of emojis) {
if (emoji.name.startsWith(q)) { if (emoji.name.startsWith(newQ)) {
matches.add(emoji); matches.add(emoji);
if (matches.size >= max) break; if (matches.size >= max) break;
} }
@ -239,7 +232,7 @@ export default defineComponent({
if (matches.size >= max) return matches; if (matches.size >= max) return matches;
for (const emoji of emojis) { for (const emoji of emojis) {
if (emoji.keywords.some(keyword => keyword.startsWith(q))) { if (emoji.keywords.some(keyword => keyword.startsWith(newQ))) {
matches.add(emoji); matches.add(emoji);
if (matches.size >= max) break; if (matches.size >= max) break;
} }
@ -247,7 +240,7 @@ export default defineComponent({
if (matches.size >= max) return matches; if (matches.size >= max) return matches;
for (const emoji of emojis) { for (const emoji of emojis) {
if (emoji.name.includes(q)) { if (emoji.name.includes(newQ)) {
matches.add(emoji); matches.add(emoji);
if (matches.size >= max) break; if (matches.size >= max) break;
} }
@ -255,7 +248,7 @@ export default defineComponent({
if (matches.size >= max) return matches; if (matches.size >= max) return matches;
for (const emoji of emojis) { for (const emoji of emojis) {
if (emoji.keywords.some(keyword => keyword.includes(q))) { if (emoji.keywords.some(keyword => keyword.includes(newQ))) {
matches.add(emoji); matches.add(emoji);
if (matches.size >= max) break; if (matches.size >= max) break;
} }
@ -265,85 +258,87 @@ export default defineComponent({
return matches; return matches;
}; };
this.searchResultCustom = Array.from(searchCustom()); searchResultCustom.value = Array.from(searchCustom());
this.searchResultUnicode = Array.from(searchUnicode()); searchResultUnicode.value = Array.from(searchUnicode());
} });
},
mounted() { function focus() {
this.focus();
},
methods: {
focus() {
if (!isMobile && !isTouchUsing) { if (!isMobile && !isTouchUsing) {
this.$refs.search.focus({ search.value?.focus({
preventScroll: true preventScroll: true
}); });
} }
}, }
reset() { function reset() {
this.$refs.emojis.scrollTop = 0; if (emojis.value) emojis.value.scrollTop = 0;
this.q = ''; q.value = '';
}, }
getKey(emoji: any) { function getKey(emoji: string | Misskey.entities.CustomEmoji | UnicodeEmojiDef): string {
return typeof emoji === 'string' ? emoji : (emoji.char || `:${emoji.name}:`); return typeof emoji === 'string' ? emoji : (emoji.char || `:${emoji.name}:`);
}, }
chosen(emoji: any, ev) { function chosen(emoji: any, ev?: MouseEvent) {
if (ev) { const el = ev && (ev.currentTarget || ev.target) as HTMLElement | null | undefined;
const el = ev.currentTarget || ev.target; if (el) {
const rect = el.getBoundingClientRect(); const rect = el.getBoundingClientRect();
const x = rect.left + (el.offsetWidth / 2); const x = rect.left + (el.offsetWidth / 2);
const y = rect.top + (el.offsetHeight / 2); const y = rect.top + (el.offsetHeight / 2);
os.popup(Ripple, { x, y }, {}, 'end'); os.popup(Ripple, { x, y }, {}, 'end');
} }
const key = this.getKey(emoji); const key = getKey(emoji);
this.$emit('chosen', key); emit('chosen', key);
// 使 // 使
if (!this.pinned.includes(key)) { if (!pinned.value.includes(key)) {
let recents = this.$store.state.recentlyUsedEmojis; let recents = defaultStore.state.recentlyUsedEmojis;
recents = recents.filter((e: any) => e !== key); recents = recents.filter((e: any) => e !== key);
recents.unshift(key); recents.unshift(key);
this.$store.set('recentlyUsedEmojis', recents.splice(0, 32)); defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32));
} }
}, }
paste(event) { function paste(event: ClipboardEvent) {
const paste = (event.clipboardData || window.clipboardData).getData('text'); const paste = (event.clipboardData || window.clipboardData).getData('text');
if (this.done(paste)) { if (done(paste)) {
event.preventDefault(); event.preventDefault();
} }
}, }
done(query) { function done(query?: any): boolean | void {
if (query == null) query = this.q; if (query == null) query = q.value;
if (query == null) return; if (query == null || typeof query !== 'string') return;
const q = query.replace(/:/g, '');
const exactMatchCustom = this.customEmojis.find(e => e.name === q); const q2 = query.replace(/:/g, '');
const exactMatchCustom = customEmojis.find(e => e.name === q2);
if (exactMatchCustom) { if (exactMatchCustom) {
this.chosen(exactMatchCustom); chosen(exactMatchCustom);
return true; return true;
} }
const exactMatchUnicode = this.emojilist.find(e => e.char === q || e.name === q); const exactMatchUnicode = emojilist.find(e => e.char === q2 || e.name === q2);
if (exactMatchUnicode) { if (exactMatchUnicode) {
this.chosen(exactMatchUnicode); chosen(exactMatchUnicode);
return true; return true;
} }
if (this.searchResultCustom.length > 0) { if (searchResultCustom.value.length > 0) {
this.chosen(this.searchResultCustom[0]); chosen(searchResultCustom.value[0]);
return true; return true;
} }
if (this.searchResultUnicode.length > 0) { if (searchResultUnicode.value.length > 0) {
this.chosen(this.searchResultUnicode[0]); chosen(searchResultUnicode.value[0]);
return true; return true;
} }
}, }
}
onMounted(() => {
focus();
});
defineExpose({
focus,
reset,
}); });
</script> </script>

View File

@ -2,25 +2,15 @@
<div v-if="meta" class="xfbouadm" :style="{ backgroundImage: `url(${ meta.backgroundImageUrl })` }"></div> <div v-if="meta" class="xfbouadm" :style="{ backgroundImage: `url(${ meta.backgroundImageUrl })` }"></div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent } from 'vue'; import { ref } from 'vue';
import * as Misskey from 'misskey-js';
import * as os from '@/os'; import * as os from '@/os';
export default defineComponent({ const meta = ref<Misskey.entities.DetailedInstanceMetadata>();
components: {
},
data() { os.api('meta', { detail: true }).then(gotMeta => {
return { meta.value = gotMeta;
meta: null,
};
},
created() {
os.api('meta', { detail: true }).then(meta => {
this.meta = meta;
});
},
}); });
</script> </script>

View File

@ -6,129 +6,110 @@
> >
<template v-if="!wait"> <template v-if="!wait">
<template v-if="hasPendingFollowRequestFromYou && user.isLocked"> <template v-if="hasPendingFollowRequestFromYou && user.isLocked">
<span v-if="full">{{ $ts.followRequestPending }}</span><i class="fas fa-hourglass-half"></i> <span v-if="full">{{ i18n.locale.followRequestPending }}</span><i class="fas fa-hourglass-half"></i>
</template> </template>
<template v-else-if="hasPendingFollowRequestFromYou && !user.isLocked"> <!-- つまりリモートフォローの場合 --> <template v-else-if="hasPendingFollowRequestFromYou && !user.isLocked"> <!-- つまりリモートフォローの場合 -->
<span v-if="full">{{ $ts.processing }}</span><i class="fas fa-spinner fa-pulse"></i> <span v-if="full">{{ i18n.locale.processing }}</span><i class="fas fa-spinner fa-pulse"></i>
</template> </template>
<template v-else-if="isFollowing"> <template v-else-if="isFollowing">
<span v-if="full">{{ $ts.unfollow }}</span><i class="fas fa-minus"></i> <span v-if="full">{{ i18n.locale.unfollow }}</span><i class="fas fa-minus"></i>
</template> </template>
<template v-else-if="!isFollowing && user.isLocked"> <template v-else-if="!isFollowing && user.isLocked">
<span v-if="full">{{ $ts.followRequest }}</span><i class="fas fa-plus"></i> <span v-if="full">{{ i18n.locale.followRequest }}</span><i class="fas fa-plus"></i>
</template> </template>
<template v-else-if="!isFollowing && !user.isLocked"> <template v-else-if="!isFollowing && !user.isLocked">
<span v-if="full">{{ $ts.follow }}</span><i class="fas fa-plus"></i> <span v-if="full">{{ i18n.locale.follow }}</span><i class="fas fa-plus"></i>
</template> </template>
</template> </template>
<template v-else> <template v-else>
<span v-if="full">{{ $ts.processing }}</span><i class="fas fa-spinner fa-pulse fa-fw"></i> <span v-if="full">{{ i18n.locale.processing }}</span><i class="fas fa-spinner fa-pulse fa-fw"></i>
</template> </template>
</button> </button>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent, markRaw } from 'vue'; import { onBeforeUnmount, onMounted, ref } from 'vue';
import * as Misskey from 'misskey-js';
import * as os from '@/os'; import * as os from '@/os';
import { stream } from '@/stream'; import { stream } from '@/stream';
import { i18n } from '@/i18n';
export default defineComponent({ const props = withDefaults(defineProps<{
props: { user: Misskey.entities.UserDetailed,
user: { full?: boolean,
type: Object, large?: boolean,
required: true }>(), {
}, full: false,
full: { large: false,
type: Boolean, });
required: false,
default: false,
},
large: {
type: Boolean,
required: false,
default: false,
},
},
data() { const isFollowing = ref(props.user.isFollowing);
return { const hasPendingFollowRequestFromYou = ref(props.user.hasPendingFollowRequestFromYou);
isFollowing: this.user.isFollowing, const wait = ref(false);
hasPendingFollowRequestFromYou: this.user.hasPendingFollowRequestFromYou, const connection = stream.useChannel('main');
wait: false,
connection: null,
};
},
created() { if (props.user.isFollowing == null) {
//
if (this.user.isFollowing == null) {
os.api('users/show', { os.api('users/show', {
userId: this.user.id userId: props.user.id
}).then(u => { }).then(u => {
this.isFollowing = u.isFollowing; isFollowing.value = u.isFollowing;
this.hasPendingFollowRequestFromYou = u.hasPendingFollowRequestFromYou; hasPendingFollowRequestFromYou.value = u.hasPendingFollowRequestFromYou;
}); });
}
function onFollowChange(user: Misskey.entities.UserDetailed) {
if (user.id == props.user.id) {
isFollowing.value = user.isFollowing;
hasPendingFollowRequestFromYou.value = user.hasPendingFollowRequestFromYou;
} }
}, }
mounted() { async function onClick() {
this.connection = markRaw(stream.useChannel('main')); wait.value = true;
this.connection.on('follow', this.onFollowChange);
this.connection.on('unfollow', this.onFollowChange);
},
beforeUnmount() {
this.connection.dispose();
},
methods: {
onFollowChange(user) {
if (user.id == this.user.id) {
this.isFollowing = user.isFollowing;
this.hasPendingFollowRequestFromYou = user.hasPendingFollowRequestFromYou;
}
},
async onClick() {
this.wait = true;
try { try {
if (this.isFollowing) { if (isFollowing.value) {
const { canceled } = await os.confirm({ const { canceled } = await os.confirm({
type: 'warning', type: 'warning',
text: this.$t('unfollowConfirm', { name: this.user.name || this.user.username }), text: i18n.t('unfollowConfirm', { name: props.user.name || props.user.username }),
}); });
if (canceled) return; if (canceled) return;
await os.api('following/delete', { await os.api('following/delete', {
userId: this.user.id userId: props.user.id
}); });
} else { } else {
if (this.hasPendingFollowRequestFromYou) { if (hasPendingFollowRequestFromYou.value) {
await os.api('following/requests/cancel', { await os.api('following/requests/cancel', {
userId: this.user.id userId: props.user.id
}); });
} else if (this.user.isLocked) { } else if (props.user.isLocked) {
await os.api('following/create', { await os.api('following/create', {
userId: this.user.id userId: props.user.id
}); });
this.hasPendingFollowRequestFromYou = true; hasPendingFollowRequestFromYou.value = true;
} else { } else {
await os.api('following/create', { await os.api('following/create', {
userId: this.user.id userId: props.user.id
}); });
this.hasPendingFollowRequestFromYou = true; hasPendingFollowRequestFromYou.value = true;
} }
} }
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} finally { } finally {
this.wait = false; wait.value = false;
}
}
} }
}
onMounted(() => {
connection.on('follow', onFollowChange);
connection.on('unfollow', onFollowChange);
});
onBeforeUnmount(() => {
connection.dispose();
}); });
</script> </script>

View File

@ -2,72 +2,64 @@
<XModalWindow ref="dialog" <XModalWindow ref="dialog"
:width="370" :width="370"
:height="400" :height="400"
@close="$refs.dialog.close()" @close="dialog.close()"
@closed="$emit('closed')" @closed="emit('closed')"
> >
<template #header>{{ $ts.forgotPassword }}</template> <template #header>{{ i18n.locale.forgotPassword }}</template>
<form v-if="$instance.enableEmail" class="bafeceda" @submit.prevent="onSubmit"> <form v-if="instance.enableEmail" class="bafeceda" @submit.prevent="onSubmit">
<div class="main _formRoot"> <div class="main _formRoot">
<MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required> <MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required>
<template #label>{{ $ts.username }}</template> <template #label>{{ i18n.locale.username }}</template>
<template #prefix>@</template> <template #prefix>@</template>
</MkInput> </MkInput>
<MkInput v-model="email" class="_formBlock" type="email" spellcheck="false" required> <MkInput v-model="email" class="_formBlock" type="email" spellcheck="false" required>
<template #label>{{ $ts.emailAddress }}</template> <template #label>{{ i18n.locale.emailAddress }}</template>
<template #caption>{{ $ts._forgotPassword.enterEmail }}</template> <template #caption>{{ i18n.locale._forgotPassword.enterEmail }}</template>
</MkInput> </MkInput>
<MkButton class="_formBlock" type="submit" :disabled="processing" primary style="margin: 0 auto;">{{ $ts.send }}</MkButton> <MkButton class="_formBlock" type="submit" :disabled="processing" primary style="margin: 0 auto;">{{ i18n.locale.send }}</MkButton>
</div> </div>
<div class="sub"> <div class="sub">
<MkA to="/about" class="_link">{{ $ts._forgotPassword.ifNoEmail }}</MkA> <MkA to="/about" class="_link">{{ i18n.locale._forgotPassword.ifNoEmail }}</MkA>
</div> </div>
</form> </form>
<div v-else> <div v-else class="bafecedb">
{{ $ts._forgotPassword.contactAdmin }} {{ i18n.locale._forgotPassword.contactAdmin }}
</div> </div>
</XModalWindow> </XModalWindow>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent } from 'vue'; import { } from 'vue';
import XModalWindow from '@/components/ui/modal-window.vue'; import XModalWindow from '@/components/ui/modal-window.vue';
import MkButton from '@/components/ui/button.vue'; import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue'; import MkInput from '@/components/form/input.vue';
import * as os from '@/os'; import * as os from '@/os';
import { instance } from '@/instance';
import { i18n } from '@/i18n';
export default defineComponent({ const emit = defineEmits<{
components: { (e: 'done'): void;
XModalWindow, (e: 'closed'): void;
MkButton, }>();
MkInput,
},
emits: ['done', 'closed'], let dialog: InstanceType<typeof XModalWindow> = $ref();
data() { let username = $ref('');
return { let email = $ref('');
username: '', let processing = $ref(false);
email: '',
processing: false,
};
},
methods: { async function onSubmit() {
async onSubmit() { processing = true;
this.processing = true;
await os.apiWithDialog('request-reset-password', { await os.apiWithDialog('request-reset-password', {
username: this.username, username,
email: this.email, email,
}); });
emit('done');
this.$emit('done'); dialog.close();
this.$refs.dialog.close(); }
}
}
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -81,4 +73,8 @@ export default defineComponent({
padding: 24px; padding: 24px;
} }
} }
.bafecedb {
padding: 24px;
}
</style> </style>

View File

@ -541,7 +541,7 @@ export const uploads = ref<{
img: string; img: string;
}[]>([]); }[]>([]);
export function upload(file: File, folder?: any, name?: string) { export function upload(file: File, folder?: any, name?: string): Promise<Misskey.entities.DriveFile> {
if (folder && typeof folder == 'object') folder = folder.id; if (folder && typeof folder == 'object') folder = folder.id;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {

View File

@ -1,7 +1,11 @@
// initial converted from https://github.com/muan/emojilib/commit/242fe68be86ed6536843b83f7e32f376468b38fb export const unicodeEmojiCategories = ['face', 'people', 'animals_and_nature', 'food_and_drink', 'activity', 'travel_and_places', 'objects', 'symbols', 'flags'] as const;
export const emojilist = require('../emojilist.json') as {
export type UnicodeEmojiDef = {
name: string; name: string;
keywords: string[]; keywords: string[];
char: string; char: string;
category: 'people' | 'animals_and_nature' | 'food_and_drink' | 'activity' | 'travel_and_places' | 'objects' | 'symbols' | 'flags'; category: typeof unicodeEmojiCategories[number];
}[]; }
// initial converted from https://github.com/muan/emojilib/commit/242fe68be86ed6536843b83f7e32f376468b38fb
export const emojilist = require('../emojilist.json') as UnicodeEmojiDef[];