forked from FoundKeyGang/FoundKey
Merge branch 'fix/download-button-in-audio-player' into mk.absturztau.be
This commit is contained in:
commit
e4ab208f68
45 changed files with 586 additions and 416 deletions
|
@ -1340,3 +1340,7 @@ _translationService:
|
||||||
_libreTranslate:
|
_libreTranslate:
|
||||||
endpoint: "LibreTranslate API Endpoint"
|
endpoint: "LibreTranslate API Endpoint"
|
||||||
authKey: "LibreTranslate Auth Key (optional)"
|
authKey: "LibreTranslate Auth Key (optional)"
|
||||||
|
_remoteInteract:
|
||||||
|
title: "I'm sorry, I'm afraid I can't do that."
|
||||||
|
description: "You cannot perform this action right now. You probably need to do it on your own instance, or sign in."
|
||||||
|
urlInstructions: "You can copy this URL. If you paste it into the search field on your instance, you should be taken to the right location."
|
||||||
|
|
13
packages/backend/migration/1678427401214-remove-unused.js
Normal file
13
packages/backend/migration/1678427401214-remove-unused.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
export class removeUnused1678427401214 {
|
||||||
|
name = 'removeUnused1678427401214'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "pinnedPages"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "pinnedClipId"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ADD "pinnedClipId" character varying(32)`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ADD "pinnedPages" character varying(512) array NOT NULL DEFAULT '{/featured,/channels,/explore,/pages,/about-misskey}'`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,7 +18,13 @@ const ev = new Xev();
|
||||||
* Init process
|
* Init process
|
||||||
*/
|
*/
|
||||||
export async function boot(): Promise<void> {
|
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) {
|
if (cluster.isPrimary || envOption.disableClustering) {
|
||||||
await masterMain();
|
await masterMain();
|
||||||
|
|
|
@ -82,17 +82,6 @@ export class Meta {
|
||||||
})
|
})
|
||||||
public blockedHosts: string[];
|
public blockedHosts: string[];
|
||||||
|
|
||||||
@Column('varchar', {
|
|
||||||
length: 512, array: true, default: '{/featured,/channels,/explore,/pages,/about-foundkey}',
|
|
||||||
})
|
|
||||||
public pinnedPages: string[];
|
|
||||||
|
|
||||||
@Column({
|
|
||||||
...id(),
|
|
||||||
nullable: true,
|
|
||||||
})
|
|
||||||
public pinnedClipId: Clip['id'] | null;
|
|
||||||
|
|
||||||
@Column('varchar', {
|
@Column('varchar', {
|
||||||
length: 512,
|
length: 512,
|
||||||
nullable: true,
|
nullable: true,
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { request } from '@/remote/activitypub/request.js';
|
||||||
import { registerOrFetchInstanceDoc } from '@/services/register-or-fetch-instance-doc.js';
|
import { registerOrFetchInstanceDoc } from '@/services/register-or-fetch-instance-doc.js';
|
||||||
import Logger from '@/services/logger.js';
|
import Logger from '@/services/logger.js';
|
||||||
import { Instances } from '@/models/index.js';
|
import { Instances } from '@/models/index.js';
|
||||||
import { apRequestChart, federationChart, instanceChart } from '@/services/chart/index.js';
|
|
||||||
import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata.js';
|
import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata.js';
|
||||||
import { toPuny } from '@/misc/convert-host.js';
|
import { toPuny } from '@/misc/convert-host.js';
|
||||||
import { StatusError } from '@/misc/fetch.js';
|
import { StatusError } from '@/misc/fetch.js';
|
||||||
|
@ -13,8 +12,6 @@ import { DeliverJobData } from '@/queue/types.js';
|
||||||
|
|
||||||
const logger = new Logger('deliver');
|
const logger = new Logger('deliver');
|
||||||
|
|
||||||
let latest: string | null = null;
|
|
||||||
|
|
||||||
export default async (job: Bull.Job<DeliverJobData>) => {
|
export default async (job: Bull.Job<DeliverJobData>) => {
|
||||||
const { host } = new URL(job.data.to);
|
const { host } = new URL(job.data.to);
|
||||||
const puny = toPuny(host);
|
const puny = toPuny(host);
|
||||||
|
@ -22,11 +19,13 @@ export default async (job: Bull.Job<DeliverJobData>) => {
|
||||||
if (await shouldSkipInstance(puny)) return 'skip';
|
if (await shouldSkipInstance(puny)) return 'skip';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (latest !== (latest = JSON.stringify(job.data.content, null, 2))) {
|
if (Array.isArray(job.data.content)) {
|
||||||
logger.debug(`delivering ${latest}`);
|
await Promise.all(
|
||||||
}
|
job.data.content.map(x => request(job.data.user, job.data.to, x))
|
||||||
|
);
|
||||||
|
} else {
|
||||||
await request(job.data.user, job.data.to, job.data.content);
|
await request(job.data.user, job.data.to, job.data.content);
|
||||||
|
}
|
||||||
|
|
||||||
// Update stats
|
// Update stats
|
||||||
registerOrFetchInstanceDoc(host).then(i => {
|
registerOrFetchInstanceDoc(host).then(i => {
|
||||||
|
@ -38,13 +37,7 @@ export default async (job: Bull.Job<DeliverJobData>) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
fetchInstanceMetadata(i);
|
fetchInstanceMetadata(i);
|
||||||
|
|
||||||
instanceChart.requestSent(i.host, true);
|
|
||||||
apRequestChart.deliverSucc();
|
|
||||||
federationChart.deliverd(i.host, true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return 'Success';
|
|
||||||
} catch (res) {
|
} catch (res) {
|
||||||
// Update stats
|
// Update stats
|
||||||
registerOrFetchInstanceDoc(host).then(i => {
|
registerOrFetchInstanceDoc(host).then(i => {
|
||||||
|
@ -53,10 +46,6 @@ export default async (job: Bull.Job<DeliverJobData>) => {
|
||||||
latestStatus: res instanceof StatusError ? res.statusCode : null,
|
latestStatus: res instanceof StatusError ? res.statusCode : null,
|
||||||
isNotResponding: true,
|
isNotResponding: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
instanceChart.requestSent(i.host, false);
|
|
||||||
apRequestChart.deliverFail();
|
|
||||||
federationChart.deliverd(i.host, false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res instanceof StatusError) {
|
if (res instanceof StatusError) {
|
||||||
|
@ -67,7 +56,7 @@ export default async (job: Bull.Job<DeliverJobData>) => {
|
||||||
return `${res.statusCode} ${res.statusMessage}`;
|
return `${res.statusCode} ${res.statusMessage}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5xx etc.
|
// 5xx etc., throwing an Error will make Bull retry
|
||||||
throw new Error(`${res.statusCode} ${res.statusMessage}`);
|
throw new Error(`${res.statusCode} ${res.statusMessage}`);
|
||||||
} else {
|
} else {
|
||||||
// DNS error, socket error, timeout ...
|
// DNS error, socket error, timeout ...
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { CacheableRemoteUser } from '@/models/entities/user.js';
|
import { CacheableRemoteUser } from '@/models/entities/user.js';
|
||||||
import deleteNode from '@/services/note/delete.js';
|
import { deleteNotes } from '@/services/note/delete.js';
|
||||||
import { getApLock } from '@/misc/app-lock.js';
|
import { getApLock } from '@/misc/app-lock.js';
|
||||||
import { deleteMessage } from '@/services/messages/delete.js';
|
import { deleteMessage } from '@/services/messages/delete.js';
|
||||||
import { DbResolver } from '@/remote/activitypub/db-resolver.js';
|
import { DbResolver } from '@/remote/activitypub/db-resolver.js';
|
||||||
|
@ -29,7 +29,7 @@ export default async function(actor: CacheableRemoteUser, uri: string): Promise<
|
||||||
return 'skip: cant delete other actors note';
|
return 'skip: cant delete other actors note';
|
||||||
}
|
}
|
||||||
|
|
||||||
await deleteNode(actor, note);
|
await deleteNotes([note], actor);
|
||||||
return 'ok: note deleted';
|
return 'ok: note deleted';
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Notes } from '@/models/index.js';
|
import { Notes } from '@/models/index.js';
|
||||||
import { CacheableRemoteUser } from '@/models/entities/user.js';
|
import { CacheableRemoteUser } from '@/models/entities/user.js';
|
||||||
import deleteNote from '@/services/note/delete.js';
|
import { deleteNotes } from '@/services/note/delete.js';
|
||||||
import { IAnnounce, getApId } from '@/remote/activitypub/type.js';
|
import { IAnnounce, getApId } from '@/remote/activitypub/type.js';
|
||||||
|
|
||||||
export const undoAnnounce = async (actor: CacheableRemoteUser, activity: IAnnounce): Promise<string> => {
|
export const undoAnnounce = async (actor: CacheableRemoteUser, activity: IAnnounce): Promise<string> => {
|
||||||
|
@ -13,6 +13,6 @@ export const undoAnnounce = async (actor: CacheableRemoteUser, activity: IAnnoun
|
||||||
|
|
||||||
if (!note) return 'skip: no such Announce';
|
if (!note) return 'skip: no such Announce';
|
||||||
|
|
||||||
await deleteNote(actor, note);
|
await deleteNotes([note], actor);
|
||||||
return 'ok: deleted';
|
return 'ok: deleted';
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,7 +4,16 @@ import { getUserKeypair } from '@/misc/keypair-store.js';
|
||||||
import { User } from '@/models/entities/user.js';
|
import { User } from '@/models/entities/user.js';
|
||||||
import { getResponse } from '@/misc/fetch.js';
|
import { getResponse } from '@/misc/fetch.js';
|
||||||
import { createSignedPost, createSignedGet } from './ap-request.js';
|
import { createSignedPost, createSignedGet } from './ap-request.js';
|
||||||
|
import { apRequestChart, federationChart, instanceChart } from '@/services/chart/index.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Post an activity to an inbox. Automatically updates the statistics
|
||||||
|
* on succeeded or failed delivery attempts.
|
||||||
|
*
|
||||||
|
* @param user http-signature user
|
||||||
|
* @param url The URL of the inbox.
|
||||||
|
* @param object The Activity or other object to be posted to the inbox.
|
||||||
|
*/
|
||||||
export async function request(user: { id: User['id'] }, url: string, object: any): Promise<void> {
|
export async function request(user: { id: User['id'] }, url: string, object: any): Promise<void> {
|
||||||
const body = JSON.stringify(object);
|
const body = JSON.stringify(object);
|
||||||
|
|
||||||
|
@ -22,6 +31,9 @@ export async function request(user: { id: User['id'] }, url: string, object: any
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { host } = new URL(url);
|
||||||
|
|
||||||
|
try {
|
||||||
await getResponse({
|
await getResponse({
|
||||||
url,
|
url,
|
||||||
method: req.request.method,
|
method: req.request.method,
|
||||||
|
@ -30,6 +42,17 @@ export async function request(user: { id: User['id'] }, url: string, object: any
|
||||||
// don't allow redirects on the inbox
|
// don't allow redirects on the inbox
|
||||||
redirect: 'error',
|
redirect: 'error',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
instanceChart.requestSent(host, true);
|
||||||
|
apRequestChart.deliverSucc();
|
||||||
|
federationChart.deliverd(host, true);
|
||||||
|
} catch (err) {
|
||||||
|
instanceChart.requestSent(host, false);
|
||||||
|
apRequestChart.deliverFail();
|
||||||
|
federationChart.deliverd(host, false);
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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___clips_update from './endpoints/clips/update.js';
|
||||||
import * as ep___drive from './endpoints/drive.js';
|
import * as ep___drive from './endpoints/drive.js';
|
||||||
import * as ep___drive_files from './endpoints/drive/files.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_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_checkExistence from './endpoints/drive/files/check-existence.js';
|
||||||
import * as ep___drive_files_create from './endpoints/drive/files/create.js';
|
import * as ep___drive_files_create from './endpoints/drive/files/create.js';
|
||||||
|
@ -400,6 +401,7 @@ const eps = [
|
||||||
['clips/update', ep___clips_update],
|
['clips/update', ep___clips_update],
|
||||||
['drive', ep___drive],
|
['drive', ep___drive],
|
||||||
['drive/files', ep___drive_files],
|
['drive/files', ep___drive_files],
|
||||||
|
['drive/show', ep___drive_show],
|
||||||
['drive/files/attached-notes', ep___drive_files_attachedNotes],
|
['drive/files/attached-notes', ep___drive_files_attachedNotes],
|
||||||
['drive/files/check-existence', ep___drive_files_checkExistence],
|
['drive/files/check-existence', ep___drive_files_checkExistence],
|
||||||
['drive/files/create', ep___drive_files_create],
|
['drive/files/create', ep___drive_files_create],
|
||||||
|
|
|
@ -288,8 +288,6 @@ export default define(meta, paramDef, async () => {
|
||||||
defaultLightTheme: instance.defaultLightTheme,
|
defaultLightTheme: instance.defaultLightTheme,
|
||||||
defaultDarkTheme: instance.defaultDarkTheme,
|
defaultDarkTheme: instance.defaultDarkTheme,
|
||||||
enableEmail: instance.enableEmail,
|
enableEmail: instance.enableEmail,
|
||||||
pinnedPages: instance.pinnedPages,
|
|
||||||
pinnedClipId: instance.pinnedClipId,
|
|
||||||
cacheRemoteFiles: instance.cacheRemoteFiles,
|
cacheRemoteFiles: instance.cacheRemoteFiles,
|
||||||
|
|
||||||
useStarForReactionFallback: instance.useStarForReactionFallback,
|
useStarForReactionFallback: instance.useStarForReactionFallback,
|
||||||
|
|
|
@ -48,10 +48,6 @@ export const paramDef = {
|
||||||
proxyAccountId: { type: 'string', format: 'misskey:id', nullable: true },
|
proxyAccountId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||||
maintainerName: { type: 'string', nullable: true },
|
maintainerName: { type: 'string', nullable: true },
|
||||||
maintainerEmail: { type: 'string', nullable: true },
|
maintainerEmail: { type: 'string', nullable: true },
|
||||||
pinnedPages: { type: 'array', items: {
|
|
||||||
type: 'string',
|
|
||||||
} },
|
|
||||||
pinnedClipId: { type: 'string', format: 'misskey:id', nullable: true },
|
|
||||||
langs: { type: 'array', items: {
|
langs: { type: 'array', items: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
} },
|
} },
|
||||||
|
@ -209,14 +205,6 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
set.langs = ps.langs.filter(Boolean);
|
set.langs = ps.langs.filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(ps.pinnedPages)) {
|
|
||||||
set.pinnedPages = ps.pinnedPages.filter(Boolean);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ps.pinnedClipId !== undefined) {
|
|
||||||
set.pinnedClipId = ps.pinnedClipId;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ps.summalyProxy !== undefined) {
|
if (ps.summalyProxy !== undefined) {
|
||||||
set.summalyProxy = ps.summalyProxy;
|
set.summalyProxy = ps.summalyProxy;
|
||||||
}
|
}
|
||||||
|
|
70
packages/backend/src/server/api/endpoints/drive/show.ts
Normal file
70
packages/backend/src/server/api/endpoints/drive/show.ts
Normal 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,
|
||||||
|
];
|
||||||
|
});
|
|
@ -17,7 +17,7 @@ export const meta = {
|
||||||
|
|
||||||
kind: 'write:following',
|
kind: 'write:following',
|
||||||
|
|
||||||
errors: ['FOLLOWER_IS_YOURSELF', 'NO_SUCH_USER', 'NOT_FOLLOWING'],
|
errors: ['FOLLOWER_IS_YOURSELF', 'NO_SUCH_USER', 'NOT_FOLLOWED'],
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
|
@ -53,7 +53,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
followeeId: followee.id,
|
followeeId: followee.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!exist) throw new ApiError('NOT_FOLLOWING');
|
if (!exist) throw new ApiError('NOT_FOLLOWED');
|
||||||
|
|
||||||
await deleteFollowing(follower, followee);
|
await deleteFollowing(follower, followee);
|
||||||
|
|
||||||
|
|
|
@ -305,8 +305,6 @@ export default define(meta, paramDef, async () => {
|
||||||
|
|
||||||
translatorAvailable: translatorAvailable(instance),
|
translatorAvailable: translatorAvailable(instance),
|
||||||
|
|
||||||
pinnedPages: instance.pinnedPages,
|
|
||||||
pinnedClipId: instance.pinnedClipId,
|
|
||||||
cacheRemoteFiles: instance.cacheRemoteFiles,
|
cacheRemoteFiles: instance.cacheRemoteFiles,
|
||||||
requireSetup: (await Users.countBy({
|
requireSetup: (await Users.countBy({
|
||||||
host: IsNull(),
|
host: IsNull(),
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import deleteNote from '@/services/note/delete.js';
|
import { deleteNotes } from '@/services/note/delete.js';
|
||||||
import { Users } from '@/models/index.js';
|
import { Users } from '@/models/index.js';
|
||||||
import { SECOND, HOUR } from '@/const.js';
|
import { SECOND, HOUR } from '@/const.js';
|
||||||
import define from '@/server/api/define.js';
|
import define from '@/server/api/define.js';
|
||||||
|
@ -47,6 +47,6 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
throw new ApiError('ACCESS_DENIED');
|
throw new ApiError('ACCESS_DENIED');
|
||||||
}
|
}
|
||||||
|
|
||||||
// この操作を行うのが投稿者とは限らない(例えばモデレーター)ため
|
// Here, we do not provide the current user because it may be an admin/moderator.
|
||||||
await deleteNote(await Users.findOneByOrFail({ id: note.userId }), note);
|
await deleteNotes([note]);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import deleteNote from '@/services/note/delete.js';
|
import { deleteNotes } from '@/services/note/delete.js';
|
||||||
import { Notes, Users } from '@/models/index.js';
|
import { Notes, Users } from '@/models/index.js';
|
||||||
import { SECOND, HOUR } from '@/const.js';
|
import { SECOND, HOUR } from '@/const.js';
|
||||||
import define from '@/server/api/define.js';
|
import define from '@/server/api/define.js';
|
||||||
|
@ -48,7 +48,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
renoteId: note.id,
|
renoteId: note.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const note of renotes) {
|
if (renotes.length === 0) return;
|
||||||
deleteNote(await Users.findOneByOrFail({ id: user.id }), note);
|
|
||||||
}
|
await deleteNotes(renotes, user);
|
||||||
});
|
});
|
||||||
|
|
|
@ -340,6 +340,10 @@ export const errors: Record<string, { message: string, httpStatusCode: number }>
|
||||||
message: 'You are not following that user.',
|
message: 'You are not following that user.',
|
||||||
httpStatusCode: 409,
|
httpStatusCode: 409,
|
||||||
},
|
},
|
||||||
|
NOT_FOLLOWED: {
|
||||||
|
message: 'You are not followed by that user.',
|
||||||
|
httpStatusCode: 409,
|
||||||
|
},
|
||||||
NOT_LIKED: {
|
NOT_LIKED: {
|
||||||
message: 'You have not liked that page.',
|
message: 'You have not liked that page.',
|
||||||
httpStatusCode: 409,
|
httpStatusCode: 409,
|
||||||
|
|
|
@ -14,14 +14,88 @@ import { notesChart, perUserNotesChart, instanceChart } from '@/services/chart/i
|
||||||
import { DeliverManager } from '@/remote/activitypub/deliver-manager.js';
|
import { DeliverManager } from '@/remote/activitypub/deliver-manager.js';
|
||||||
import { countSameRenotes } from '@/misc/count-same-renotes.js';
|
import { countSameRenotes } from '@/misc/count-same-renotes.js';
|
||||||
import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc.js';
|
import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc.js';
|
||||||
import { deliverToRelays } from '../relay.js';
|
import { deliverMultipleToRelays } from '../relay.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete your note.
|
* Delete several notes of the same user.
|
||||||
* @param user author
|
* @param notes Array of notes to be deleted.
|
||||||
* @param note note to be deleted
|
* @param user Author of the notes. Will be fetched if not provided.
|
||||||
*/
|
*/
|
||||||
export default async function(user: { id: User['id']; uri: User['uri']; host: User['host']; }, note: Note, quiet = false): Promise<void> {
|
export async function deleteNotes(notes: Note[], user?: User): Promise<void> {
|
||||||
|
if (notes.length === 0) return;
|
||||||
|
|
||||||
|
const fetchedUser = user ?? await Users.findOneByOrFail({ id: notes[0].userId });
|
||||||
|
|
||||||
|
const cascadingNotes = await Promise.all(
|
||||||
|
notes.map(note => findCascadingNotes(note))
|
||||||
|
).then(res => res.flat());
|
||||||
|
|
||||||
|
// perform side effects for notes and cascaded notes
|
||||||
|
await Promise.all(
|
||||||
|
notes.concat(cascadingNotes)
|
||||||
|
.map(note => deletionSideEffects(note, fetchedUser))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Compute delivery content for later.
|
||||||
|
// It is important that this is done before deleting notes from
|
||||||
|
// the database since we may need some information from parent
|
||||||
|
// notes that cause this one to be cascade-deleted.
|
||||||
|
let content = await Promise.all(
|
||||||
|
notes.concat(cascadingNotes)
|
||||||
|
// only deliver for local notes that are not local-only
|
||||||
|
.filter(note => note.userHost == null && !note.localOnly)
|
||||||
|
.map(async note => {
|
||||||
|
let renote: Note | null = null;
|
||||||
|
|
||||||
|
// if the deleted note is a renote
|
||||||
|
if (foundkey.entities.isPureRenote(note)) {
|
||||||
|
renote = await Notes.findOneBy({ id: note.renoteId });
|
||||||
|
}
|
||||||
|
|
||||||
|
return renderActivity(renote
|
||||||
|
? renderUndo(renderAnnounce(renote.uri || `${config.url}/notes/${renote.id}`, note), fetchedUser)
|
||||||
|
: renderDelete(renderTombstone(`${config.url}/notes/${note.id}`), fetchedUser));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Compute addressing information.
|
||||||
|
// Since we do not send any actual content, we send all note deletions to everyone.
|
||||||
|
const manager = new DeliverManager(fetchedUser, content);
|
||||||
|
manager.addFollowersRecipe();
|
||||||
|
manager.addEveryone();
|
||||||
|
// Check mentioned users, since not all may have a shared inbox.
|
||||||
|
await Promise.all(
|
||||||
|
notes.concat(cascadingNotes)
|
||||||
|
.map(note => getMentionedRemoteUsers(note))
|
||||||
|
)
|
||||||
|
.then(remoteUsers => {
|
||||||
|
remoteUsers.flat()
|
||||||
|
.forEach(remoteUser => manager.addDirectRecipe(remoteUser))
|
||||||
|
});
|
||||||
|
|
||||||
|
// Actually delete notes from the database.
|
||||||
|
// It is important that this is done before delivering the activities.
|
||||||
|
// Otherwise there might be a race condition where we tell someone
|
||||||
|
// the note exists and they can successfully fetch it.
|
||||||
|
await Notes.delete({
|
||||||
|
id: In(notes.map(x => x.id)),
|
||||||
|
userId: fetchedUser.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
// deliver the previously computed content
|
||||||
|
await Promise.all([
|
||||||
|
manager.execute(),
|
||||||
|
deliverMultipleToRelays(user, content),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform side effects of deletion, such as updating statistics.
|
||||||
|
* Does not actually delete the note itself.
|
||||||
|
* @param note The soon to be deleted note.
|
||||||
|
* @param user The author of said note.
|
||||||
|
*/
|
||||||
|
async function deletionSideEffects(note: Note, user: User): Promise<void> {
|
||||||
const deletedAt = new Date();
|
const deletedAt = new Date();
|
||||||
|
|
||||||
// If this is the only renote of this note by this user
|
// If this is the only renote of this note by this user
|
||||||
|
@ -34,32 +108,8 @@ export default async function(user: { id: User['id']; uri: User['uri']; host: Us
|
||||||
await Notes.decrement({ id: note.replyId }, 'repliesCount', 1);
|
await Notes.decrement({ id: note.replyId }, 'repliesCount', 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!quiet) {
|
|
||||||
publishNoteStream(note.id, 'deleted', { deletedAt });
|
publishNoteStream(note.id, 'deleted', { deletedAt });
|
||||||
|
|
||||||
// deliver delete activity of note itself for local posts
|
|
||||||
if (Users.isLocalUser(user) && !note.localOnly) {
|
|
||||||
let renote: Note | null = null;
|
|
||||||
|
|
||||||
// if deleted note is renote
|
|
||||||
if (foundkey.entities.isPureRenote(note)) {
|
|
||||||
renote = await Notes.findOneBy({ id: note.renoteId });
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = renderActivity(renote
|
|
||||||
? renderUndo(renderAnnounce(renote.uri || `${config.url}/notes/${renote.id}`, note), user)
|
|
||||||
: renderDelete(renderTombstone(`${config.url}/notes/${note.id}`), user));
|
|
||||||
|
|
||||||
deliverToConcerned(user, note, content);
|
|
||||||
}
|
|
||||||
|
|
||||||
// also deliver delete activity to cascaded notes
|
|
||||||
const cascadingNotes = await findCascadingNotes(note);
|
|
||||||
for (const cascadingNote of cascadingNotes) {
|
|
||||||
const content = renderActivity(renderDelete(renderTombstone(`${config.url}/notes/${cascadingNote.id}`), cascadingNote.user));
|
|
||||||
deliverToConcerned(cascadingNote.user, cascadingNote, content);
|
|
||||||
}
|
|
||||||
|
|
||||||
// update statistics
|
// update statistics
|
||||||
notesChart.update(note, false);
|
notesChart.update(note, false);
|
||||||
perUserNotesChart.update(user, note, false);
|
perUserNotesChart.update(user, note, false);
|
||||||
|
@ -72,12 +122,6 @@ export default async function(user: { id: User['id']; uri: User['uri']; host: Us
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await Notes.delete({
|
|
||||||
id: note.id,
|
|
||||||
userId: user.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search for notes that will be affected by ON CASCADE DELETE.
|
* Search for notes that will be affected by ON CASCADE DELETE.
|
||||||
* However, only notes for which it is relevant to deliver delete activities are searched.
|
* However, only notes for which it is relevant to deliver delete activities are searched.
|
||||||
|
@ -143,23 +187,3 @@ async function getMentionedRemoteUsers(note: Note): Promise<IRemoteUser[]> {
|
||||||
}) as IRemoteUser[];
|
}) as IRemoteUser[];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deliverToConcerned(user: { id: ILocalUser['id']; host: null; }, note: Note, content: any): Promise<void> {
|
|
||||||
const manager = new DeliverManager(user, content);
|
|
||||||
|
|
||||||
const remoteUsers = await getMentionedRemoteUsers(note);
|
|
||||||
for (const remoteUser of remoteUsers) {
|
|
||||||
manager.addDirectRecipe(remoteUser);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (['public', 'home', 'followers'].includes(note.visibility)) {
|
|
||||||
manager.addFollowersRecipe();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (['public', 'home'].includes(note.visibility)) {
|
|
||||||
manager.addEveryone();
|
|
||||||
}
|
|
||||||
|
|
||||||
await manager.execute();
|
|
||||||
|
|
||||||
deliverToRelays(user, content);
|
|
||||||
}
|
|
||||||
|
|
|
@ -105,3 +105,21 @@ export async function deliverToRelays(user: { id: User['id']; host: null; }, act
|
||||||
deliver(user, signed, relay.inbox);
|
deliver(user, signed, relay.inbox);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function deliverMultipleToRelays(user: User, activities: any[]): Promise<void> {
|
||||||
|
if (activities.length === 0) return;
|
||||||
|
|
||||||
|
const relays = await relaysCache.fetch('');
|
||||||
|
if (relays == null || relays.length === 0) return;
|
||||||
|
|
||||||
|
const content = await Promise.all(activities.map(activity => {
|
||||||
|
const copy = structuredClone(activity);
|
||||||
|
if (!copy.to) copy.to = ['https://www.w3.org/ns/activitystreams#Public'];
|
||||||
|
|
||||||
|
return attachLdSignature(copy, user);
|
||||||
|
}));
|
||||||
|
|
||||||
|
for (const relay of relays) {
|
||||||
|
deliver(user, content, relay.inbox);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 106 KiB |
Binary file not shown.
Before Width: | Height: | Size: 76 KiB |
Binary file not shown.
Before Width: | Height: | Size: 9.2 KiB |
|
@ -52,7 +52,7 @@ const props = withDefaults(defineProps<{
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'chosen', r: foundkey.entities.DriveFile): void;
|
(ev: 'chosen', r: foundkey.entities.DriveFile, extendSelection: boolean): void;
|
||||||
(ev: 'dragstart'): void;
|
(ev: 'dragstart'): void;
|
||||||
(ev: 'dragend'): void;
|
(ev: 'dragend'): void;
|
||||||
}>();
|
}>();
|
||||||
|
@ -95,9 +95,7 @@ function getMenu(): MenuItem[] {
|
||||||
|
|
||||||
function onClick(ev: MouseEvent): void {
|
function onClick(ev: MouseEvent): void {
|
||||||
if (props.selectMode) {
|
if (props.selectMode) {
|
||||||
emit('chosen', props.file);
|
emit('chosen', props.file, ev.ctrlKey);
|
||||||
} else {
|
|
||||||
os.popupMenu(getMenu(), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -330,7 +328,7 @@ async function deleteFile(): Promise<void> {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
> .ext {
|
> .ext {
|
||||||
opacity: 0.5;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="rghtznwe"
|
class="rghtznwe"
|
||||||
:class="{ draghover }"
|
:class="{ draghover, isSelected }"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
:title="title"
|
:title="title"
|
||||||
@click="onClick"
|
@click="selected"
|
||||||
@contextmenu.stop="onContextmenu"
|
@contextmenu.stop="onContextmenu"
|
||||||
@mouseover="onMouseover"
|
@mouseover="onMouseover"
|
||||||
@mouseout="onMouseout"
|
@mouseout="onMouseout"
|
||||||
|
@ -15,15 +15,16 @@
|
||||||
@dragstart="onDragstart"
|
@dragstart="onDragstart"
|
||||||
@dragend="onDragend"
|
@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">
|
<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 }}
|
{{ folder.name }}
|
||||||
</p>
|
</p>
|
||||||
<p v-if="defaultStore.state.uploadFolder == folder.id" class="upload">
|
<p v-if="defaultStore.state.uploadFolder == folder.id" class="upload">
|
||||||
{{ i18n.ts.uploadFolder }}
|
{{ i18n.ts.uploadFolder }}
|
||||||
</p>
|
</p>
|
||||||
<button v-if="selectMode" class="checkbox _button" :class="{ checked: isSelected }" @click.prevent.stop="checkboxClicked"></button>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -44,7 +45,7 @@ const props = withDefaults(defineProps<{
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
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: 'move', v: foundkey.entities.DriveFolder): void;
|
||||||
(ev: 'upload', file: File, folder: foundkey.entities.DriveFolder);
|
(ev: 'upload', file: File, folder: foundkey.entities.DriveFolder);
|
||||||
(ev: 'removeFile', v: foundkey.entities.DriveFile['id']): void;
|
(ev: 'removeFile', v: foundkey.entities.DriveFile['id']): void;
|
||||||
|
@ -59,20 +60,10 @@ const isDragging = ref(false);
|
||||||
|
|
||||||
const title = computed(() => props.folder.name);
|
const title = computed(() => props.folder.name);
|
||||||
|
|
||||||
function checkboxClicked() {
|
function selected(ev: MouseEvent) {
|
||||||
emit('chosen', props.folder);
|
if (props.selectMode) {
|
||||||
|
emit('chosen', props.folder, ev.ctrlKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onClick() {
|
|
||||||
emit('move', props.folder);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onMouseover() {
|
|
||||||
hover.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function onMouseout() {
|
|
||||||
hover.value = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDragover(ev: DragEvent) {
|
function onDragover(ev: DragEvent) {
|
||||||
|
@ -260,30 +251,34 @@ function onContextmenu(ev: MouseEvent) {
|
||||||
.rghtznwe {
|
.rghtznwe {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
height: 64px;
|
min-height: 180px;
|
||||||
background: var(--driveFolderBg);
|
border-radius: 8px;
|
||||||
border-radius: 4px;
|
|
||||||
|
|
||||||
&, * {
|
&, * {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
*:not(.checkbox) {
|
> .thumbnail {
|
||||||
|
width: 110px;
|
||||||
|
height: 110px;
|
||||||
|
margin: auto;
|
||||||
|
|
||||||
|
/* same style as drive-file-thumbnail.vue */
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
background: var(--panel);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: clip;
|
||||||
|
|
||||||
|
> i {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
margin: auto;
|
||||||
|
font-size: 33px;
|
||||||
|
color: #777;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .checkbox {
|
&:not(:hover) > i.hover,
|
||||||
position: absolute;
|
&:hover > i:not(.hover) { display: none; }
|
||||||
bottom: 8px;
|
|
||||||
right: 8px;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
background: #fff;
|
|
||||||
border: solid 1px #000;
|
|
||||||
|
|
||||||
&.checked {
|
|
||||||
background: var(--accent);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.draghover {
|
&.draghover {
|
||||||
|
@ -300,23 +295,37 @@ function onContextmenu(ev: MouseEvent) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .name {
|
&.isSelected {
|
||||||
margin: 0;
|
background: var(--accent);
|
||||||
font-size: 0.9em;
|
|
||||||
color: var(--desktopDriveFolderFg);
|
|
||||||
|
|
||||||
> i {
|
&:hover {
|
||||||
margin-right: 4px;
|
background: var(--accentLighten);
|
||||||
margin-left: 2px;
|
|
||||||
text-align: left;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> .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 {
|
> .upload {
|
||||||
margin: 4px 4px;
|
margin: 4px 4px;
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
text-align: right;
|
text-align: center;
|
||||||
color: var(--desktopDriveFolderFg);
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -35,31 +35,37 @@
|
||||||
@drop.prevent.stop="onDrop"
|
@drop.prevent.stop="onDrop"
|
||||||
@contextmenu.stop="onContextmenu"
|
@contextmenu.stop="onContextmenu"
|
||||||
>
|
>
|
||||||
<div ref="contents" class="contents">
|
|
||||||
<MkPagination
|
<MkPagination
|
||||||
ref="foldersPaginationElem"
|
ref="paginationElem"
|
||||||
:pagination="foldersPagination"
|
:pagination="pagination"
|
||||||
class="folders"
|
class="contents"
|
||||||
@loaded="foldersLoading = false"
|
|
||||||
>
|
>
|
||||||
<template #empty>
|
<template #empty>
|
||||||
<!--
|
<p v-if="folder == null" class="empty"><strong>{{ i18n.ts.emptyDrive }}</strong></p>
|
||||||
Don't display anything here if there are no folders,
|
<p v-else class="empty">{{ i18n.ts.emptyFolder }}</p>
|
||||||
there is a separate check if both paginations are empty.
|
|
||||||
-->
|
|
||||||
{{ null }}
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #default="{ items: folders }">
|
<template #default="{ items }">
|
||||||
<XFolder
|
<template v-for="(f, i) in items">
|
||||||
v-for="(f, i) in folders"
|
<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"
|
:key="f.id"
|
||||||
v-anim="i"
|
v-anim="i"
|
||||||
class="folder"
|
|
||||||
:folder="f"
|
:folder="f"
|
||||||
:select-mode="select === 'folder'"
|
:select-mode="select !== 'file'"
|
||||||
:is-selected="selectedFolders.some(x => x.id === f.id)"
|
:is-selected="selected.some(x => x.id === f.id)"
|
||||||
@chosen="chooseFolder"
|
@chosen="choose"
|
||||||
@move="move"
|
@move="move"
|
||||||
@upload="upload"
|
@upload="upload"
|
||||||
@removeFile="removeFile"
|
@removeFile="removeFile"
|
||||||
|
@ -67,46 +73,9 @@
|
||||||
@dragstart="isDragSource = true"
|
@dragstart="isDragSource = true"
|
||||||
@dragend="isDragSource = false"
|
@dragend="isDragSource = false"
|
||||||
/>
|
/>
|
||||||
<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
|
</template>
|
||||||
<div v-for="(n, i) in 16" :key="i" class="padding"></div>
|
|
||||||
</template>
|
</template>
|
||||||
</MkPagination>
|
</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>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-if="draghover" class="dropzone"></div>
|
<div v-if="draghover" class="dropzone"></div>
|
||||||
<input ref="fileInput" type="file" accept="*/*" multiple tabindex="-1" @change="onChangeFileInput"/>
|
<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<{
|
const props = withDefaults(defineProps<{
|
||||||
initialFolder?: foundkey.entities.DriveFolder;
|
initialFolder?: foundkey.entities.DriveFolder;
|
||||||
type?: string;
|
|
||||||
multiple?: boolean;
|
multiple?: boolean;
|
||||||
select?: 'file' | 'folder' | null;
|
select?: 'file' | 'folder' | null;
|
||||||
}>(), {
|
}>(), {
|
||||||
|
@ -139,19 +107,13 @@ const props = withDefaults(defineProps<{
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'selected', v: foundkey.entities.DriveFile | foundkey.entities.DriveFolder): void;
|
(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: 'move-root'): void;
|
||||||
(ev: 'cd', v: foundkey.entities.DriveFolder | null): void;
|
(ev: 'cd', v: foundkey.entities.DriveFolder | null): void;
|
||||||
(ev: 'open-folder', v: foundkey.entities.DriveFolder): void;
|
(ev: 'open-folder', v: foundkey.entities.DriveFolder): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let foldersPaginationElem = $ref<InstanceType<typeof MkPagination>>();
|
let paginationElem = $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 fileInput = $ref<HTMLInputElement>();
|
let fileInput = $ref<HTMLInputElement>();
|
||||||
|
|
||||||
|
@ -160,8 +122,7 @@ const connection = stream.useChannel('drive');
|
||||||
|
|
||||||
let folder = $ref<foundkey.entities.DriveFolder | null>(null);
|
let folder = $ref<foundkey.entities.DriveFolder | null>(null);
|
||||||
let hierarchyFolders = $ref<foundkey.entities.DriveFolder[]>([]);
|
let hierarchyFolders = $ref<foundkey.entities.DriveFolder[]>([]);
|
||||||
let selectedFiles = $ref<foundkey.entities.DriveFile[]>([]);
|
let selected = $ref<Array<foundkey.entities.DriveFile | foundkey.entities.DriveFolder>>([]);
|
||||||
let selectedFolders = $ref<foundkey.entities.DriveFolder[]>([]);
|
|
||||||
let keepOriginal = $ref<boolean>(defaultStore.state.keepOriginalUploading);
|
let keepOriginal = $ref<boolean>(defaultStore.state.keepOriginalUploading);
|
||||||
|
|
||||||
// ドロップされようとしているか
|
// ドロップされようとしているか
|
||||||
|
@ -171,7 +132,14 @@ let draghover = $ref(false);
|
||||||
// (自分自身の階層にドロップできないようにするためのフラグ)
|
// (自分自身の階層にドロップできないようにするためのフラグ)
|
||||||
let isDragSource = $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) {
|
function onStreamDriveFileCreated(file: foundkey.entities.DriveFile) {
|
||||||
addFile(file, true);
|
addFile(file, true);
|
||||||
|
@ -210,9 +178,8 @@ function onStreamDriveFolderDeleted(folderId: string) {
|
||||||
function onDragover(ev: DragEvent): any {
|
function onDragover(ev: DragEvent): any {
|
||||||
if (!ev.dataTransfer) return;
|
if (!ev.dataTransfer) return;
|
||||||
|
|
||||||
// ドラッグ元が自分自身の所有するアイテムだったら
|
|
||||||
if (isDragSource) {
|
if (isDragSource) {
|
||||||
// 自分自身にはドロップさせない
|
// We are the drag source, do not allow to drop.
|
||||||
ev.dataTransfer.dropEffect = 'none';
|
ev.dataTransfer.dropEffect = 'none';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -257,17 +224,16 @@ function onDrop(ev: DragEvent): any {
|
||||||
|
|
||||||
if (!ev.dataTransfer) return;
|
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) {
|
if (ev.dataTransfer.files.length > 0) {
|
||||||
|
// dropping operating system files
|
||||||
for (const file of Array.from(ev.dataTransfer.files)) {
|
for (const file of Array.from(ev.dataTransfer.files)) {
|
||||||
upload(file, folder);
|
upload(file, folder);
|
||||||
}
|
}
|
||||||
return;
|
} else if (driveFile != null && driveFile !== '') {
|
||||||
}
|
// dropping drive files
|
||||||
|
|
||||||
//#region ドライブのファイル
|
|
||||||
const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
|
|
||||||
if (driveFile != null && driveFile !== '') {
|
|
||||||
const file = JSON.parse(driveFile);
|
const file = JSON.parse(driveFile);
|
||||||
|
|
||||||
// cannot move file within parent folder
|
// cannot move file within parent folder
|
||||||
|
@ -278,18 +244,14 @@ function onDrop(ev: DragEvent): any {
|
||||||
fileId: file.id,
|
fileId: file.id,
|
||||||
folderId: folder?.id ?? null,
|
folderId: folder?.id ?? null,
|
||||||
});
|
});
|
||||||
}
|
} else if (driveFolder != null && driveFolder !== '') {
|
||||||
//#endregion
|
// dropping drive folders
|
||||||
|
|
||||||
//#region ドライブのフォルダ
|
|
||||||
const driveFolder = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
|
|
||||||
if (driveFolder != null && driveFolder !== '') {
|
|
||||||
const droppedFolder = JSON.parse(driveFolder);
|
const droppedFolder = JSON.parse(driveFolder);
|
||||||
|
|
||||||
// cannot move folder into itself
|
// cannot move folder into itself
|
||||||
if (droppedFolder.id === folder?.id) return false;
|
if (droppedFolder.id === folder?.id) return false;
|
||||||
// cannot move folder within parent folder
|
// 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);
|
removeFolder(droppedFolder.id);
|
||||||
os.api('drive/folders/update', {
|
os.api('drive/folders/update', {
|
||||||
|
@ -311,7 +273,6 @@ function onDrop(ev: DragEvent): any {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
//#endregion
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectLocalFile() {
|
function selectLocalFile() {
|
||||||
|
@ -346,8 +307,6 @@ function createFolder() {
|
||||||
os.api('drive/folders/create', {
|
os.api('drive/folders/create', {
|
||||||
name,
|
name,
|
||||||
parentId: folder?.id ?? undefined,
|
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);
|
uploadFile(file, folderToUpload?.id ?? null, undefined, keepOriginal);
|
||||||
}
|
}
|
||||||
|
|
||||||
function chooseFile(file: foundkey.entities.DriveFile) {
|
function choose(choice: foundkey.entities.DriveFile | foundkey.entities.DriveFolder, extendSelection: boolean) {
|
||||||
const isAlreadySelected = selectedFiles.some(f => f.id === file.id);
|
const alreadySelected = selected.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 chooseFolder(folderToChoose: foundkey.entities.DriveFolder) {
|
const action = (() => {
|
||||||
const isAlreadySelected = selectedFolders.some(f => f.id === folderToChoose.id);
|
if (props.select != null) {
|
||||||
if (props.multiple) {
|
// file picker mode, extendSelection is disregarded
|
||||||
if (isAlreadySelected) {
|
if (props.multiple && alreadySelected) {
|
||||||
selectedFolders = selectedFolders.filter(f => f.id !== folderToChoose.id);
|
return 'remove';
|
||||||
|
} else if (props.multiple) {
|
||||||
|
return 'add';
|
||||||
|
} else if (!props.multiple && alreadySelected) {
|
||||||
|
return 'emit';
|
||||||
} else {
|
} else {
|
||||||
selectedFolders.push(folderToChoose);
|
return 'set';
|
||||||
}
|
}
|
||||||
emit('change-selection', selectedFolders);
|
|
||||||
} else {
|
} else {
|
||||||
if (isAlreadySelected) {
|
// explorer mode, props.multiple is disregarded
|
||||||
emit('selected', folderToChoose);
|
if (extendSelection && alreadySelected) {
|
||||||
} else {
|
return 'remove';
|
||||||
selectedFolders = [folderToChoose];
|
} else if (extendSelection) {
|
||||||
emit('change-selection', [folderToChoose]);
|
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) {
|
function move(target?: string | foundkey.entities.DriveFolder) {
|
||||||
// reset loading state
|
|
||||||
foldersLoading = true;
|
|
||||||
filesLoading = true;
|
|
||||||
|
|
||||||
if (!target) {
|
if (!target) {
|
||||||
goRoot();
|
goRoot();
|
||||||
return;
|
return;
|
||||||
|
@ -475,13 +437,13 @@ function addFolder(folderToAdd: foundkey.entities.DriveFolder, unshift = false)
|
||||||
const current = folder?.id ?? null;
|
const current = folder?.id ?? null;
|
||||||
if (current !== folderToAdd.parentId) return;
|
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) {
|
if (exist) {
|
||||||
foldersPaginationElem.updateItem(folderToAdd.id, () => folderToAdd);
|
paginationElem.updateItem(folderToAdd.id, () => folderToAdd);
|
||||||
} else if (unshift) {
|
} else if (unshift) {
|
||||||
foldersPaginationElem.prepend(folderToAdd);
|
paginationElem.prepend(folderToAdd);
|
||||||
} else {
|
} else {
|
||||||
foldersPaginationElem.append(folderToAdd);
|
paginationElem.append(folderToAdd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -489,24 +451,24 @@ function addFile(fileToAdd: foundkey.entities.DriveFile, unshift = false) {
|
||||||
const current = folder?.id ?? null;
|
const current = folder?.id ?? null;
|
||||||
if (current !== fileToAdd.folderId) return;
|
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) {
|
if (exist) {
|
||||||
filesPaginationElem.updateItem(fileToAdd.id, () => fileToAdd);
|
paginationElem.updateItem(fileToAdd.id, () => fileToAdd);
|
||||||
} else if (unshift) {
|
} else if (unshift) {
|
||||||
filesPaginationElem.prepend(fileToAdd);
|
paginationElem.prepend(fileToAdd);
|
||||||
} else {
|
} else {
|
||||||
filesPaginationElem.append(fileToAdd);
|
paginationElem.append(fileToAdd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeFolder(folderToRemove: foundkey.entities.DriveFolder | string): void {
|
function removeFolder(folderToRemove: foundkey.entities.DriveFolder | string): void {
|
||||||
const folderIdToRemove = typeof folderToRemove === 'object' ? folderToRemove.id : folderToRemove;
|
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 {
|
function removeFile(fileToRemove: foundkey.entities.DriveFile | string): void {
|
||||||
const fileIdToRemove = typeof fileToRemove === 'object' ? fileToRemove.id : fileToRemove;
|
const fileIdToRemove = typeof fileToRemove === 'object' ? fileToRemove.id : fileToRemove;
|
||||||
filesPaginationElem.removeItem(item => item.id === fileIdToRemove);
|
paginationElem.removeItem(item => item.id === fileIdToRemove);
|
||||||
}
|
}
|
||||||
|
|
||||||
function goRoot() {
|
function goRoot() {
|
||||||
|
@ -518,23 +480,14 @@ function goRoot() {
|
||||||
emit('move-root');
|
emit('move-root');
|
||||||
}
|
}
|
||||||
|
|
||||||
const foldersPagination = {
|
const pagination = {
|
||||||
endpoint: 'drive/folders' as const,
|
endpoint: 'drive/show' as const,
|
||||||
limit: 30,
|
limit: 30,
|
||||||
params: computed(() => ({
|
params: computed(() => ({
|
||||||
folderId: folder?.id ?? null,
|
folderId: folder?.id ?? null,
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
const filesPagination = {
|
|
||||||
endpoint: 'drive/files' as const,
|
|
||||||
limit: 30,
|
|
||||||
params: computed(() => ({
|
|
||||||
folderId: folder?.id ?? null,
|
|
||||||
type: props.type,
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
|
|
||||||
function getMenu() {
|
function getMenu() {
|
||||||
return [{
|
return [{
|
||||||
type: 'switch',
|
type: 'switch',
|
||||||
|
@ -678,25 +631,9 @@ onBeforeUnmount(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
> .contents {
|
> .contents {
|
||||||
|
display: grid;
|
||||||
> .folders,
|
grid-template-columns: repeat(5, 1fr);
|
||||||
> .files {
|
gap: .5em;
|
||||||
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 {
|
> .empty {
|
||||||
|
@ -704,13 +641,9 @@ onBeforeUnmount(() => {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
|
|
||||||
> p {
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .dropzone {
|
> .dropzone {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
|
@ -26,6 +26,7 @@ import * as foundkey from 'foundkey-js';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { stream } from '@/stream';
|
import { stream } from '@/stream';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
import { pleaseLoginOrRemote, urlForUser } from '@/scripts/please-login';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
user: foundkey.entities.UserDetailed,
|
user: foundkey.entities.UserDetailed,
|
||||||
|
@ -59,6 +60,8 @@ async function onClick() {
|
||||||
wait = true;
|
wait = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
pleaseLoginOrRemote(urlForUser(props.user));
|
||||||
|
|
||||||
if (isFollowing) {
|
if (isFollowing) {
|
||||||
const { canceled } = await os.confirm({
|
const { canceled } = await os.confirm({
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="value">
|
<div class="value">
|
||||||
<slot name="value"></slot>
|
<slot name="value"></slot>
|
||||||
|
<!-- FIXME the button should not be part of the overflow: hidden element to ensure its always visible -->
|
||||||
<button v-if="copy" v-tooltip="i18n.ts.copy" class="_textButton" style="margin-left: 0.5em;" @click="copy_"><i class="far fa-copy"></i></button>
|
<button v-if="copy" v-tooltip="i18n.ts.copy" class="_textButton" style="margin-left: 0.5em;" @click="copy_"><i class="far fa-copy"></i></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -126,6 +126,7 @@ function help(ev: MouseEvent) {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
height: 100px;
|
height: 100px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
|
|
@ -126,7 +126,7 @@ import XRenoteButton from './renote-button.vue';
|
||||||
import MkUrlPreview from '@/components/url-preview.vue';
|
import MkUrlPreview from '@/components/url-preview.vue';
|
||||||
import MkInstanceTicker from '@/components/instance-ticker.vue';
|
import MkInstanceTicker from '@/components/instance-ticker.vue';
|
||||||
import MkVisibility from '@/components/visibility.vue';
|
import MkVisibility from '@/components/visibility.vue';
|
||||||
import { pleaseLogin } from '@/scripts/please-login';
|
import { pleaseLoginOrRemote, urlForNote } from '@/scripts/please-login';
|
||||||
import { checkWordMute } from '@/scripts/check-word-mute';
|
import { checkWordMute } from '@/scripts/check-word-mute';
|
||||||
import { userPage } from '@/filters/user';
|
import { userPage } from '@/filters/user';
|
||||||
import { notePage } from '@/filters/note';
|
import { notePage } from '@/filters/note';
|
||||||
|
@ -195,7 +195,8 @@ useNoteCapture({
|
||||||
});
|
});
|
||||||
|
|
||||||
function reply(viaKeyboard = false): void {
|
function reply(viaKeyboard = false): void {
|
||||||
pleaseLogin();
|
pleaseLoginOrRemote(urlForNote(appearNote));
|
||||||
|
|
||||||
os.post({
|
os.post({
|
||||||
reply: appearNote,
|
reply: appearNote,
|
||||||
animation: !viaKeyboard,
|
animation: !viaKeyboard,
|
||||||
|
@ -205,7 +206,8 @@ function reply(viaKeyboard = false): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
function react(): void {
|
function react(): void {
|
||||||
pleaseLogin();
|
pleaseLoginOrRemote(urlForNote(appearNote));
|
||||||
|
|
||||||
blur();
|
blur();
|
||||||
reactionPicker.show(reactButton.value, reaction => {
|
reactionPicker.show(reactButton.value, reaction => {
|
||||||
os.api('notes/reactions/create', {
|
os.api('notes/reactions/create', {
|
||||||
|
|
|
@ -115,7 +115,7 @@ import XRenoteButton from './renote-button.vue';
|
||||||
import MkUrlPreview from '@/components/url-preview.vue';
|
import MkUrlPreview from '@/components/url-preview.vue';
|
||||||
import MkInstanceTicker from '@/components/instance-ticker.vue';
|
import MkInstanceTicker from '@/components/instance-ticker.vue';
|
||||||
import MkVisibility from '@/components/visibility.vue';
|
import MkVisibility from '@/components/visibility.vue';
|
||||||
import { pleaseLogin } from '@/scripts/please-login';
|
import { pleaseLoginOrRemote, urlForNote } from '@/scripts/please-login';
|
||||||
import { focusPrev, focusNext } from '@/scripts/focus';
|
import { focusPrev, focusNext } from '@/scripts/focus';
|
||||||
import { checkWordMute } from '@/scripts/check-word-mute';
|
import { checkWordMute } from '@/scripts/check-word-mute';
|
||||||
import { userPage } from '@/filters/user';
|
import { userPage } from '@/filters/user';
|
||||||
|
@ -188,7 +188,8 @@ useNoteCapture({
|
||||||
});
|
});
|
||||||
|
|
||||||
function reply(viaKeyboard = false): void {
|
function reply(viaKeyboard = false): void {
|
||||||
pleaseLogin();
|
pleaseLoginOrRemote(urlForNote(appearNote));
|
||||||
|
|
||||||
os.post({
|
os.post({
|
||||||
reply: appearNote,
|
reply: appearNote,
|
||||||
animation: !viaKeyboard,
|
animation: !viaKeyboard,
|
||||||
|
@ -198,7 +199,8 @@ function reply(viaKeyboard = false): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
function react(): void {
|
function react(): void {
|
||||||
pleaseLogin();
|
pleaseLoginOrRemote(urlForNote(appearNote));
|
||||||
|
|
||||||
blur();
|
blur();
|
||||||
reactionPicker.show(reactButton.value, reaction => {
|
reactionPicker.show(reactButton.value, reaction => {
|
||||||
os.api('notes/reactions/create', {
|
os.api('notes/reactions/create', {
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import * as foundkey from 'foundkey-js';
|
import * as foundkey from 'foundkey-js';
|
||||||
import { sum } from '@/scripts/array';
|
import { sum } from '@/scripts/array';
|
||||||
import { pleaseLogin } from '@/scripts/please-login';
|
import { pleaseLoginOrRemote, urlForNote } from '@/scripts/please-login';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { useInterval } from '@/scripts/use-interval';
|
import { useInterval } from '@/scripts/use-interval';
|
||||||
|
@ -68,7 +68,7 @@ if (props.note.poll.expiresAt) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const vote = async (id) => {
|
const vote = async (id) => {
|
||||||
pleaseLogin();
|
pleaseLoginOrRemote(urlForNote(props.note));
|
||||||
|
|
||||||
if (props.readOnly || closed.value || isVoted.value) return;
|
if (props.readOnly || closed.value || isVoted.value) return;
|
||||||
|
|
||||||
|
|
89
packages/client/src/components/remote-interact.vue
Normal file
89
packages/client/src/components/remote-interact.vue
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
<template>
|
||||||
|
<XModalWindow
|
||||||
|
ref="dialog"
|
||||||
|
:width="500"
|
||||||
|
@close="onClose()"
|
||||||
|
@closed="emit('closed')"
|
||||||
|
>
|
||||||
|
<template #header>{{ i18n.ts._remoteInteract.title }}</template>
|
||||||
|
|
||||||
|
<MkSpacer :margin-min="20" :margin-max="32" class="remote-interact" style="padding-top: 0;">
|
||||||
|
<p>{{ i18n.ts._remoteInteract.description }}</p>
|
||||||
|
<section>
|
||||||
|
<p>{{ i18n.ts._remoteInteract.urlInstructions }}</p>
|
||||||
|
<a :href="remoteUrl" class="_link">{{ remoteUrl }}</a>
|
||||||
|
<button v-tooltip="i18n.ts.copyUrl" class="_textButton" @click="copyUrl"><i class="far fa-copy"></i></button>
|
||||||
|
</section>
|
||||||
|
<aside>
|
||||||
|
<button class="_button" @click="signin()">{{ i18n.ts.login }}</button>
|
||||||
|
<button class="_button" @click="onClose()">{{ i18n.ts.cancel }}</button>
|
||||||
|
</aside>
|
||||||
|
</MkSpacer>
|
||||||
|
</XModalWindow>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import XModalWindow from '@/components/ui/modal-window.vue';
|
||||||
|
import XSigninDialog from '@/components/signin-dialog.vue';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
import * as os from '@/os';
|
||||||
|
import copyToClipboard from '@/scripts/copy-to-clipboard';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
remoteUrl: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'closed'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const dialog = $ref<InstanceType<typeof XModalWindow>>();
|
||||||
|
|
||||||
|
function onClose(): void {
|
||||||
|
emit('closed');
|
||||||
|
dialog.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function signin() {
|
||||||
|
os.popup(XSigninDialog, {
|
||||||
|
autoSet: true,
|
||||||
|
}, {}, 'closed');
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyUrl() {
|
||||||
|
copyToClipboard(props.remoteUrl);
|
||||||
|
os.success();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.remote-interact {
|
||||||
|
section {
|
||||||
|
padding: var(--radius);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
border: solid .2em var(--accentDarken);
|
||||||
|
|
||||||
|
> p {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
> button {
|
||||||
|
margin-left: .5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
aside {
|
||||||
|
background-color: var(--bg);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
margin-top: 1em;
|
||||||
|
|
||||||
|
> button {
|
||||||
|
padding: 1em;
|
||||||
|
border-radius: 2em;
|
||||||
|
background-color: var(--navBg);
|
||||||
|
color: var(--navFg);
|
||||||
|
margin: .5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -17,7 +17,7 @@
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { Note } from 'foundkey-js/built/entities';
|
import { Note } from 'foundkey-js/built/entities';
|
||||||
import XDetails from '@/components/users-tooltip.vue';
|
import XDetails from '@/components/users-tooltip.vue';
|
||||||
import { pleaseLogin } from '@/scripts/please-login';
|
import { pleaseLoginOrRemote, urlForNote } from '@/scripts/please-login';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { $i } from '@/account';
|
import { $i } from '@/account';
|
||||||
import { useTooltip } from '@/scripts/use-tooltip';
|
import { useTooltip } from '@/scripts/use-tooltip';
|
||||||
|
@ -51,7 +51,8 @@ useTooltip(buttonRef, async (showing) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
function renote(viaKeyboard = false): void {
|
function renote(viaKeyboard = false): void {
|
||||||
pleaseLogin();
|
pleaseLoginOrRemote(urlForNote(props.note));
|
||||||
|
|
||||||
os.popupMenu([{
|
os.popupMenu([{
|
||||||
text: i18n.ts.renote,
|
text: i18n.ts.renote,
|
||||||
icon: 'fas fa-retweet',
|
icon: 'fas fa-retweet',
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
{{ formatTime(duration) }}
|
{{ formatTime(duration) }}
|
||||||
</span>
|
</span>
|
||||||
<input class="volume" type="range" min="0" max="1" step="0.1" v-model="volume" @input="player.setVolume(volume)" />
|
<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>
|
<i class="fas fa-download"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import { EventEmitter } from 'eventemitter3';
|
import { EventEmitter } from 'eventemitter3';
|
||||||
import { Component, shallowRef, ShallowRef } from 'vue';
|
import { Component, shallowRef, ShallowRef } from 'vue';
|
||||||
import { pleaseLogin } from '@/scripts/please-login';
|
import { pleaseLoginOrPage } from '@/scripts/please-login';
|
||||||
import { safeURIDecode } from '@/scripts/safe-uri-decode';
|
import { safeURIDecode } from '@/scripts/safe-uri-decode';
|
||||||
|
|
||||||
type RouteDef = {
|
type RouteDef = {
|
||||||
|
@ -174,7 +174,7 @@ export class Router extends EventEmitter<{
|
||||||
}
|
}
|
||||||
|
|
||||||
if (res.route.loginRequired) {
|
if (res.route.loginRequired) {
|
||||||
pleaseLogin('/');
|
pleaseLoginOrPage('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSamePath = beforePath === path;
|
const isSamePath = beforePath === path;
|
||||||
|
|
|
@ -51,7 +51,7 @@
|
||||||
|
|
||||||
<FormSection>
|
<FormSection>
|
||||||
<div class="_formLinksGrid">
|
<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="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="/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>
|
<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 FormSection from '@/components/form/section.vue';
|
||||||
import FormLink from '@/components/form/link.vue';
|
import FormLink from '@/components/form/link.vue';
|
||||||
import MkButton from '@/components/ui/button.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 { selectFile } from '@/scripts/select-file';
|
||||||
import { isDeviceDarkmode } from '@/scripts/is-device-darkmode';
|
import { isDeviceDarkmode } from '@/scripts/is-device-darkmode';
|
||||||
import { ColdDeviceStorage , defaultStore } from '@/store';
|
import { ColdDeviceStorage , defaultStore } from '@/store';
|
||||||
|
@ -81,38 +81,38 @@ import { uniqueBy } from '@/scripts/array';
|
||||||
import { fetchThemes, getThemes } from '@/theme-store';
|
import { fetchThemes, getThemes } from '@/theme-store';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||||
|
|
||||||
const installedThemes = ref(getThemes());
|
const [installedThemes, builtinThemes] = await Promise.all([fetchThemes(), getBuiltinThemes()]);
|
||||||
const builtinThemes = getBuiltinThemesRef();
|
|
||||||
const instanceThemes = [];
|
const instanceThemes = [];
|
||||||
|
|
||||||
if (instance.defaultLightTheme != null) instanceThemes.push(JSON5.parse(instance.defaultLightTheme));
|
if (instance.defaultLightTheme != null) instanceThemes.push(JSON5.parse(instance.defaultLightTheme));
|
||||||
if (instance.defaultDarkTheme != null) instanceThemes.push(JSON5.parse(instance.defaultDarkTheme));
|
if (instance.defaultDarkTheme != null) instanceThemes.push(JSON5.parse(instance.defaultDarkTheme));
|
||||||
|
|
||||||
const themes = computed(() => uniqueBy([ ...instanceThemes, ...builtinThemes.value, ...installedThemes.value ], theme => theme.id));
|
const themes = uniqueBy([...installedThemes, ...instanceThemes, ...builtinThemes], theme => theme.id);
|
||||||
const darkThemes = computed(() => themes.value.filter(t => t.base === 'dark' || t.kind === 'dark'));
|
const darkThemes = themes.filter(t => t.base === 'dark' || t.kind === 'dark');
|
||||||
const lightThemes = computed(() => themes.value.filter(t => t.base === 'light' || t.kind === 'light'));
|
const lightThemes = themes.filter(t => t.base === 'light' || t.kind === 'light');
|
||||||
const darkTheme = ColdDeviceStorage.ref('darkTheme');
|
let darkTheme = $ref(ColdDeviceStorage.get('darkTheme'));
|
||||||
const darkThemeId = computed({
|
const darkThemeId = computed({
|
||||||
get() {
|
get() {
|
||||||
return darkTheme.value.id;
|
return darkTheme.id;
|
||||||
},
|
},
|
||||||
set(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({
|
const lightThemeId = computed({
|
||||||
get() {
|
get() {
|
||||||
return lightTheme.value.id;
|
return lightTheme.id;
|
||||||
},
|
},
|
||||||
set(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 darkMode = computed(defaultStore.makeGetterSetter('darkMode'));
|
||||||
const syncDeviceDarkMode = computed(ColdDeviceStorage.makeGetterSetter('syncDeviceDarkMode'));
|
const syncDeviceDarkMode = computed(ColdDeviceStorage.makeGetterSetter('syncDeviceDarkMode'));
|
||||||
const wallpaper = ref(localStorage.getItem('wallpaper'));
|
const wallpaper = ref(localStorage.getItem('wallpaper'));
|
||||||
const themesCount = installedThemes.value.length;
|
|
||||||
|
|
||||||
watch(syncDeviceDarkMode, () => {
|
watch(syncDeviceDarkMode, () => {
|
||||||
if (syncDeviceDarkMode.value) {
|
if (syncDeviceDarkMode.value) {
|
||||||
|
@ -129,16 +129,6 @@ watch(wallpaper, () => {
|
||||||
location.reload();
|
location.reload();
|
||||||
});
|
});
|
||||||
|
|
||||||
onActivated(() => {
|
|
||||||
fetchThemes().then(() => {
|
|
||||||
installedThemes.value = getThemes();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
fetchThemes().then(() => {
|
|
||||||
installedThemes.value = getThemes();
|
|
||||||
});
|
|
||||||
|
|
||||||
function setWallpaper(event) {
|
function setWallpaper(event) {
|
||||||
selectFile(event.currentTarget ?? event.target, null).then(file => {
|
selectFile(event.currentTarget ?? event.target, null).then(file => {
|
||||||
wallpaper.value = file.url;
|
wallpaper.value = file.url;
|
||||||
|
|
|
@ -23,10 +23,10 @@
|
||||||
<span v-if="user.isBot" :title="i18n.ts.isBot"><i class="fas fa-robot"></i></span>
|
<span v-if="user.isBot" :title="i18n.ts.isBot"><i class="fas fa-robot"></i></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span v-if="$i && $i.id != user.id && user.isFollowed" class="followed">{{ i18n.ts.followsYou }}</span>
|
<span v-if="$i?.id != user.id && user.isFollowed" class="followed">{{ i18n.ts.followsYou }}</span>
|
||||||
<div v-if="$i" class="actions">
|
<div class="actions">
|
||||||
<button class="menu _button" @click="menu"><i class="fas fa-ellipsis-h"></i></button>
|
<button class="menu _button" @click="menu"><i class="fas fa-ellipsis-h"></i></button>
|
||||||
<MkFollowButton v-if="$i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
|
<MkFollowButton v-if="$i?.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<MkAvatar class="avatar" :user="user" :disable-preview="true" :show-indicator="true"/>
|
<MkAvatar class="avatar" :user="user" :disable-preview="true" :show-indicator="true"/>
|
||||||
|
|
|
@ -20,7 +20,6 @@
|
||||||
import { defineAsyncComponent, computed, watch } from 'vue';
|
import { defineAsyncComponent, computed, watch } from 'vue';
|
||||||
import * as Acct from 'foundkey-js/built/acct';
|
import * as Acct from 'foundkey-js/built/acct';
|
||||||
import * as foundkey from 'foundkey-js';
|
import * as foundkey from 'foundkey-js';
|
||||||
import { getUserMenu } from '@/scripts/get-user-menu';
|
|
||||||
import { acct as getAcct } from '@/filters/user';
|
import { acct as getAcct } from '@/filters/user';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { useRouter } from '@/router';
|
import { useRouter } from '@/router';
|
||||||
|
@ -60,10 +59,6 @@ watch(() => props.acct, fetchUser, {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
function menu(ev) {
|
|
||||||
os.popupMenu(getUserMenu(user), ev.currentTarget ?? ev.target);
|
|
||||||
}
|
|
||||||
|
|
||||||
const headerTabs = $computed(() => [{
|
const headerTabs = $computed(() => [{
|
||||||
key: 'home',
|
key: 'home',
|
||||||
title: i18n.ts.overview,
|
title: i18n.ts.overview,
|
||||||
|
|
|
@ -2,8 +2,10 @@ import { defineAsyncComponent } from 'vue';
|
||||||
import { $i } from '@/account';
|
import { $i } from '@/account';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { popup } from '@/os';
|
import { popup } from '@/os';
|
||||||
|
import { url } from '@/config';
|
||||||
|
import { entities } from 'foundkey-js';
|
||||||
|
|
||||||
export function pleaseLogin(path?: string) {
|
export function pleaseLoginOrPage(path?: string) {
|
||||||
if ($i) return;
|
if ($i) return;
|
||||||
|
|
||||||
popup(defineAsyncComponent(() => import('@/components/signin-dialog.vue')), {
|
popup(defineAsyncComponent(() => import('@/components/signin-dialog.vue')), {
|
||||||
|
@ -19,3 +21,25 @@ export function pleaseLogin(path?: string) {
|
||||||
|
|
||||||
if (!path) throw new Error('signin required');
|
if (!path) throw new Error('signin required');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function pleaseLoginOrRemote(remoteUrl: string) {
|
||||||
|
if ($i) return;
|
||||||
|
|
||||||
|
popup(defineAsyncComponent(() => import('@/components/remote-interact.vue')), {
|
||||||
|
remoteUrl,
|
||||||
|
}, {}, 'closed');
|
||||||
|
|
||||||
|
throw new Error('signin required');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function urlForNote(note: entities.Note): string {
|
||||||
|
return note.url
|
||||||
|
?? note.uri
|
||||||
|
?? `${url}/notes/${note.id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function urlForUser(user: entities.User): string {
|
||||||
|
return user.url
|
||||||
|
?? user.uri
|
||||||
|
?? `${url}/users/${user.id}`;
|
||||||
|
}
|
||||||
|
|
|
@ -8,21 +8,22 @@ export function getThemes(): Theme[] {
|
||||||
return JSON.parse(localStorage.getItem(lsCacheKey) || '[]');
|
return JSON.parse(localStorage.getItem(lsCacheKey) || '[]');
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchThemes(): Promise<void> {
|
export async function fetchThemes(): Promise<Theme[]> {
|
||||||
if ($i == null) return;
|
if ($i == null) return [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const themes = await api('i/registry/get', { scope: ['client'], key: 'themes' });
|
const themes = await api('i/registry/get', { scope: ['client'], key: 'themes' });
|
||||||
localStorage.setItem(lsCacheKey, JSON.stringify(themes));
|
localStorage.setItem(lsCacheKey, JSON.stringify(themes));
|
||||||
|
return themes;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.code === 'NO_SUCH_KEY') return;
|
if (err.code === 'NO_SUCH_KEY') return [];
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addTheme(theme: Theme): Promise<void> {
|
export async function addTheme(theme: Theme): Promise<void> {
|
||||||
await fetchThemes();
|
const themes = await fetchThemes()
|
||||||
const themes = getThemes().concat(theme);
|
.then(themes => themes.concat(theme));
|
||||||
await api('i/registry/set', { scope: ['client'], key: 'themes', value: themes });
|
await api('i/registry/set', { scope: ['client'], key: 'themes', value: themes });
|
||||||
localStorage.setItem(lsCacheKey, JSON.stringify(themes));
|
localStorage.setItem(lsCacheKey, JSON.stringify(themes));
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,7 +64,6 @@
|
||||||
inputBorder: 'rgba(255, 255, 255, 0.1)',
|
inputBorder: 'rgba(255, 255, 255, 0.1)',
|
||||||
inputBorderHover: 'rgba(255, 255, 255, 0.2)',
|
inputBorderHover: 'rgba(255, 255, 255, 0.2)',
|
||||||
listItemHoverBg: 'rgba(255, 255, 255, 0.03)',
|
listItemHoverBg: 'rgba(255, 255, 255, 0.03)',
|
||||||
driveFolderBg: ':alpha<0.3<@accent',
|
|
||||||
wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
|
wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
|
||||||
badge: '#31b1ce',
|
badge: '#31b1ce',
|
||||||
messageBg: '@bg',
|
messageBg: '@bg',
|
||||||
|
|
|
@ -64,7 +64,6 @@
|
||||||
inputBorder: 'rgba(0, 0, 0, 0.1)',
|
inputBorder: 'rgba(0, 0, 0, 0.1)',
|
||||||
inputBorderHover: 'rgba(0, 0, 0, 0.2)',
|
inputBorderHover: 'rgba(0, 0, 0, 0.2)',
|
||||||
listItemHoverBg: 'rgba(0, 0, 0, 0.03)',
|
listItemHoverBg: 'rgba(0, 0, 0, 0.03)',
|
||||||
driveFolderBg: ':alpha<0.3<@accent',
|
|
||||||
wallpaperOverlay: 'rgba(255, 255, 255, 0.5)',
|
wallpaperOverlay: 'rgba(255, 255, 255, 0.5)',
|
||||||
badge: '#31b1ce',
|
badge: '#31b1ce',
|
||||||
messageBg: '@bg',
|
messageBg: '@bg',
|
||||||
|
|
|
@ -46,7 +46,6 @@
|
||||||
buttonHoverBg: 'rgba(255, 255, 255, 0.1)',
|
buttonHoverBg: 'rgba(255, 255, 255, 0.1)',
|
||||||
buttonGradateA: '@accent',
|
buttonGradateA: '@accent',
|
||||||
buttonGradateB: ':hue<-20<@accent',
|
buttonGradateB: ':hue<-20<@accent',
|
||||||
driveFolderBg: ':alpha<0.3<@accent',
|
|
||||||
fgHighlighted: ':lighten<3<@fg',
|
fgHighlighted: ':lighten<3<@fg',
|
||||||
panelHeaderBg: ':lighten<3<@panel',
|
panelHeaderBg: ':lighten<3<@panel',
|
||||||
panelHeaderFg: '@fg',
|
panelHeaderFg: '@fg',
|
||||||
|
|
|
@ -66,7 +66,6 @@
|
||||||
navIndicator: '@indicator',
|
navIndicator: '@indicator',
|
||||||
accentLighten: ':lighten<10<@accent',
|
accentLighten: ':lighten<10<@accent',
|
||||||
buttonHoverBg: 'rgba(255, 255, 255, 0.1)',
|
buttonHoverBg: 'rgba(255, 255, 255, 0.1)',
|
||||||
driveFolderBg: ':alpha<0.3<@accent',
|
|
||||||
fgHighlighted: ':lighten<3<@fg',
|
fgHighlighted: ':lighten<3<@fg',
|
||||||
fgTransparent: ':alpha<0.5<@fg',
|
fgTransparent: ':alpha<0.5<@fg',
|
||||||
panelHeaderBg: ':lighten<3<@panel',
|
panelHeaderBg: ':lighten<3<@panel',
|
||||||
|
|
|
@ -47,7 +47,6 @@
|
||||||
navIndicator: '@accent',
|
navIndicator: '@accent',
|
||||||
accentLighten: ':lighten<10<@accent',
|
accentLighten: ':lighten<10<@accent',
|
||||||
buttonHoverBg: 'rgba(0, 0, 0, 0.1)',
|
buttonHoverBg: 'rgba(0, 0, 0, 0.1)',
|
||||||
driveFolderBg: ':alpha<0.3<@accent',
|
|
||||||
fgHighlighted: ':darken<3<@fg',
|
fgHighlighted: ':darken<3<@fg',
|
||||||
fgTransparent: ':alpha<0.5<@fg',
|
fgTransparent: ':alpha<0.5<@fg',
|
||||||
panelHeaderBg: ':lighten<3<@panel',
|
panelHeaderBg: ':lighten<3<@panel',
|
||||||
|
|
Loading…
Reference in a new issue