Compare commits

...

18 Commits

Author SHA1 Message Date
Puniko 43f5f7f839 fix download button in wavesurfer 2023-03-19 12:46:07 +01:00
Johann150 af49f811c1
client: refactor away a variable 2023-03-19 10:26:51 +01:00
Johann150 5391ae4a1b
client: list custom themes first 2023-03-19 10:26:37 +01:00
Johann150 3cf728a664
client: don't use refs for themes
I am not sure why ref's were used here before, since all changes to
those refs could only be from the same page the user was already on.
It seems the ColdDeviceStorage.ref was causing some circular thingies
that went wrong.

closes FoundKeyGang/FoundKey#353

Changelog: Fixed
2023-03-19 10:15:19 +01:00
Johann150 742fa37e2b
server: show worker mode in process name
Changelog: Added
2023-03-19 09:39:33 +01:00
Johann150 00332ed37f
client: fix drive file selection
closes FoundKeyGang/FoundKey#358
2023-03-19 09:28:31 +01:00
Johann150 ae0a7b668f
client: return themes when fetching them 2023-03-18 10:34:25 +01:00
Richard "EpicKitty" Bowey 79a9b04d25
put the migration in its place 2023-03-17 16:16:41 +01:00
Johann150 32beda4344
server: improve error message for invalidating follows
This error was broken out to be a separate error code and message.

Changelog: Changed
2023-03-16 20:42:02 +01:00
Johann150 d6837814d9
Merge branch 'drive-api-combined'
Reviewed-on: FoundKeyGang/FoundKey#297
2023-03-13 19:30:24 +01:00
Johann150 02aaee6050
client: select folder when entering it
As a convenience when a user is in the "select a folder" dialog,
opens a folder an then clicks on the checkmark, the currently open folder
is selected.
2022-12-23 02:21:52 +01:00
Johann150 d26e2588e3
client: combine selection of files & folders 2022-12-23 02:20:36 +01:00
Johann150 df9064c284
client: remove driveFolderBg theme color
Changelog: Removed
2022-12-23 02:02:20 +01:00
Johann150 f7c4107ca4
client: drive uses grid instead of flexbox 2022-12-22 18:55:06 +01:00
Johann150 c983c4860c
client: use combined drive endpoint 2022-12-22 17:55:13 +01:00
Johann150 d96070bc80
client: fix duplicate folder when creating new folder 2022-12-22 17:32:05 +01:00
Johann150 240cf98920
client: refactor drive drag&drop 2022-12-22 17:06:52 +01:00
Johann150 7b39483966
server: drive endpoint to fetch files and folders
Changelog: Added
2022-12-22 16:46:48 +01:00
17 changed files with 275 additions and 267 deletions

View File

@ -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();

View File

@ -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],

View File

@ -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,
];
});

View File

@ -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);

View File

@ -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,

View File

@ -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;
}
}
}

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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;

View File

@ -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));
}

View File

@ -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',

View File

@ -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',

View File

@ -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',

View File

@ -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',

View File

@ -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',