forked from FoundKeyGang/FoundKey
Compare commits
18 Commits
1274af05a4
...
43f5f7f839
Author | SHA1 | Date |
---|---|---|
Puniko | 43f5f7f839 | |
Johann150 | af49f811c1 | |
Johann150 | 5391ae4a1b | |
Johann150 | 3cf728a664 | |
Johann150 | 742fa37e2b | |
Johann150 | 00332ed37f | |
Johann150 | ae0a7b668f | |
Richard "EpicKitty" Bowey | 79a9b04d25 | |
Johann150 | 32beda4344 | |
Johann150 | d6837814d9 | |
Johann150 | 02aaee6050 | |
Johann150 | d26e2588e3 | |
Johann150 | df9064c284 | |
Johann150 | f7c4107ca4 | |
Johann150 | c983c4860c | |
Johann150 | d96070bc80 | |
Johann150 | 240cf98920 | |
Johann150 | 7b39483966 |
|
@ -18,7 +18,13 @@ const ev = new Xev();
|
|||
* Init process
|
||||
*/
|
||||
export async function boot(): Promise<void> {
|
||||
process.title = `FoundKey (${cluster.isPrimary ? 'master' : 'worker'})`;
|
||||
if (envOption.disableClustering) {
|
||||
process.title = "Foundkey";
|
||||
} else if (cluster.isPrimary) {
|
||||
process.title = "Foundkey (master)";
|
||||
} else if (cluster.isWorker) {
|
||||
process.title = `Foundkey (${process.env.mode})`;
|
||||
}
|
||||
|
||||
if (cluster.isPrimary || envOption.disableClustering) {
|
||||
await masterMain();
|
||||
|
|
|
@ -104,6 +104,7 @@ import * as ep___clips_show from './endpoints/clips/show.js';
|
|||
import * as ep___clips_update from './endpoints/clips/update.js';
|
||||
import * as ep___drive from './endpoints/drive.js';
|
||||
import * as ep___drive_files from './endpoints/drive/files.js';
|
||||
import * as ep___drive_show from './endpoints/drive/show.js';
|
||||
import * as ep___drive_files_attachedNotes from './endpoints/drive/files/attached-notes.js';
|
||||
import * as ep___drive_files_checkExistence from './endpoints/drive/files/check-existence.js';
|
||||
import * as ep___drive_files_create from './endpoints/drive/files/create.js';
|
||||
|
@ -400,6 +401,7 @@ const eps = [
|
|||
['clips/update', ep___clips_update],
|
||||
['drive', ep___drive],
|
||||
['drive/files', ep___drive_files],
|
||||
['drive/show', ep___drive_show],
|
||||
['drive/files/attached-notes', ep___drive_files_attachedNotes],
|
||||
['drive/files/check-existence', ep___drive_files_checkExistence],
|
||||
['drive/files/create', ep___drive_files_create],
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
import { DriveFiles, DriveFolders } from '@/models/index.js';
|
||||
import define from '../../define.js';
|
||||
import { makePaginationQuery } from '../../common/make-pagination-query.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['drive'],
|
||||
|
||||
description: "Lists all folders and files in the authenticated user's drive. Folders are always listed first. The limit, if specified, is applied over the total number of elements.",
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'read:drive',
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
oneOf: [{
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'DriveFile',
|
||||
}, {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'DriveFolder',
|
||||
}],
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
folderId: { type: 'string', format: 'misskey:id', nullable: true, default: null },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const foldersQuery = makePaginationQuery(DriveFolders.createQueryBuilder('folder'), ps.sinceId, ps.untilId)
|
||||
.andWhere('folder.userId = :userId', { userId: user.id });
|
||||
const filesQuery = makePaginationQuery(DriveFiles.createQueryBuilder('file'), ps.sinceId, ps.untilId)
|
||||
.andWhere('file.userId = :userId', { userId: user.id });
|
||||
|
||||
if (ps.folderId) {
|
||||
foldersQuery.andWhere('folder.parentId = :parentId', { parentId: ps.folderId });
|
||||
filesQuery.andWhere('file.folderId = :folderId', { folderId: ps.folderId });
|
||||
} else {
|
||||
foldersQuery.andWhere('folder.parentId IS NULL');
|
||||
filesQuery.andWhere('file.folderId IS NULL');
|
||||
}
|
||||
|
||||
const folders = await foldersQuery.take(ps.limit).getMany();
|
||||
|
||||
const [files, ...packedFolders] = await Promise.all([
|
||||
filesQuery.take(ps.limit - folders.length).getMany(),
|
||||
...(folders.map(folder => DriveFolders.pack(folder))),
|
||||
]);
|
||||
|
||||
const packedFiles = await DriveFiles.packMany(files, { detail: false, self: true });
|
||||
|
||||
return [
|
||||
...packedFolders,
|
||||
...packedFiles,
|
||||
];
|
||||
});
|
|
@ -17,7 +17,7 @@ export const meta = {
|
|||
|
||||
kind: 'write:following',
|
||||
|
||||
errors: ['FOLLOWER_IS_YOURSELF', 'NO_SUCH_USER', 'NOT_FOLLOWING'],
|
||||
errors: ['FOLLOWER_IS_YOURSELF', 'NO_SUCH_USER', 'NOT_FOLLOWED'],
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
|
@ -53,7 +53,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
followeeId: followee.id,
|
||||
});
|
||||
|
||||
if (!exist) throw new ApiError('NOT_FOLLOWING');
|
||||
if (!exist) throw new ApiError('NOT_FOLLOWED');
|
||||
|
||||
await deleteFollowing(follower, followee);
|
||||
|
||||
|
|
|
@ -340,6 +340,10 @@ export const errors: Record<string, { message: string, httpStatusCode: number }>
|
|||
message: 'You are not following that user.',
|
||||
httpStatusCode: 409,
|
||||
},
|
||||
NOT_FOLLOWED: {
|
||||
message: 'You are not followed by that user.',
|
||||
httpStatusCode: 409,
|
||||
},
|
||||
NOT_LIKED: {
|
||||
message: 'You have not liked that page.',
|
||||
httpStatusCode: 409,
|
||||
|
|
|
@ -52,7 +52,7 @@ const props = withDefaults(defineProps<{
|
|||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'chosen', r: foundkey.entities.DriveFile): void;
|
||||
(ev: 'chosen', r: foundkey.entities.DriveFile, extendSelection: boolean): void;
|
||||
(ev: 'dragstart'): void;
|
||||
(ev: 'dragend'): void;
|
||||
}>();
|
||||
|
@ -95,9 +95,7 @@ function getMenu(): MenuItem[] {
|
|||
|
||||
function onClick(ev: MouseEvent): void {
|
||||
if (props.selectMode) {
|
||||
emit('chosen', props.file);
|
||||
} else {
|
||||
os.popupMenu(getMenu(), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined);
|
||||
emit('chosen', props.file, ev.ctrlKey);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -330,7 +328,7 @@ async function deleteFile(): Promise<void> {
|
|||
overflow: hidden;
|
||||
|
||||
> .ext {
|
||||
opacity: 0.5;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<template>
|
||||
<div
|
||||
class="rghtznwe"
|
||||
:class="{ draghover }"
|
||||
:class="{ draghover, isSelected }"
|
||||
draggable="true"
|
||||
:title="title"
|
||||
@click="onClick"
|
||||
@click="selected"
|
||||
@contextmenu.stop="onContextmenu"
|
||||
@mouseover="onMouseover"
|
||||
@mouseout="onMouseout"
|
||||
|
@ -15,15 +15,16 @@
|
|||
@dragstart="onDragstart"
|
||||
@dragend="onDragend"
|
||||
>
|
||||
<div class="thumbnail" @click.stop="emit('move', folder)">
|
||||
<i class="fas fa-folder-open fa-fw hover"></i>
|
||||
<i class="fas fa-folder fa-fw"></i>
|
||||
</div>
|
||||
<p class="name">
|
||||
<template v-if="hover"><i class="fas fa-folder-open fa-fw"></i></template>
|
||||
<template v-if="!hover"><i class="fas fa-folder fa-fw"></i></template>
|
||||
{{ folder.name }}
|
||||
</p>
|
||||
<p v-if="defaultStore.state.uploadFolder == folder.id" class="upload">
|
||||
{{ i18n.ts.uploadFolder }}
|
||||
</p>
|
||||
<button v-if="selectMode" class="checkbox _button" :class="{ checked: isSelected }" @click.prevent.stop="checkboxClicked"></button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -44,7 +45,7 @@ const props = withDefaults(defineProps<{
|
|||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'chosen', v: foundkey.entities.DriveFolder): void;
|
||||
(ev: 'chosen', v: foundkey.entities.DriveFolder, extendSelection: boolean): void;
|
||||
(ev: 'move', v: foundkey.entities.DriveFolder): void;
|
||||
(ev: 'upload', file: File, folder: foundkey.entities.DriveFolder);
|
||||
(ev: 'removeFile', v: foundkey.entities.DriveFile['id']): void;
|
||||
|
@ -59,20 +60,10 @@ const isDragging = ref(false);
|
|||
|
||||
const title = computed(() => props.folder.name);
|
||||
|
||||
function checkboxClicked() {
|
||||
emit('chosen', props.folder);
|
||||
}
|
||||
|
||||
function onClick() {
|
||||
emit('move', props.folder);
|
||||
}
|
||||
|
||||
function onMouseover() {
|
||||
hover.value = true;
|
||||
}
|
||||
|
||||
function onMouseout() {
|
||||
hover.value = false;
|
||||
function selected(ev: MouseEvent) {
|
||||
if (props.selectMode) {
|
||||
emit('chosen', props.folder, ev.ctrlKey);
|
||||
}
|
||||
}
|
||||
|
||||
function onDragover(ev: DragEvent) {
|
||||
|
@ -260,30 +251,34 @@ function onContextmenu(ev: MouseEvent) {
|
|||
.rghtznwe {
|
||||
position: relative;
|
||||
padding: 8px;
|
||||
height: 64px;
|
||||
background: var(--driveFolderBg);
|
||||
border-radius: 4px;
|
||||
min-height: 180px;
|
||||
border-radius: 8px;
|
||||
|
||||
&, * {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
*:not(.checkbox) {
|
||||
pointer-events: none;
|
||||
}
|
||||
> .thumbnail {
|
||||
width: 110px;
|
||||
height: 110px;
|
||||
margin: auto;
|
||||
|
||||
> .checkbox {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
right: 8px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: #fff;
|
||||
border: solid 1px #000;
|
||||
/* same style as drive-file-thumbnail.vue */
|
||||
position: relative;
|
||||
display: flex;
|
||||
background: var(--panel);
|
||||
border-radius: 8px;
|
||||
overflow: clip;
|
||||
|
||||
&.checked {
|
||||
background: var(--accent);
|
||||
> i {
|
||||
pointer-events: none;
|
||||
margin: auto;
|
||||
font-size: 33px;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
&:not(:hover) > i.hover,
|
||||
&:hover > i:not(.hover) { display: none; }
|
||||
}
|
||||
|
||||
&.draghover {
|
||||
|
@ -300,23 +295,37 @@ function onContextmenu(ev: MouseEvent) {
|
|||
}
|
||||
}
|
||||
|
||||
> .name {
|
||||
margin: 0;
|
||||
font-size: 0.9em;
|
||||
color: var(--desktopDriveFolderFg);
|
||||
&.isSelected {
|
||||
background: var(--accent);
|
||||
|
||||
> i {
|
||||
margin-right: 4px;
|
||||
margin-left: 2px;
|
||||
text-align: left;
|
||||
&:hover {
|
||||
background: var(--accentLighten);
|
||||
}
|
||||
|
||||
> .name {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
> .thumbnail {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
> .name {
|
||||
display: block;
|
||||
margin: 4px 0 0 0;
|
||||
font-size: 0.8em;
|
||||
text-align: center;
|
||||
word-break: break-all;
|
||||
color: var(--fg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
> .upload {
|
||||
margin: 4px 4px;
|
||||
font-size: 0.8em;
|
||||
text-align: right;
|
||||
color: var(--desktopDriveFolderFg);
|
||||
text-align: center;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -35,31 +35,37 @@
|
|||
@drop.prevent.stop="onDrop"
|
||||
@contextmenu.stop="onContextmenu"
|
||||
>
|
||||
<div ref="contents" class="contents">
|
||||
<MkPagination
|
||||
ref="foldersPaginationElem"
|
||||
:pagination="foldersPagination"
|
||||
class="folders"
|
||||
@loaded="foldersLoading = false"
|
||||
>
|
||||
<template #empty>
|
||||
<!--
|
||||
Don't display anything here if there are no folders,
|
||||
there is a separate check if both paginations are empty.
|
||||
-->
|
||||
{{ null }}
|
||||
</template>
|
||||
<MkPagination
|
||||
ref="paginationElem"
|
||||
:pagination="pagination"
|
||||
class="contents"
|
||||
>
|
||||
<template #empty>
|
||||
<p v-if="folder == null" class="empty"><strong>{{ i18n.ts.emptyDrive }}</strong></p>
|
||||
<p v-else class="empty">{{ i18n.ts.emptyFolder }}</p>
|
||||
</template>
|
||||
|
||||
<template #default="{ items: folders }">
|
||||
<XFolder
|
||||
v-for="(f, i) in folders"
|
||||
<template #default="{ items }">
|
||||
<template v-for="(f, i) in items">
|
||||
<XFile
|
||||
v-if="'size' in f"
|
||||
:key="f.id"
|
||||
v-anim="i"
|
||||
:file="f"
|
||||
:select-mode="select !== 'folder'"
|
||||
:is-selected="selected.some(x => x.id === f.id)"
|
||||
@chosen="choose"
|
||||
@dragstart="isDragSource = true"
|
||||
@dragend="isDragSource = false"
|
||||
/>
|
||||
<XFolder
|
||||
v-else
|
||||
: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"
|
||||
:select-mode="select !== 'file'"
|
||||
:is-selected="selected.some(x => x.id === f.id)"
|
||||
@chosen="choose"
|
||||
@move="move"
|
||||
@upload="upload"
|
||||
@removeFile="removeFile"
|
||||
|
@ -67,46 +73,9 @@
|
|||
@dragstart="isDragSource = true"
|
||||
@dragend="isDragSource = false"
|
||||
/>
|
||||
<!-- 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>
|
||||
</template>
|
||||
</MkPagination>
|
||||
<MkPagination
|
||||
ref="filesPaginationElem"
|
||||
:pagination="filesPagination"
|
||||
class="files"
|
||||
@loaded="filesLoading = false"
|
||||
>
|
||||
<template #empty>
|
||||
<!--
|
||||
Don't display anything here if there are no files,
|
||||
there is a separate check if both paginations are empty.
|
||||
-->
|
||||
{{ null }}
|
||||
</template>
|
||||
|
||||
<template #default="{ items: 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"
|
||||
@dragstart="isDragSource = true"
|
||||
@dragend="isDragSource = false"
|
||||
/>
|
||||
<!-- 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>
|
||||
</template>
|
||||
</MkPagination>
|
||||
<div v-if="empty" class="empty">
|
||||
<p v-if="folder == null"><strong>{{ i18n.ts.emptyDrive }}</strong></p>
|
||||
<p v-else>{{ i18n.ts.emptyFolder }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</div>
|
||||
<div v-if="draghover" class="dropzone"></div>
|
||||
<input ref="fileInput" type="file" accept="*/*" multiple tabindex="-1" @change="onChangeFileInput"/>
|
||||
|
@ -129,7 +98,6 @@ import { uploadFile, uploads } from '@/scripts/upload';
|
|||
|
||||
const props = withDefaults(defineProps<{
|
||||
initialFolder?: foundkey.entities.DriveFolder;
|
||||
type?: string;
|
||||
multiple?: boolean;
|
||||
select?: 'file' | 'folder' | null;
|
||||
}>(), {
|
||||
|
@ -139,19 +107,13 @@ const props = withDefaults(defineProps<{
|
|||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'selected', v: foundkey.entities.DriveFile | foundkey.entities.DriveFolder): void;
|
||||
(ev: 'change-selection', v: foundkey.entities.DriveFile[] | foundkey.entities.DriveFolder[]): void;
|
||||
(ev: 'change-selection', v: Array<foundkey.entities.DriveFile | foundkey.entities.DriveFolder>): void;
|
||||
(ev: 'move-root'): void;
|
||||
(ev: 'cd', v: foundkey.entities.DriveFolder | null): void;
|
||||
(ev: 'open-folder', v: foundkey.entities.DriveFolder): void;
|
||||
}>();
|
||||
|
||||
let foldersPaginationElem = $ref<InstanceType<typeof MkPagination>>();
|
||||
let filesPaginationElem = $ref<InstanceType<typeof MkPagination>>();
|
||||
|
||||
let foldersLoading = $ref<boolean>(true);
|
||||
let filesLoading = $ref<boolean>(true);
|
||||
const empty = $computed(() => !foldersLoading && !filesLoading
|
||||
&& foldersPaginationElem?.items.length === 0 && filesPaginationElem?.items.length === 0);
|
||||
let paginationElem = $ref<InstanceType<typeof MkPagination>>();
|
||||
|
||||
let fileInput = $ref<HTMLInputElement>();
|
||||
|
||||
|
@ -160,8 +122,7 @@ const connection = stream.useChannel('drive');
|
|||
|
||||
let folder = $ref<foundkey.entities.DriveFolder | null>(null);
|
||||
let hierarchyFolders = $ref<foundkey.entities.DriveFolder[]>([]);
|
||||
let selectedFiles = $ref<foundkey.entities.DriveFile[]>([]);
|
||||
let selectedFolders = $ref<foundkey.entities.DriveFolder[]>([]);
|
||||
let selected = $ref<Array<foundkey.entities.DriveFile | foundkey.entities.DriveFolder>>([]);
|
||||
let keepOriginal = $ref<boolean>(defaultStore.state.keepOriginalUploading);
|
||||
|
||||
// ドロップされようとしているか
|
||||
|
@ -171,7 +132,14 @@ let draghover = $ref(false);
|
|||
// (自分自身の階層にドロップできないようにするためのフラグ)
|
||||
let isDragSource = $ref(false);
|
||||
|
||||
watch($$(folder), () => emit('cd', folder));
|
||||
watch($$(folder), () => {
|
||||
emit('cd', folder)
|
||||
if (props.select === 'folder') {
|
||||
// convenience: entering a folder selects it
|
||||
selected = [folder];
|
||||
emit('change-selection', selected);
|
||||
}
|
||||
});
|
||||
|
||||
function onStreamDriveFileCreated(file: foundkey.entities.DriveFile) {
|
||||
addFile(file, true);
|
||||
|
@ -210,9 +178,8 @@ function onStreamDriveFolderDeleted(folderId: string) {
|
|||
function onDragover(ev: DragEvent): any {
|
||||
if (!ev.dataTransfer) return;
|
||||
|
||||
// ドラッグ元が自分自身の所有するアイテムだったら
|
||||
if (isDragSource) {
|
||||
// 自分自身にはドロップさせない
|
||||
// We are the drag source, do not allow to drop.
|
||||
ev.dataTransfer.dropEffect = 'none';
|
||||
return;
|
||||
}
|
||||
|
@ -257,17 +224,16 @@ function onDrop(ev: DragEvent): any {
|
|||
|
||||
if (!ev.dataTransfer) return;
|
||||
|
||||
// ドロップされてきたものがファイルだったら
|
||||
const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
|
||||
const driveFolder = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
|
||||
|
||||
if (ev.dataTransfer.files.length > 0) {
|
||||
// dropping operating system files
|
||||
for (const file of Array.from(ev.dataTransfer.files)) {
|
||||
upload(file, folder);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
//#region ドライブのファイル
|
||||
const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
|
||||
if (driveFile != null && driveFile !== '') {
|
||||
} else if (driveFile != null && driveFile !== '') {
|
||||
// dropping drive files
|
||||
const file = JSON.parse(driveFile);
|
||||
|
||||
// cannot move file within parent folder
|
||||
|
@ -278,18 +244,14 @@ function onDrop(ev: DragEvent): any {
|
|||
fileId: file.id,
|
||||
folderId: folder?.id ?? null,
|
||||
});
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region ドライブのフォルダ
|
||||
const driveFolder = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
|
||||
if (driveFolder != null && driveFolder !== '') {
|
||||
} else if (driveFolder != null && driveFolder !== '') {
|
||||
// dropping drive folders
|
||||
const droppedFolder = JSON.parse(driveFolder);
|
||||
|
||||
// cannot move folder into itself
|
||||
if (droppedFolder.id === folder?.id) return false;
|
||||
// cannot move folder within parent folder
|
||||
if (foldersPaginationElem.items.some(f => f.id === droppedFolder.id)) return false;
|
||||
if (folder.id === droppedFolder.parentId) return false;
|
||||
|
||||
removeFolder(droppedFolder.id);
|
||||
os.api('drive/folders/update', {
|
||||
|
@ -311,7 +273,6 @@ function onDrop(ev: DragEvent): any {
|
|||
}
|
||||
});
|
||||
}
|
||||
//#endregion
|
||||
}
|
||||
|
||||
function selectLocalFile() {
|
||||
|
@ -346,8 +307,6 @@ function createFolder() {
|
|||
os.api('drive/folders/create', {
|
||||
name,
|
||||
parentId: folder?.id ?? undefined,
|
||||
}).then(createdFolder => {
|
||||
addFolder(createdFolder, true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -404,49 +363,52 @@ function upload(file: File, folderToUpload?: foundkey.entities.DriveFolder | nul
|
|||
uploadFile(file, folderToUpload?.id ?? null, undefined, keepOriginal);
|
||||
}
|
||||
|
||||
function chooseFile(file: foundkey.entities.DriveFile) {
|
||||
const isAlreadySelected = selectedFiles.some(f => f.id === file.id);
|
||||
if (props.multiple) {
|
||||
if (isAlreadySelected) {
|
||||
selectedFiles = selectedFiles.filter(f => f.id !== file.id);
|
||||
} else {
|
||||
selectedFiles.push(file);
|
||||
}
|
||||
emit('change-selection', selectedFiles);
|
||||
} else {
|
||||
if (isAlreadySelected) {
|
||||
emit('selected', file);
|
||||
} else {
|
||||
selectedFiles = [file];
|
||||
emit('change-selection', [file]);
|
||||
}
|
||||
}
|
||||
}
|
||||
function choose(choice: foundkey.entities.DriveFile | foundkey.entities.DriveFolder, extendSelection: boolean) {
|
||||
const alreadySelected = selected.some(f => f.id === file.id);
|
||||
|
||||
function chooseFolder(folderToChoose: foundkey.entities.DriveFolder) {
|
||||
const isAlreadySelected = selectedFolders.some(f => f.id === folderToChoose.id);
|
||||
if (props.multiple) {
|
||||
if (isAlreadySelected) {
|
||||
selectedFolders = selectedFolders.filter(f => f.id !== folderToChoose.id);
|
||||
const action = (() => {
|
||||
if (props.select != null) {
|
||||
// file picker mode, extendSelection is disregarded
|
||||
if (props.multiple && alreadySelected) {
|
||||
return 'remove';
|
||||
} else if (props.multiple) {
|
||||
return 'add';
|
||||
} else if (!props.multiple && alreadySelected) {
|
||||
return 'emit';
|
||||
} else {
|
||||
return 'set';
|
||||
}
|
||||
} else {
|
||||
selectedFolders.push(folderToChoose);
|
||||
}
|
||||
emit('change-selection', selectedFolders);
|
||||
} else {
|
||||
if (isAlreadySelected) {
|
||||
emit('selected', folderToChoose);
|
||||
} else {
|
||||
selectedFolders = [folderToChoose];
|
||||
emit('change-selection', [folderToChoose]);
|
||||
// explorer mode, props.multiple is disregarded
|
||||
if (extendSelection && alreadySelected) {
|
||||
return 'remove';
|
||||
} else if (extendSelection) {
|
||||
return 'add';
|
||||
} else if (!alreadySelected) {
|
||||
return 'set';
|
||||
}
|
||||
// already selected && ! extend selection is a noop
|
||||
}
|
||||
})();
|
||||
|
||||
switch (action) {
|
||||
case 'emit':
|
||||
emit('selected', choice);
|
||||
return; // don't emit the change-selection event
|
||||
case 'add':
|
||||
selected.push(choice);
|
||||
break;
|
||||
case 'set':
|
||||
selected = [choice];
|
||||
break;
|
||||
case 'remove':
|
||||
selected = selected.filter(f => f.id !== choice.id);
|
||||
break;
|
||||
}
|
||||
emit('change-selection', selected);
|
||||
}
|
||||
|
||||
function move(target?: string | foundkey.entities.DriveFolder) {
|
||||
// reset loading state
|
||||
foldersLoading = true;
|
||||
filesLoading = true;
|
||||
|
||||
if (!target) {
|
||||
goRoot();
|
||||
return;
|
||||
|
@ -475,13 +437,13 @@ function addFolder(folderToAdd: foundkey.entities.DriveFolder, unshift = false)
|
|||
const current = folder?.id ?? null;
|
||||
if (current !== folderToAdd.parentId) return;
|
||||
|
||||
const exist = foldersPaginationElem.items.some(f => f.id === folderToAdd.id);
|
||||
const exist = paginationElem.items.some(f => f.id === folderToAdd.id);
|
||||
if (exist) {
|
||||
foldersPaginationElem.updateItem(folderToAdd.id, () => folderToAdd);
|
||||
paginationElem.updateItem(folderToAdd.id, () => folderToAdd);
|
||||
} else if (unshift) {
|
||||
foldersPaginationElem.prepend(folderToAdd);
|
||||
paginationElem.prepend(folderToAdd);
|
||||
} else {
|
||||
foldersPaginationElem.append(folderToAdd);
|
||||
paginationElem.append(folderToAdd);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -489,24 +451,24 @@ function addFile(fileToAdd: foundkey.entities.DriveFile, unshift = false) {
|
|||
const current = folder?.id ?? null;
|
||||
if (current !== fileToAdd.folderId) return;
|
||||
|
||||
const exist = filesPaginationElem.items.some(f => f.id === fileToAdd.id);
|
||||
const exist = paginationElem.items.some(f => f.id === fileToAdd.id);
|
||||
if (exist) {
|
||||
filesPaginationElem.updateItem(fileToAdd.id, () => fileToAdd);
|
||||
paginationElem.updateItem(fileToAdd.id, () => fileToAdd);
|
||||
} else if (unshift) {
|
||||
filesPaginationElem.prepend(fileToAdd);
|
||||
paginationElem.prepend(fileToAdd);
|
||||
} else {
|
||||
filesPaginationElem.append(fileToAdd);
|
||||
paginationElem.append(fileToAdd);
|
||||
}
|
||||
}
|
||||
|
||||
function removeFolder(folderToRemove: foundkey.entities.DriveFolder | string): void {
|
||||
const folderIdToRemove = typeof folderToRemove === 'object' ? folderToRemove.id : folderToRemove;
|
||||
foldersPaginationElem.removeItem(item => item.id === folderIdToRemove);
|
||||
paginationElem.removeItem(item => item.id === folderIdToRemove);
|
||||
}
|
||||
|
||||
function removeFile(fileToRemove: foundkey.entities.DriveFile | string): void {
|
||||
const fileIdToRemove = typeof fileToRemove === 'object' ? fileToRemove.id : fileToRemove;
|
||||
filesPaginationElem.removeItem(item => item.id === fileIdToRemove);
|
||||
paginationElem.removeItem(item => item.id === fileIdToRemove);
|
||||
}
|
||||
|
||||
function goRoot() {
|
||||
|
@ -518,23 +480,14 @@ function goRoot() {
|
|||
emit('move-root');
|
||||
}
|
||||
|
||||
const foldersPagination = {
|
||||
endpoint: 'drive/folders' as const,
|
||||
const pagination = {
|
||||
endpoint: 'drive/show' as const,
|
||||
limit: 30,
|
||||
params: computed(() => ({
|
||||
folderId: folder?.id ?? null,
|
||||
})),
|
||||
};
|
||||
|
||||
const filesPagination = {
|
||||
endpoint: 'drive/files' as const,
|
||||
limit: 30,
|
||||
params: computed(() => ({
|
||||
folderId: folder?.id ?? null,
|
||||
type: props.type,
|
||||
})),
|
||||
};
|
||||
|
||||
function getMenu() {
|
||||
return [{
|
||||
type: 'switch',
|
||||
|
@ -678,37 +631,17 @@ onBeforeUnmount(() => {
|
|||
}
|
||||
|
||||
> .contents {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: .5em;
|
||||
}
|
||||
|
||||
> .folders,
|
||||
> .files {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
> .folder,
|
||||
> .file {
|
||||
flex-grow: 1;
|
||||
width: 128px;
|
||||
margin: 4px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
> .padding {
|
||||
flex-grow: 1;
|
||||
pointer-events: none;
|
||||
width: 128px + 8px;
|
||||
}
|
||||
}
|
||||
|
||||
> .empty {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
|
||||
> p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
> .empty {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
{{ formatTime(duration) }}
|
||||
</span>
|
||||
<input class="volume" type="range" min="0" max="1" step="0.1" v-model="volume" @input="player.setVolume(volume)" />
|
||||
<a class="download" :href="src" target="_blank">
|
||||
<a class="download" :href="src.url" target="_blank">
|
||||
<i class="fas fa-download"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
|
||||
<FormSection>
|
||||
<div class="_formLinksGrid">
|
||||
<FormLink to="/settings/theme/manage"><template #icon><i class="fas fa-folder-open"></i></template>{{ i18n.ts._theme.manage }}<template #suffix>{{ themesCount }}</template></FormLink>
|
||||
<FormLink to="/settings/theme/manage"><template #icon><i class="fas fa-folder-open"></i></template>{{ i18n.ts._theme.manage }}<template #suffix>{{ installedThemes.length }}</template></FormLink>
|
||||
<FormLink to="https://assets.misskey.io/theme/list" external><template #icon><i class="fas fa-globe"></i></template>{{ i18n.ts._theme.explore }}</FormLink>
|
||||
<FormLink to="/settings/theme/install"><template #icon><i class="fas fa-download"></i></template>{{ i18n.ts._theme.install }}</FormLink>
|
||||
<FormLink to="/theme-editor"><template #icon><i class="fas fa-paint-roller"></i></template>{{ i18n.ts._theme.make }}</FormLink>
|
||||
|
@ -71,7 +71,7 @@ import FormSelect from '@/components/form/select.vue';
|
|||
import FormSection from '@/components/form/section.vue';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import { getBuiltinThemesRef } from '@/scripts/theme';
|
||||
import { getBuiltinThemes } from '@/scripts/theme';
|
||||
import { selectFile } from '@/scripts/select-file';
|
||||
import { isDeviceDarkmode } from '@/scripts/is-device-darkmode';
|
||||
import { ColdDeviceStorage , defaultStore } from '@/store';
|
||||
|
@ -81,38 +81,38 @@ import { uniqueBy } from '@/scripts/array';
|
|||
import { fetchThemes, getThemes } from '@/theme-store';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const installedThemes = ref(getThemes());
|
||||
const builtinThemes = getBuiltinThemesRef();
|
||||
const [installedThemes, builtinThemes] = await Promise.all([fetchThemes(), getBuiltinThemes()]);
|
||||
const instanceThemes = [];
|
||||
|
||||
if (instance.defaultLightTheme != null) instanceThemes.push(JSON5.parse(instance.defaultLightTheme));
|
||||
if (instance.defaultDarkTheme != null) instanceThemes.push(JSON5.parse(instance.defaultDarkTheme));
|
||||
|
||||
const themes = computed(() => uniqueBy([ ...instanceThemes, ...builtinThemes.value, ...installedThemes.value ], theme => theme.id));
|
||||
const darkThemes = computed(() => themes.value.filter(t => t.base === 'dark' || t.kind === 'dark'));
|
||||
const lightThemes = computed(() => themes.value.filter(t => t.base === 'light' || t.kind === 'light'));
|
||||
const darkTheme = ColdDeviceStorage.ref('darkTheme');
|
||||
const themes = uniqueBy([...installedThemes, ...instanceThemes, ...builtinThemes], theme => theme.id);
|
||||
const darkThemes = themes.filter(t => t.base === 'dark' || t.kind === 'dark');
|
||||
const lightThemes = themes.filter(t => t.base === 'light' || t.kind === 'light');
|
||||
let darkTheme = $ref(ColdDeviceStorage.get('darkTheme'));
|
||||
const darkThemeId = computed({
|
||||
get() {
|
||||
return darkTheme.value.id;
|
||||
return darkTheme.id;
|
||||
},
|
||||
set(id) {
|
||||
ColdDeviceStorage.set('darkTheme', themes.value.find(x => x.id === id));
|
||||
darkTheme = themes.find(x => x.id === id);
|
||||
ColdDeviceStorage.set('darkTheme', darkTheme);
|
||||
},
|
||||
});
|
||||
const lightTheme = ColdDeviceStorage.ref('lightTheme');
|
||||
let lightTheme = $ref(ColdDeviceStorage.get('lightTheme'));
|
||||
const lightThemeId = computed({
|
||||
get() {
|
||||
return lightTheme.value.id;
|
||||
return lightTheme.id;
|
||||
},
|
||||
set(id) {
|
||||
ColdDeviceStorage.set('lightTheme', themes.value.find(x => x.id === id));
|
||||
lightTheme = themes.find(x => x.id === id);
|
||||
ColdDeviceStorage.set('lightTheme', lightTheme);
|
||||
},
|
||||
});
|
||||
const darkMode = computed(defaultStore.makeGetterSetter('darkMode'));
|
||||
const syncDeviceDarkMode = computed(ColdDeviceStorage.makeGetterSetter('syncDeviceDarkMode'));
|
||||
const wallpaper = ref(localStorage.getItem('wallpaper'));
|
||||
const themesCount = installedThemes.value.length;
|
||||
|
||||
watch(syncDeviceDarkMode, () => {
|
||||
if (syncDeviceDarkMode.value) {
|
||||
|
@ -129,16 +129,6 @@ watch(wallpaper, () => {
|
|||
location.reload();
|
||||
});
|
||||
|
||||
onActivated(() => {
|
||||
fetchThemes().then(() => {
|
||||
installedThemes.value = getThemes();
|
||||
});
|
||||
});
|
||||
|
||||
fetchThemes().then(() => {
|
||||
installedThemes.value = getThemes();
|
||||
});
|
||||
|
||||
function setWallpaper(event) {
|
||||
selectFile(event.currentTarget ?? event.target, null).then(file => {
|
||||
wallpaper.value = file.url;
|
||||
|
|
|
@ -8,21 +8,22 @@ export function getThemes(): Theme[] {
|
|||
return JSON.parse(localStorage.getItem(lsCacheKey) || '[]');
|
||||
}
|
||||
|
||||
export async function fetchThemes(): Promise<void> {
|
||||
if ($i == null) return;
|
||||
export async function fetchThemes(): Promise<Theme[]> {
|
||||
if ($i == null) return [];
|
||||
|
||||
try {
|
||||
const themes = await api('i/registry/get', { scope: ['client'], key: 'themes' });
|
||||
localStorage.setItem(lsCacheKey, JSON.stringify(themes));
|
||||
return themes;
|
||||
} catch (err) {
|
||||
if (err.code === 'NO_SUCH_KEY') return;
|
||||
if (err.code === 'NO_SUCH_KEY') return [];
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export async function addTheme(theme: Theme): Promise<void> {
|
||||
await fetchThemes();
|
||||
const themes = getThemes().concat(theme);
|
||||
const themes = await fetchThemes()
|
||||
.then(themes => themes.concat(theme));
|
||||
await api('i/registry/set', { scope: ['client'], key: 'themes', value: themes });
|
||||
localStorage.setItem(lsCacheKey, JSON.stringify(themes));
|
||||
}
|
||||
|
|
|
@ -64,7 +64,6 @@
|
|||
inputBorder: 'rgba(255, 255, 255, 0.1)',
|
||||
inputBorderHover: 'rgba(255, 255, 255, 0.2)',
|
||||
listItemHoverBg: 'rgba(255, 255, 255, 0.03)',
|
||||
driveFolderBg: ':alpha<0.3<@accent',
|
||||
wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
|
||||
badge: '#31b1ce',
|
||||
messageBg: '@bg',
|
||||
|
|
|
@ -64,7 +64,6 @@
|
|||
inputBorder: 'rgba(0, 0, 0, 0.1)',
|
||||
inputBorderHover: 'rgba(0, 0, 0, 0.2)',
|
||||
listItemHoverBg: 'rgba(0, 0, 0, 0.03)',
|
||||
driveFolderBg: ':alpha<0.3<@accent',
|
||||
wallpaperOverlay: 'rgba(255, 255, 255, 0.5)',
|
||||
badge: '#31b1ce',
|
||||
messageBg: '@bg',
|
||||
|
|
|
@ -46,7 +46,6 @@
|
|||
buttonHoverBg: 'rgba(255, 255, 255, 0.1)',
|
||||
buttonGradateA: '@accent',
|
||||
buttonGradateB: ':hue<-20<@accent',
|
||||
driveFolderBg: ':alpha<0.3<@accent',
|
||||
fgHighlighted: ':lighten<3<@fg',
|
||||
panelHeaderBg: ':lighten<3<@panel',
|
||||
panelHeaderFg: '@fg',
|
||||
|
|
|
@ -66,7 +66,6 @@
|
|||
navIndicator: '@indicator',
|
||||
accentLighten: ':lighten<10<@accent',
|
||||
buttonHoverBg: 'rgba(255, 255, 255, 0.1)',
|
||||
driveFolderBg: ':alpha<0.3<@accent',
|
||||
fgHighlighted: ':lighten<3<@fg',
|
||||
fgTransparent: ':alpha<0.5<@fg',
|
||||
panelHeaderBg: ':lighten<3<@panel',
|
||||
|
|
|
@ -47,7 +47,6 @@
|
|||
navIndicator: '@accent',
|
||||
accentLighten: ':lighten<10<@accent',
|
||||
buttonHoverBg: 'rgba(0, 0, 0, 0.1)',
|
||||
driveFolderBg: ':alpha<0.3<@accent',
|
||||
fgHighlighted: ':darken<3<@fg',
|
||||
fgTransparent: ':alpha<0.5<@fg',
|
||||
panelHeaderBg: ':lighten<3<@panel',
|
||||
|
|
Loading…
Reference in New Issue