Merge upstream changes #5
96 changed files with 2070 additions and 295 deletions
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
|
@ -14,5 +14,6 @@
|
||||||
"editor.formatOnSave": false,
|
"editor.formatOnSave": false,
|
||||||
"rust-analyzer.linkedProjects": [
|
"rust-analyzer.linkedProjects": [
|
||||||
"yume-mods/nyuukyou/Cargo.toml",
|
"yume-mods/nyuukyou/Cargo.toml",
|
||||||
|
"yume-mods/misskey-auto-deploy/Cargo.toml",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
13
CHANGELOG.md
13
CHANGELOG.md
|
@ -15,22 +15,33 @@
|
||||||
- どのアカウントで認証しようとしているのかがわかるように
|
- どのアカウントで認証しようとしているのかがわかるように
|
||||||
- 認証するアカウントを切り替えられるように
|
- 認証するアカウントを切り替えられるように
|
||||||
- Enhance: Self-XSS防止用の警告を追加
|
- Enhance: Self-XSS防止用の警告を追加
|
||||||
- Enhance: カタルーニャ語 (ca-ES) に対応
|
- Enhance: カタルーニャ語 (ca-ES) に対応
|
||||||
|
- Enhance: 個別お知らせページではMetaタグを出力するように
|
||||||
- Fix: 通知の範囲指定の設定項目が必要ない通知設定でも範囲指定の設定がでている問題を修正
|
- Fix: 通知の範囲指定の設定項目が必要ない通知設定でも範囲指定の設定がでている問題を修正
|
||||||
- Fix: Turnstileが失敗・期限切れした際にも成功扱いとなってしまう問題を修正
|
- Fix: Turnstileが失敗・期限切れした際にも成功扱いとなってしまう問題を修正
|
||||||
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/768)
|
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/768)
|
||||||
- Fix: デッキのタイムラインカラムで「センシティブなファイルを含むノートを表示」設定が使用できなかった問題を修正
|
- Fix: デッキのタイムラインカラムで「センシティブなファイルを含むノートを表示」設定が使用できなかった問題を修正
|
||||||
- Fix: Encode RSS urls with escape sequences before fetching allowing query parameters to be used
|
- Fix: Encode RSS urls with escape sequences before fetching allowing query parameters to be used
|
||||||
- Fix: リンク切れを修正
|
- Fix: リンク切れを修正
|
||||||
|
= Fix: ノート投稿ボタンにホバー時のスタイルが適用されていないのを修正
|
||||||
|
(Cherry-picked from https://github.com/taiyme/misskey/pull/305)
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
- Enhance: 起動前の疎通チェックで、DBとメイン以外のRedisの疎通確認も行うように
|
- Enhance: 起動前の疎通チェックで、DBとメイン以外のRedisの疎通確認も行うように
|
||||||
(Based on https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/588)
|
(Based on https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/588)
|
||||||
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/715)
|
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/715)
|
||||||
|
- fix(backend): フォロワーへのメッセージの絵文字をemojisに含めるように
|
||||||
- Fix: Nested proxy requestsを検出した際にブロックするように
|
- Fix: Nested proxy requestsを検出した際にブロックするように
|
||||||
[ghsa-gq5q-c77c-v236](https://github.com/misskey-dev/misskey/security/advisories/ghsa-gq5q-c77c-v236)
|
[ghsa-gq5q-c77c-v236](https://github.com/misskey-dev/misskey/security/advisories/ghsa-gq5q-c77c-v236)
|
||||||
- Fix: 招待コードの発行可能な残り数算出に使用すべきロールポリシーの値が違う問題を修正
|
- Fix: 招待コードの発行可能な残り数算出に使用すべきロールポリシーの値が違う問題を修正
|
||||||
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/706)
|
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/706)
|
||||||
|
- Fix: 連合への配信時に、acctの大小文字が区別されてしまい正しくメンションが処理されないことがある問題を修正
|
||||||
|
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/711)
|
||||||
|
- Fix: ローカルユーザーへのメンションを含むノートが連合される際に正しいURLに変換されないことがある問題を修正
|
||||||
|
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/712)
|
||||||
|
- Fix: FTT無効時にユーザーリストタイムラインが使用できない問題を修正
|
||||||
|
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/709)
|
||||||
|
- Enhance: リモートユーザーの照会をオリジナルにリダイレクトするように
|
||||||
|
|
||||||
### Misskey.js
|
### Misskey.js
|
||||||
- Fix: Stream初期化時、別途WebSocketを指定する場合の型定義を修正
|
- Fix: Stream初期化時、別途WebSocketを指定する場合の型定義を修正
|
||||||
|
|
|
@ -83,6 +83,9 @@ One should not add property that has defined before by other implementation, or
|
||||||
## Reviewers guide
|
## Reviewers guide
|
||||||
Be willing to comment on the good points and not just the things you want fixed 💯
|
Be willing to comment on the good points and not just the things you want fixed 💯
|
||||||
|
|
||||||
|
読んでおくといいやつ
|
||||||
|
- https://blog.lacolaco.net/posts/1e2cf439b3c2/
|
||||||
|
|
||||||
### Review perspective
|
### Review perspective
|
||||||
- Scope
|
- Scope
|
||||||
- Are the goals of the PR clear?
|
- Are the goals of the PR clear?
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"version": "2024.10.2-alpha.2",
|
"version": "2024.11.0-yumechinokuni.alpha.0",
|
||||||
"codename": "nasubi",
|
"codename": "nasubi",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
@ -406,8 +406,10 @@ export class MfmService {
|
||||||
mention: (node) => {
|
mention: (node) => {
|
||||||
const a = doc.createElement('a');
|
const a = doc.createElement('a');
|
||||||
const { username, host, acct } = node.props;
|
const { username, host, acct } = node.props;
|
||||||
const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username === username && remoteUser.host === host);
|
const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username.toLowerCase() === username.toLowerCase() && remoteUser.host?.toLowerCase() === host?.toLowerCase());
|
||||||
a.setAttribute('href', remoteUserInfo ? (remoteUserInfo.url ? remoteUserInfo.url : remoteUserInfo.uri) : `${this.config.url}/${acct}`);
|
a.setAttribute('href', remoteUserInfo
|
||||||
|
? (remoteUserInfo.url ? remoteUserInfo.url : remoteUserInfo.uri)
|
||||||
|
: `${this.config.url}/${acct.endsWith(`@${this.config.url}`) ? acct.substring(0, acct.length - this.config.url.length - 1) : acct}`);
|
||||||
a.className = 'u-url mention';
|
a.className = 'u-url mention';
|
||||||
a.textContent = acct;
|
a.textContent = acct;
|
||||||
return a;
|
return a;
|
||||||
|
|
|
@ -4,5 +4,5 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function sqlLikeEscape(s: string) {
|
export function sqlLikeEscape(s: string) {
|
||||||
return s.replace(/([%_])/g, '\\$1');
|
return s.replace(/([\\%_])/g, '\\$1');
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { IActivity } from '@/core/activitypub/type.js';
|
import { IActivity } from '@/core/activitypub/type.js';
|
||||||
import { isQuote, isRenote } from '@/misc/is-renote.js';
|
import { isQuote, isRenote } from '@/misc/is-renote.js';
|
||||||
|
import * as Acct from '@/misc/acct.js';
|
||||||
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions, FastifyBodyParser } from 'fastify';
|
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions, FastifyBodyParser } from 'fastify';
|
||||||
import type { FindOptionsWhere } from 'typeorm';
|
import type { FindOptionsWhere } from 'typeorm';
|
||||||
|
|
||||||
|
@ -486,6 +487,16 @@ export class ActivityPubServerService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// リモートだったらリダイレクト
|
||||||
|
if (user.host != null) {
|
||||||
|
if (user.uri == null || this.utilityService.isSelfHost(user.host)) {
|
||||||
|
reply.code(500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reply.redirect(user.uri, 301);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
reply.header('Cache-Control', 'public, max-age=180');
|
reply.header('Cache-Control', 'public, max-age=180');
|
||||||
this.setResponseType(request, reply);
|
this.setResponseType(request, reply);
|
||||||
return (this.apRendererService.addContext(await this.apRendererService.renderPerson(user as MiLocalUser)));
|
return (this.apRendererService.addContext(await this.apRendererService.renderPerson(user as MiLocalUser)));
|
||||||
|
@ -654,19 +665,20 @@ export class ActivityPubServerService {
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({
|
const user = await this.usersRepository.findOneBy({
|
||||||
id: userId,
|
id: userId,
|
||||||
host: IsNull(),
|
|
||||||
isSuspended: false,
|
isSuspended: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
return await this.userInfo(request, reply, user);
|
return await this.userInfo(request, reply, user);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get<{ Params: { user: string; } }>('/@:user', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => {
|
fastify.get<{ Params: { acct: string; } }>('/@:acct', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => {
|
||||||
vary(reply.raw, 'Accept');
|
vary(reply.raw, 'Accept');
|
||||||
|
|
||||||
|
const acct = Acct.parse(request.params.acct);
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({
|
const user = await this.usersRepository.findOneBy({
|
||||||
usernameLower: request.params.user.toLowerCase(),
|
usernameLower: acct.username,
|
||||||
host: IsNull(),
|
host: acct.host ?? IsNull(),
|
||||||
isSuspended: false,
|
isSuspended: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -465,6 +465,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
const newName = updates.name === undefined ? user.name : updates.name;
|
const newName = updates.name === undefined ? user.name : updates.name;
|
||||||
const newDescription = profileUpdates.description === undefined ? profile.description : profileUpdates.description;
|
const newDescription = profileUpdates.description === undefined ? profile.description : profileUpdates.description;
|
||||||
const newFields = profileUpdates.fields === undefined ? profile.fields : profileUpdates.fields;
|
const newFields = profileUpdates.fields === undefined ? profile.fields : profileUpdates.fields;
|
||||||
|
const newFollowedMessage = profileUpdates.followedMessage === undefined ? profile.followedMessage : profileUpdates.followedMessage;
|
||||||
|
|
||||||
if (newName != null) {
|
if (newName != null) {
|
||||||
let hasProhibitedWords = false;
|
let hasProhibitedWords = false;
|
||||||
|
@ -494,6 +495,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (newFollowedMessage != null) {
|
||||||
|
const tokens = mfm.parse(newFollowedMessage);
|
||||||
|
emojis = emojis.concat(extractCustomEmojisFromMfm(tokens));
|
||||||
|
}
|
||||||
|
|
||||||
updates.emojis = emojis;
|
updates.emojis = emojis;
|
||||||
updates.tags = tags;
|
updates.tags = tags;
|
||||||
|
|
||||||
|
|
|
@ -112,7 +112,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
|
|
||||||
this.activeUsersChart.read(me);
|
this.activeUsersChart.read(me);
|
||||||
|
|
||||||
await this.noteEntityService.packMany(timeline, me);
|
return await this.noteEntityService.packMany(timeline, me);
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeline = await this.fanoutTimelineEndpointService.timeline({
|
const timeline = await this.fanoutTimelineEndpointService.timeline({
|
||||||
|
|
|
@ -42,13 +42,26 @@ import { MetaEntityService } from '@/core/entities/MetaEntityService.js';
|
||||||
import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
|
import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
|
||||||
import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
|
import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
|
||||||
import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js';
|
import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js';
|
||||||
import type { ChannelsRepository, ClipsRepository, FlashsRepository, GalleryPostsRepository, MiMeta, NotesRepository, PagesRepository, ReversiGamesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
import type {
|
||||||
|
AnnouncementsRepository,
|
||||||
|
ChannelsRepository,
|
||||||
|
ClipsRepository,
|
||||||
|
FlashsRepository,
|
||||||
|
GalleryPostsRepository,
|
||||||
|
MiMeta,
|
||||||
|
NotesRepository,
|
||||||
|
PagesRepository,
|
||||||
|
ReversiGamesRepository,
|
||||||
|
UserProfilesRepository,
|
||||||
|
UsersRepository,
|
||||||
|
} from '@/models/_.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js';
|
import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
|
import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js';
|
import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js';
|
||||||
|
import { AnnouncementEntityService } from '@/core/entities/AnnouncementEntityService.js';
|
||||||
import { FeedService } from './FeedService.js';
|
import { FeedService } from './FeedService.js';
|
||||||
import { UrlPreviewService } from './UrlPreviewService.js';
|
import { UrlPreviewService } from './UrlPreviewService.js';
|
||||||
import { ClientLoggerService } from './ClientLoggerService.js';
|
import { ClientLoggerService } from './ClientLoggerService.js';
|
||||||
|
@ -104,6 +117,9 @@ export class ClientServerService {
|
||||||
@Inject(DI.reversiGamesRepository)
|
@Inject(DI.reversiGamesRepository)
|
||||||
private reversiGamesRepository: ReversiGamesRepository,
|
private reversiGamesRepository: ReversiGamesRepository,
|
||||||
|
|
||||||
|
@Inject(DI.announcementsRepository)
|
||||||
|
private announcementsRepository: AnnouncementsRepository,
|
||||||
|
|
||||||
private flashEntityService: FlashEntityService,
|
private flashEntityService: FlashEntityService,
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
|
@ -113,6 +129,7 @@ export class ClientServerService {
|
||||||
private clipEntityService: ClipEntityService,
|
private clipEntityService: ClipEntityService,
|
||||||
private channelEntityService: ChannelEntityService,
|
private channelEntityService: ChannelEntityService,
|
||||||
private reversiGameEntityService: ReversiGameEntityService,
|
private reversiGameEntityService: ReversiGameEntityService,
|
||||||
|
private announcementEntityService: AnnouncementEntityService,
|
||||||
private urlPreviewService: UrlPreviewService,
|
private urlPreviewService: UrlPreviewService,
|
||||||
private feedService: FeedService,
|
private feedService: FeedService,
|
||||||
private roleService: RoleService,
|
private roleService: RoleService,
|
||||||
|
@ -784,6 +801,24 @@ export class ClientServerService {
|
||||||
return await renderBase(reply);
|
return await renderBase(reply);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 個別お知らせページ
|
||||||
|
fastify.get<{ Params: { announcementId: string; } }>('/announcements/:announcementId', async (request, reply) => {
|
||||||
|
const announcement = await this.announcementsRepository.findOneBy({
|
||||||
|
id: request.params.announcementId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (announcement) {
|
||||||
|
const _announcement = await this.announcementEntityService.pack(announcement);
|
||||||
|
reply.header('Cache-Control', 'public, max-age=3600');
|
||||||
|
return await reply.view('announcement', {
|
||||||
|
announcement: _announcement,
|
||||||
|
...await this.generateCommonPugData(this.meta),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return await renderBase(reply);
|
||||||
|
}
|
||||||
|
});
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region noindex pages
|
//#region noindex pages
|
||||||
|
|
21
packages/backend/src/server/web/views/announcement.pug
Normal file
21
packages/backend/src/server/web/views/announcement.pug
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
extends ./base
|
||||||
|
|
||||||
|
block vars
|
||||||
|
- const title = announcement.title;
|
||||||
|
- const description = announcement.text.length > 100 ? announcement.text.slice(0, 100) + '…' : announcement.text;
|
||||||
|
- const url = `${config.url}/announcements/${announcement.id}`;
|
||||||
|
|
||||||
|
block title
|
||||||
|
= `${title} | ${instanceName}`
|
||||||
|
|
||||||
|
block desc
|
||||||
|
meta(name='description' content=description)
|
||||||
|
|
||||||
|
block og
|
||||||
|
meta(property='og:type' content='article')
|
||||||
|
meta(property='og:title' content= title)
|
||||||
|
meta(property='og:description' content= description)
|
||||||
|
meta(property='og:url' content= url)
|
||||||
|
if announcement.imageUrl
|
||||||
|
meta(property='og:image' content=announcement.imageUrl)
|
||||||
|
meta(property='twitter:card' content='summary_large_image')
|
|
@ -2,6 +2,7 @@ block vars
|
||||||
|
|
||||||
block loadClientEntry
|
block loadClientEntry
|
||||||
- const entry = config.frontendEntry;
|
- const entry = config.frontendEntry;
|
||||||
|
- const baseUrl = config.url;
|
||||||
|
|
||||||
doctype html
|
doctype html
|
||||||
|
|
||||||
|
@ -32,7 +33,7 @@ html
|
||||||
link(rel='icon' href= icon || '/favicon.ico')
|
link(rel='icon' href= icon || '/favicon.ico')
|
||||||
link(rel='apple-touch-icon' href= appleTouchIcon || '/apple-touch-icon.png')
|
link(rel='apple-touch-icon' href= appleTouchIcon || '/apple-touch-icon.png')
|
||||||
link(rel='manifest' href='/manifest.json')
|
link(rel='manifest' href='/manifest.json')
|
||||||
link(rel='search' type='application/opensearchdescription+xml' title=(title || "Misskey") href=`${url}/opensearch.xml`)
|
link(rel='search' type='application/opensearchdescription+xml' title=(title || "Misskey") href=`${baseUrl}/opensearch.xml`)
|
||||||
link(rel='prefetch' href=serverErrorImageUrl)
|
link(rel='prefetch' href=serverErrorImageUrl)
|
||||||
link(rel='prefetch' href=infoImageUrl)
|
link(rel='prefetch' href=infoImageUrl)
|
||||||
link(rel='prefetch' href=notFoundImageUrl)
|
link(rel='prefetch' href=notFoundImageUrl)
|
||||||
|
|
|
@ -230,6 +230,7 @@ describe('Webリソース', () => {
|
||||||
path: path('xxxxxxxxxx'),
|
path: path('xxxxxxxxxx'),
|
||||||
type: HTML,
|
type: HTML,
|
||||||
}));
|
}));
|
||||||
|
test.todo('HTMLとしてGETできる。(リモートユーザーでもリダイレクトせず)');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe.each([
|
describe.each([
|
||||||
|
@ -249,6 +250,7 @@ describe('Webリソース', () => {
|
||||||
path: path('xxxxxxxxxx'),
|
path: path('xxxxxxxxxx'),
|
||||||
accept,
|
accept,
|
||||||
}));
|
}));
|
||||||
|
test.todo('はオリジナルにリダイレクトされる。(リモートユーザー)');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ import { updateI18n, i18n } from '@/i18n.js';
|
||||||
import { $i, refreshAccount, login } from '@/account.js';
|
import { $i, refreshAccount, login } from '@/account.js';
|
||||||
import { defaultStore, ColdDeviceStorage } from '@/store.js';
|
import { defaultStore, ColdDeviceStorage } from '@/store.js';
|
||||||
import { fetchInstance, instance } from '@/instance.js';
|
import { fetchInstance, instance } from '@/instance.js';
|
||||||
import { deviceKind } from '@/scripts/device-kind.js';
|
import { deviceKind, updateDeviceKind } from '@/scripts/device-kind.js';
|
||||||
import { reloadChannel } from '@/scripts/unison-reload.js';
|
import { reloadChannel } from '@/scripts/unison-reload.js';
|
||||||
import { getUrlWithoutLoginId } from '@/scripts/login-id.js';
|
import { getUrlWithoutLoginId } from '@/scripts/login-id.js';
|
||||||
import { getAccountFromId } from '@/scripts/get-account-from-id.js';
|
import { getAccountFromId } from '@/scripts/get-account-from-id.js';
|
||||||
|
@ -185,6 +185,10 @@ export async function common(createVue: () => App<Element>) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(defaultStore.reactiveState.overridedDeviceKind, (kind) => {
|
||||||
|
updateDeviceKind(kind);
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
watch(defaultStore.reactiveState.useBlurEffectForModal, v => {
|
watch(defaultStore.reactiveState.useBlurEffectForModal, v => {
|
||||||
document.documentElement.style.setProperty('--MI-modalBgFilter', v ? 'blur(4px)' : 'none');
|
document.documentElement.style.setProperty('--MI-modalBgFilter', v ? 'blur(4px)' : 'none');
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|
|
@ -160,7 +160,7 @@ async function deleteAntenna() {
|
||||||
function addUser() {
|
function addUser() {
|
||||||
os.selectUser({ includeSelf: true }).then(user => {
|
os.selectUser({ includeSelf: true }).then(user => {
|
||||||
users.value = users.value.trim();
|
users.value = users.value.trim();
|
||||||
users.value += '\n@' + Misskey.acct.toString(user as any);
|
users.value += '\n@' + Misskey.acct.toString(user);
|
||||||
users.value = users.value.trim();
|
users.value = users.value.trim();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,11 +47,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { miLocalStorage } from '@/local-storage.js';
|
import { miLocalStorage } from '@/local-storage.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
channel: Record<string, any>;
|
channel: Misskey.entities.Channel;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const getLastReadedAt = (): number | null => {
|
const getLastReadedAt = (): number | null => {
|
||||||
|
|
|
@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
:withOkButton="true"
|
:withOkButton="true"
|
||||||
@close="cancel()"
|
@close="cancel()"
|
||||||
@ok="ok()"
|
@ok="ok()"
|
||||||
@closed="$emit('closed')"
|
@closed="emit('closed')"
|
||||||
>
|
>
|
||||||
<template #header>{{ i18n.ts.cropImage }}</template>
|
<template #header>{{ i18n.ts.cropImage }}</template>
|
||||||
<template #default="{ width, height }">
|
<template #default="{ width, height }">
|
||||||
|
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MkModalWindow ref="dialogEl" @close="cancel()" @closed="$emit('closed')">
|
<MkModalWindow ref="dialogEl" @close="cancel()" @closed="emit('closed')">
|
||||||
<template #header>:{{ emoji.name }}:</template>
|
<template #header>:{{ emoji.name }}:</template>
|
||||||
<template #default>
|
<template #default>
|
||||||
<MkSpacer>
|
<MkSpacer>
|
||||||
|
|
|
@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<div v-if="(showOkButton || showCancelButton) && !actions" :class="$style.buttons">
|
<div v-if="(showOkButton || showCancelButton) && !actions" :class="$style.buttons">
|
||||||
<MkButton v-if="showOkButton" data-cy-modal-dialog-ok inline primary rounded :autofocus="!input && !select" :disabled="okButtonDisabledReason" @click="ok">{{ okText ?? ((showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt) }}</MkButton>
|
<MkButton v-if="showOkButton" data-cy-modal-dialog-ok inline primary rounded :autofocus="!input && !select" :disabled="okButtonDisabledReason != null" @click="ok">{{ okText ?? ((showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt) }}</MkButton>
|
||||||
<MkButton v-if="showCancelButton || input || select" data-cy-modal-dialog-cancel inline rounded @click="cancel">{{ cancelText ?? i18n.ts.cancel }}</MkButton>
|
<MkButton v-if="showCancelButton || input || select" data-cy-modal-dialog-cancel inline rounded @click="cancel">{{ cancelText ?? i18n.ts.cancel }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="actions" :class="$style.buttons">
|
<div v-if="actions" :class="$style.buttons">
|
||||||
|
@ -98,7 +98,7 @@ const props = withDefaults(defineProps<{
|
||||||
text: string;
|
text: string;
|
||||||
primary?: boolean,
|
primary?: boolean,
|
||||||
danger?: boolean,
|
danger?: boolean,
|
||||||
callback: (...args: any[]) => void;
|
callback: (...args: unknown[]) => void;
|
||||||
}[];
|
}[];
|
||||||
showOkButton?: boolean;
|
showOkButton?: boolean;
|
||||||
showCancelButton?: boolean;
|
showCancelButton?: boolean;
|
||||||
|
|
|
@ -157,7 +157,7 @@ const ilFilesObserver = new IntersectionObserver(
|
||||||
(entries) => entries.some((entry) => entry.isIntersecting) && !fetching.value && moreFiles.value && fetchMoreFiles(),
|
(entries) => entries.some((entry) => entry.isIntersecting) && !fetching.value && moreFiles.value && fetchMoreFiles(),
|
||||||
);
|
);
|
||||||
|
|
||||||
const sortModeSelect = ref('+createdAt');
|
const sortModeSelect = ref<NonNullable<Misskey.entities.DriveFilesRequest['sort']>>('+createdAt');
|
||||||
|
|
||||||
watch(folder, () => emit('cd', folder.value));
|
watch(folder, () => emit('cd', folder.value));
|
||||||
watch(sortModeSelect, () => {
|
watch(sortModeSelect, () => {
|
||||||
|
@ -198,7 +198,7 @@ function onStreamDriveFolderDeleted(folderId: string) {
|
||||||
removeFolder(folderId);
|
removeFolder(folderId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDragover(ev: DragEvent): any {
|
function onDragover(ev: DragEvent) {
|
||||||
if (!ev.dataTransfer) return;
|
if (!ev.dataTransfer) return;
|
||||||
|
|
||||||
// ドラッグ元が自分自身の所有するアイテムだったら
|
// ドラッグ元が自分自身の所有するアイテムだったら
|
||||||
|
@ -243,7 +243,7 @@ function onDragleave() {
|
||||||
draghover.value = false;
|
draghover.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDrop(ev: DragEvent): any {
|
function onDrop(ev: DragEvent) {
|
||||||
draghover.value = false;
|
draghover.value = false;
|
||||||
|
|
||||||
if (!ev.dataTransfer) return;
|
if (!ev.dataTransfer) return;
|
||||||
|
@ -332,7 +332,7 @@ function createFolder() {
|
||||||
title: i18n.ts.createFolder,
|
title: i18n.ts.createFolder,
|
||||||
placeholder: i18n.ts.folderName,
|
placeholder: i18n.ts.folderName,
|
||||||
}).then(({ canceled, result: name }) => {
|
}).then(({ canceled, result: name }) => {
|
||||||
if (canceled) return;
|
if (canceled || name == null) return;
|
||||||
misskeyApi('drive/folders/create', {
|
misskeyApi('drive/folders/create', {
|
||||||
name: name,
|
name: name,
|
||||||
parentId: folder.value ? folder.value.id : undefined,
|
parentId: folder.value ? folder.value.id : undefined,
|
||||||
|
|
|
@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
:scroll="false"
|
:scroll="false"
|
||||||
:withOkButton="false"
|
:withOkButton="false"
|
||||||
@close="cancel()"
|
@close="cancel()"
|
||||||
@closed="$emit('closed')"
|
@closed="emit('closed')"
|
||||||
>
|
>
|
||||||
<template #header><i class="ti ti-code"></i> {{ i18n.ts._embedCodeGen.title }}</template>
|
<template #header><i class="ti ti-code"></i> {{ i18n.ts._embedCodeGen.title }}</template>
|
||||||
|
|
||||||
|
|
|
@ -90,7 +90,7 @@ function computeButtonTitle(ev: MouseEvent): void {
|
||||||
elm.title = getEmojiName(emoji);
|
elm.title = getEmojiName(emoji);
|
||||||
}
|
}
|
||||||
|
|
||||||
function nestedChosen(emoji: any, ev: MouseEvent) {
|
function nestedChosen(emoji: string, ev: MouseEvent) {
|
||||||
emit('chosen', emoji, ev);
|
emit('chosen', emoji, ev);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -409,7 +409,7 @@ function computeButtonTitle(ev: MouseEvent): void {
|
||||||
elm.title = getEmojiName(emoji);
|
elm.title = getEmojiName(emoji);
|
||||||
}
|
}
|
||||||
|
|
||||||
function chosen(emoji: any, ev?: MouseEvent) {
|
function chosen(emoji: string | Misskey.entities.EmojiSimple | UnicodeEmojiDef, ev?: MouseEvent) {
|
||||||
const el = ev && (ev.currentTarget ?? ev.target) as HTMLElement | null | undefined;
|
const el = ev && (ev.currentTarget ?? ev.target) as HTMLElement | null | undefined;
|
||||||
if (el) {
|
if (el) {
|
||||||
const rect = el.getBoundingClientRect();
|
const rect = el.getBoundingClientRect();
|
||||||
|
@ -426,7 +426,7 @@ function chosen(emoji: any, ev?: MouseEvent) {
|
||||||
// 最近使った絵文字更新
|
// 最近使った絵文字更新
|
||||||
if (!pinned.value?.includes(key)) {
|
if (!pinned.value?.includes(key)) {
|
||||||
let recents = defaultStore.state.recentlyUsedEmojis;
|
let recents = defaultStore.state.recentlyUsedEmojis;
|
||||||
recents = recents.filter((emoji: any) => emoji !== key);
|
recents = recents.filter((emoji) => emoji !== key);
|
||||||
recents.unshift(key);
|
recents.unshift(key);
|
||||||
defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32));
|
defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32));
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,7 +73,7 @@ export type Extension = {
|
||||||
author: string;
|
author: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
permissions?: string[];
|
permissions?: string[];
|
||||||
config?: Record<string, any>;
|
config?: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
} | {
|
} | {
|
||||||
type: 'theme';
|
type: 'theme';
|
||||||
|
|
|
@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
@click="cancel()"
|
@click="cancel()"
|
||||||
@ok="ok()"
|
@ok="ok()"
|
||||||
@close="cancel()"
|
@close="cancel()"
|
||||||
@closed="$emit('closed')"
|
@closed="emit('closed')"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
{{ title }}
|
{{ title }}
|
||||||
|
|
|
@ -44,7 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, onUnmounted, nextTick, ref, shallowRef, watch, computed, toRefs } from 'vue';
|
import { onMounted, onUnmounted, nextTick, ref, shallowRef, watch, computed, toRefs, InputHTMLAttributes } from 'vue';
|
||||||
import { debounce } from 'throttle-debounce';
|
import { debounce } from 'throttle-debounce';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import { useInterval } from '@@/js/use-interval.js';
|
import { useInterval } from '@@/js/use-interval.js';
|
||||||
|
@ -53,7 +53,7 @@ import { Autocomplete, SuggestionType } from '@/scripts/autocomplete.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: string | number | null;
|
modelValue: string | number | null;
|
||||||
type?: 'text' | 'number' | 'password' | 'email' | 'url' | 'date' | 'time' | 'search' | 'datetime-local';
|
type?: InputHTMLAttributes['type'];
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
@ -64,8 +64,8 @@ const props = defineProps<{
|
||||||
mfmAutocomplete?: boolean | SuggestionType[],
|
mfmAutocomplete?: boolean | SuggestionType[],
|
||||||
autocapitalize?: string;
|
autocapitalize?: string;
|
||||||
spellcheck?: boolean;
|
spellcheck?: boolean;
|
||||||
inputmode?: 'none' | 'text' | 'search' | 'email' | 'url' | 'numeric' | 'tel' | 'decimal';
|
inputmode?: InputHTMLAttributes['inputmode'];
|
||||||
step?: any;
|
step?: InputHTMLAttributes['step'];
|
||||||
datalist?: string[];
|
datalist?: string[];
|
||||||
min?: number;
|
min?: number;
|
||||||
max?: number;
|
max?: number;
|
||||||
|
|
|
@ -118,7 +118,7 @@ import { hms } from '@/filters/hms.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { isFullscreenNotSupported } from '@/scripts/device-kind.js';
|
import { exitFullscreen, requestFullscreen } from '@/scripts/fullscreen.js';
|
||||||
import hasAudio from '@/scripts/media-has-audio.js';
|
import hasAudio from '@/scripts/media-has-audio.js';
|
||||||
import MkMediaRange from '@/components/MkMediaRange.vue';
|
import MkMediaRange from '@/components/MkMediaRange.vue';
|
||||||
import { $i, iAmModerator } from '@/account.js';
|
import { $i, iAmModerator } from '@/account.js';
|
||||||
|
@ -334,26 +334,21 @@ function togglePlayPause() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleFullscreen() {
|
function toggleFullscreen() {
|
||||||
if (isFullscreenNotSupported && videoEl.value) {
|
if (playerEl.value == null || videoEl.value == null) return;
|
||||||
if (isFullscreen.value) {
|
if (isFullscreen.value) {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
exitFullscreen({
|
||||||
//@ts-ignore
|
videoEl: videoEl.value,
|
||||||
videoEl.value.webkitExitFullscreen();
|
});
|
||||||
isFullscreen.value = false;
|
isFullscreen.value = false;
|
||||||
} else {
|
} else {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
requestFullscreen({
|
||||||
//@ts-ignore
|
videoEl: videoEl.value,
|
||||||
videoEl.value.webkitEnterFullscreen();
|
playerEl: playerEl.value,
|
||||||
isFullscreen.value = true;
|
options: {
|
||||||
}
|
navigationUI: 'hide',
|
||||||
} else if (playerEl.value) {
|
},
|
||||||
if (isFullscreen.value) {
|
});
|
||||||
document.exitFullscreen();
|
isFullscreen.value = true;
|
||||||
isFullscreen.value = false;
|
|
||||||
} else {
|
|
||||||
playerEl.value.requestFullscreen({ navigationUI: 'hide' });
|
|
||||||
isFullscreen.value = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -454,8 +449,10 @@ watch(loop, (to) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(hide, (to) => {
|
watch(hide, (to) => {
|
||||||
if (to && isFullscreen.value) {
|
if (videoEl.value && to && isFullscreen.value) {
|
||||||
document.exitFullscreen();
|
exitFullscreen({
|
||||||
|
videoEl: videoEl.value,
|
||||||
|
});
|
||||||
isFullscreen.value = false;
|
isFullscreen.value = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -227,7 +227,7 @@ const emit = defineEmits<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const inTimeline = inject<boolean>('inTimeline', false);
|
const inTimeline = inject<boolean>('inTimeline', false);
|
||||||
const tl_withSensitive = inject<Ref<boolean>>('tl_withSensitive', ref(false));
|
const tl_withSensitive = inject<Ref<boolean>>('tl_withSensitive', ref(true));
|
||||||
const inChannel = inject('inChannel', null);
|
const inChannel = inject('inChannel', null);
|
||||||
const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null);
|
const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null);
|
||||||
|
|
||||||
|
@ -292,15 +292,18 @@ function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string
|
||||||
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: false): boolean | 'sensitiveMute';
|
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: false): boolean | 'sensitiveMute';
|
||||||
*/
|
*/
|
||||||
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly = false): boolean | 'sensitiveMute' {
|
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly = false): boolean | 'sensitiveMute' {
|
||||||
if (mutedWords == null) return false;
|
if (mutedWords != null) {
|
||||||
|
if (checkWordMute(noteToCheck, $i, mutedWords)) return true;
|
||||||
if (checkWordMute(noteToCheck, $i, mutedWords)) return true;
|
if (noteToCheck.reply && checkWordMute(noteToCheck.reply, $i, mutedWords)) return true;
|
||||||
if (noteToCheck.reply && checkWordMute(noteToCheck.reply, $i, mutedWords)) return true;
|
if (noteToCheck.renote && checkWordMute(noteToCheck.renote, $i, mutedWords)) return true;
|
||||||
if (noteToCheck.renote && checkWordMute(noteToCheck.renote, $i, mutedWords)) return true;
|
}
|
||||||
|
|
||||||
if (checkOnly) return false;
|
if (checkOnly) return false;
|
||||||
|
|
||||||
if (inTimeline && !tl_withSensitive.value && noteToCheck.files?.some((v) => v.isSensitive)) return 'sensitiveMute';
|
if (inTimeline && tl_withSensitive.value === false && noteToCheck.files?.some((v) => v.isSensitive)) {
|
||||||
|
return 'sensitiveMute';
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -53,7 +53,7 @@ const props = withDefaults(defineProps<{
|
||||||
|
|
||||||
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
|
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||||
|
|
||||||
const typesMap: TypesMap = notificationTypes.reduce((p, t) => ({ ...p, [t]: ref<boolean>(!props.excludeTypes.includes(t)) }), {} as any);
|
const typesMap = notificationTypes.reduce((p, t) => ({ ...p, [t]: ref<boolean>(!props.excludeTypes.includes(t)) }), {} as TypesMap);
|
||||||
|
|
||||||
function ok() {
|
function ok() {
|
||||||
emit('done', {
|
emit('done', {
|
||||||
|
|
|
@ -39,7 +39,7 @@ import number from '@/filters/number.js';
|
||||||
import XValue from '@/components/MkObjectView.value.vue';
|
import XValue from '@/components/MkObjectView.value.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
value: any;
|
value: unknown;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const collapsed = reactive({});
|
const collapsed = reactive({});
|
||||||
|
@ -50,19 +50,19 @@ if (isObject(props.value)) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isObject(v): boolean {
|
function isObject(v: unknown): v is Record<PropertyKey, unknown> {
|
||||||
return typeof v === 'object' && !Array.isArray(v) && v !== null;
|
return typeof v === 'object' && !Array.isArray(v) && v !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isArray(v): boolean {
|
function isArray(v: unknown): v is unknown[] {
|
||||||
return Array.isArray(v);
|
return Array.isArray(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isEmpty(v): boolean {
|
function isEmpty(v: unknown): v is Record<PropertyKey, never> | never[] {
|
||||||
return (isArray(v) && v.length === 0) || (isObject(v) && Object.keys(v).length === 0);
|
return (isArray(v) && v.length === 0) || (isObject(v) && Object.keys(v).length === 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function collapsable(v): boolean {
|
function collapsable(v: unknown): boolean {
|
||||||
return (isObject(v) || isArray(v)) && !isEmpty(v);
|
return (isObject(v) || isArray(v)) && !isEmpty(v);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
:buttonsLeft="buttonsLeft"
|
:buttonsLeft="buttonsLeft"
|
||||||
:buttonsRight="buttonsRight"
|
:buttonsRight="buttonsRight"
|
||||||
:contextmenu="contextmenu"
|
:contextmenu="contextmenu"
|
||||||
@closed="$emit('closed')"
|
@closed="emit('closed')"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<template v-if="pageMetadata">
|
<template v-if="pageMetadata">
|
||||||
|
@ -30,17 +30,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, onMounted, onUnmounted, provide, ref, shallowRef } from 'vue';
|
import { computed, onMounted, onUnmounted, provide, ref, shallowRef } from 'vue';
|
||||||
|
import { url } from '@@/js/config.js';
|
||||||
|
import { getScrollContainer } from '@@/js/scroll.js';
|
||||||
import RouterView from '@/components/global/RouterView.vue';
|
import RouterView from '@/components/global/RouterView.vue';
|
||||||
import MkWindow from '@/components/MkWindow.vue';
|
import MkWindow from '@/components/MkWindow.vue';
|
||||||
import { popout as _popout } from '@/scripts/popout.js';
|
import { popout as _popout } from '@/scripts/popout.js';
|
||||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||||
import { url } from '@@/js/config.js';
|
|
||||||
import { useScrollPositionManager } from '@/nirax.js';
|
import { useScrollPositionManager } from '@/nirax.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
|
import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
|
||||||
import { openingWindowsCount } from '@/os.js';
|
import { openingWindowsCount } from '@/os.js';
|
||||||
import { claimAchievement } from '@/scripts/achievements.js';
|
import { claimAchievement } from '@/scripts/achievements.js';
|
||||||
import { getScrollContainer } from '@@/js/scroll.js';
|
|
||||||
import { useRouterFactory } from '@/router/supplier.js';
|
import { useRouterFactory } from '@/router/supplier.js';
|
||||||
import { mainRouter } from '@/router/main.js';
|
import { mainRouter } from '@/router/main.js';
|
||||||
|
|
||||||
|
@ -48,7 +48,7 @@ const props = defineProps<{
|
||||||
initialPath: string;
|
initialPath: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'closed'): void;
|
(ev: 'closed'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
@ -58,7 +58,7 @@ const windowRouter = routerFactory(props.initialPath);
|
||||||
const contents = shallowRef<HTMLElement | null>(null);
|
const contents = shallowRef<HTMLElement | null>(null);
|
||||||
const pageMetadata = ref<null | PageMetadata>(null);
|
const pageMetadata = ref<null | PageMetadata>(null);
|
||||||
const windowEl = shallowRef<InstanceType<typeof MkWindow>>();
|
const windowEl = shallowRef<InstanceType<typeof MkWindow>>();
|
||||||
const history = ref<{ path: string; key: any; }[]>([{
|
const history = ref<{ path: string; key: string; }[]>([{
|
||||||
path: windowRouter.getCurrentPath(),
|
path: windowRouter.getCurrentPath(),
|
||||||
key: windowRouter.getCurrentKey(),
|
key: windowRouter.getCurrentKey(),
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -19,7 +19,7 @@ defineProps<{
|
||||||
items: MenuItem[];
|
items: MenuItem[];
|
||||||
align?: 'center' | string;
|
align?: 'center' | string;
|
||||||
width?: number;
|
width?: number;
|
||||||
src?: any;
|
src?: HTMLElement | null;
|
||||||
returnFocusTo?: HTMLElement | null;
|
returnFocusTo?: HTMLElement | null;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
|
|
@ -129,25 +129,13 @@ import { miLocalStorage } from '@/local-storage.js';
|
||||||
import { claimAchievement } from '@/scripts/achievements.js';
|
import { claimAchievement } from '@/scripts/achievements.js';
|
||||||
import { emojiPicker } from '@/scripts/emoji-picker.js';
|
import { emojiPicker } from '@/scripts/emoji-picker.js';
|
||||||
import { mfmFunctionPicker } from '@/scripts/mfm-function-picker.js';
|
import { mfmFunctionPicker } from '@/scripts/mfm-function-picker.js';
|
||||||
|
import type { PostFormProps } from '@/types/post-form.js';
|
||||||
|
|
||||||
const $i = signinRequired();
|
const $i = signinRequired();
|
||||||
|
|
||||||
const modal = inject('modal');
|
const modal = inject('modal');
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<PostFormProps & {
|
||||||
reply?: Misskey.entities.Note;
|
|
||||||
renote?: Misskey.entities.Note;
|
|
||||||
channel?: Misskey.entities.Channel; // TODO
|
|
||||||
mention?: Misskey.entities.User;
|
|
||||||
specified?: Misskey.entities.UserDetailed;
|
|
||||||
initialText?: string;
|
|
||||||
initialCw?: string;
|
|
||||||
initialVisibility?: (typeof Misskey.noteVisibilities)[number];
|
|
||||||
initialFiles?: Misskey.entities.DriveFile[];
|
|
||||||
initialLocalOnly?: boolean;
|
|
||||||
initialVisibleUsers?: Misskey.entities.UserDetailed[];
|
|
||||||
initialNote?: Misskey.entities.Note;
|
|
||||||
instant?: boolean;
|
|
||||||
fixed?: boolean;
|
fixed?: boolean;
|
||||||
autofocus?: boolean;
|
autofocus?: boolean;
|
||||||
freezeAfterPosted?: boolean;
|
freezeAfterPosted?: boolean;
|
||||||
|
@ -955,8 +943,8 @@ function showActions(ev: MouseEvent) {
|
||||||
action.handler({
|
action.handler({
|
||||||
text: text.value,
|
text: text.value,
|
||||||
cw: cw.value,
|
cw: cw.value,
|
||||||
}, (key, value: any) => {
|
}, (key, value) => {
|
||||||
if (typeof key !== 'string') return;
|
if (typeof key !== 'string' || typeof value !== 'string') return;
|
||||||
if (key === 'text') { text.value = value; }
|
if (key === 'text') { text.value = value; }
|
||||||
if (key === 'cw') { useCw.value = value !== null; cw.value = value; }
|
if (key === 'cw') { useCw.value = value !== null; cw.value = value; }
|
||||||
});
|
});
|
||||||
|
@ -1120,7 +1108,7 @@ defineExpose({
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|
||||||
.submitInner {
|
> .submitInner {
|
||||||
outline: 2px solid var(--MI_THEME-fgOnAccent);
|
outline: 2px solid var(--MI_THEME-fgOnAccent);
|
||||||
outline-offset: -4px;
|
outline-offset: -4px;
|
||||||
}
|
}
|
||||||
|
@ -1135,13 +1123,13 @@ defineExpose({
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(:disabled):hover {
|
&:not(:disabled):hover {
|
||||||
> .inner {
|
> .submitInner {
|
||||||
background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
|
background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(:disabled):active {
|
&:not(:disabled):active {
|
||||||
> .inner {
|
> .submitInner {
|
||||||
background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
|
background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template>
|
<template>
|
||||||
<div v-show="props.modelValue.length != 0" :class="$style.root">
|
<div v-show="props.modelValue.length != 0" :class="$style.root">
|
||||||
<Sortable :modelValue="props.modelValue" :class="$style.files" itemKey="id" :animation="150" :delay="100" :delayOnTouchOnly="true" @update:modelValue="v => emit('update:modelValue', v)">
|
<Sortable :modelValue="props.modelValue" :class="$style.files" itemKey="id" :animation="150" :delay="100" :delayOnTouchOnly="true" @update:modelValue="v => emit('update:modelValue', v)">
|
||||||
<template #item="{element}">
|
<template #item="{ element }">
|
||||||
<div
|
<div
|
||||||
:class="$style.file"
|
:class="$style.file"
|
||||||
role="button"
|
role="button"
|
||||||
|
@ -38,14 +38,14 @@ import type { MenuItem } from '@/types/menu.js';
|
||||||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: any[];
|
modelValue: Misskey.entities.DriveFile[];
|
||||||
detachMediaFn?: (id: string) => void;
|
detachMediaFn?: (id: string) => void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const mock = inject<boolean>('mock', false);
|
const mock = inject<boolean>('mock', false);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'update:modelValue', value: any[]): void;
|
(ev: 'update:modelValue', value: Misskey.entities.DriveFile[]): void;
|
||||||
(ev: 'detach', id: string): void;
|
(ev: 'detach', id: string): void;
|
||||||
(ev: 'changeSensitive', file: Misskey.entities.DriveFile, isSensitive: boolean): void;
|
(ev: 'changeSensitive', file: Misskey.entities.DriveFile, isSensitive: boolean): void;
|
||||||
(ev: 'changeName', file: Misskey.entities.DriveFile, newName: string): void;
|
(ev: 'changeName', file: Misskey.entities.DriveFile, newName: string): void;
|
||||||
|
@ -113,7 +113,7 @@ async function rename(file) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function describe(file) {
|
async function describe(file: Misskey.entities.DriveFile) {
|
||||||
if (mock) return;
|
if (mock) return;
|
||||||
|
|
||||||
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), {
|
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), {
|
||||||
|
|
|
@ -11,23 +11,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { shallowRef } from 'vue';
|
import { shallowRef } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
|
||||||
import MkModal from '@/components/MkModal.vue';
|
import MkModal from '@/components/MkModal.vue';
|
||||||
import MkPostForm from '@/components/MkPostForm.vue';
|
import MkPostForm from '@/components/MkPostForm.vue';
|
||||||
|
import type { PostFormProps } from '@/types/post-form.js';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<PostFormProps & {
|
||||||
reply?: Misskey.entities.Note;
|
|
||||||
renote?: Misskey.entities.Note;
|
|
||||||
channel?: any; // TODO
|
|
||||||
mention?: Misskey.entities.User;
|
|
||||||
specified?: Misskey.entities.UserDetailed;
|
|
||||||
initialText?: string;
|
|
||||||
initialCw?: string;
|
|
||||||
initialVisibility?: (typeof Misskey.noteVisibilities)[number];
|
|
||||||
initialFiles?: Misskey.entities.DriveFile[];
|
|
||||||
initialLocalOnly?: boolean;
|
|
||||||
initialVisibleUsers?: Misskey.entities.UserDetailed[];
|
|
||||||
initialNote?: Misskey.entities.Note;
|
|
||||||
instant?: boolean;
|
instant?: boolean;
|
||||||
fixed?: boolean;
|
fixed?: boolean;
|
||||||
autofocus?: boolean;
|
autofocus?: boolean;
|
||||||
|
|
|
@ -24,17 +24,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup generic="T extends unknown">
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: any;
|
modelValue: T;
|
||||||
value: any;
|
value: T;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'update:modelValue', value: any): void;
|
(ev: 'update:modelValue', value: T): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const checked = computed(() => props.modelValue === props.value);
|
const checked = computed(() => props.modelValue === props.value);
|
||||||
|
|
|
@ -23,6 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import { } from 'vue';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
import { getEmojiName } from '@@/js/emojilist.js';
|
import { getEmojiName } from '@@/js/emojilist.js';
|
||||||
import MkTooltip from './MkTooltip.vue';
|
import MkTooltip from './MkTooltip.vue';
|
||||||
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
||||||
|
@ -30,7 +31,7 @@ import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
||||||
defineProps<{
|
defineProps<{
|
||||||
showing: boolean;
|
showing: boolean;
|
||||||
reaction: string;
|
reaction: string;
|
||||||
users: any[]; // TODO
|
users: Misskey.entities.UserLite[];
|
||||||
count: number;
|
count: number;
|
||||||
targetElement: HTMLElement;
|
targetElement: HTMLElement;
|
||||||
}>();
|
}>();
|
||||||
|
|
|
@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
:width="500"
|
:width="500"
|
||||||
:height="600"
|
:height="600"
|
||||||
@close="onClose"
|
@close="onClose"
|
||||||
@closed="$emit('closed')"
|
@closed="emit('closed')"
|
||||||
>
|
>
|
||||||
<template #header>{{ i18n.ts.signup }}</template>
|
<template #header>{{ i18n.ts.signup }}</template>
|
||||||
|
|
||||||
|
|
|
@ -28,11 +28,38 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts">
|
||||||
import { } from 'vue';
|
export type SuperMenuDef = {
|
||||||
|
title?: string;
|
||||||
|
items: ({
|
||||||
|
type: 'a';
|
||||||
|
href: string;
|
||||||
|
target?: string;
|
||||||
|
icon?: string;
|
||||||
|
text: string;
|
||||||
|
danger?: boolean;
|
||||||
|
active?: boolean;
|
||||||
|
} | {
|
||||||
|
type: 'button';
|
||||||
|
icon?: string;
|
||||||
|
text: string;
|
||||||
|
danger?: boolean;
|
||||||
|
active?: boolean;
|
||||||
|
action: (ev: MouseEvent) => void;
|
||||||
|
} | {
|
||||||
|
type: 'link';
|
||||||
|
to: string;
|
||||||
|
icon?: string;
|
||||||
|
text: string;
|
||||||
|
danger?: boolean;
|
||||||
|
active?: boolean;
|
||||||
|
})[];
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
defineProps<{
|
defineProps<{
|
||||||
def: any[];
|
def: SuperMenuDef[];
|
||||||
grid?: boolean;
|
grid?: boolean;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -43,6 +43,7 @@ const props = withDefaults(defineProps<{
|
||||||
}>(), {
|
}>(), {
|
||||||
withRenotes: true,
|
withRenotes: true,
|
||||||
withReplies: false,
|
withReplies: false,
|
||||||
|
withSensitive: true,
|
||||||
onlyFiles: false,
|
onlyFiles: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
:okButtonDisabled="false"
|
:okButtonDisabled="false"
|
||||||
:canClose="false"
|
:canClose="false"
|
||||||
@close="dialog?.close()"
|
@close="dialog?.close()"
|
||||||
@closed="$emit('closed')"
|
@closed="emit('closed')"
|
||||||
@ok="ok()"
|
@ok="ok()"
|
||||||
>
|
>
|
||||||
<template #header>{{ title || i18n.ts.generateAccessToken }}</template>
|
<template #header>{{ title || i18n.ts.generateAccessToken }}</template>
|
||||||
|
|
|
@ -180,7 +180,7 @@ window.fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${versatileLa
|
||||||
sensitive.value = info.sensitive ?? false;
|
sensitive.value = info.sensitive ?? false;
|
||||||
});
|
});
|
||||||
|
|
||||||
function adjustTweetHeight(message: any) {
|
function adjustTweetHeight(message: MessageEvent) {
|
||||||
if (message.origin !== 'https://platform.twitter.com') return;
|
if (message.origin !== 'https://platform.twitter.com') return;
|
||||||
const embed = message.data?.['twttr.embed'];
|
const embed = message.data?.['twttr.embed'];
|
||||||
if (embed?.method !== 'twttr.private.resize') return;
|
if (embed?.method !== 'twttr.private.resize') return;
|
||||||
|
@ -193,14 +193,16 @@ function openPlayer(): void {
|
||||||
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkYouTubePlayer.vue')), {
|
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkYouTubePlayer.vue')), {
|
||||||
url: requestUrl.href,
|
url: requestUrl.href,
|
||||||
}, {
|
}, {
|
||||||
// TODO
|
closed: () => {
|
||||||
|
dispose();
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
(window as any).addEventListener('message', adjustTweetHeight);
|
window.addEventListener('message', adjustTweetHeight);
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
(window as any).removeEventListener('message', adjustTweetHeight);
|
window.removeEventListener('message', adjustTweetHeight);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
ref="dialog"
|
ref="dialog"
|
||||||
:width="400"
|
:width="400"
|
||||||
@close="dialog?.close()"
|
@close="dialog?.close()"
|
||||||
@closed="$emit('closed')"
|
@closed="emit('closed')"
|
||||||
>
|
>
|
||||||
<template v-if="announcement" #header>:{{ announcement.title }}:</template>
|
<template v-if="announcement" #header>:{{ announcement.title }}:</template>
|
||||||
<template v-else #header>New announcement</template>
|
<template v-else #header>New announcement</template>
|
||||||
|
@ -62,9 +62,16 @@ import MkTextarea from '@/components/MkTextarea.vue';
|
||||||
import MkSwitch from '@/components/MkSwitch.vue';
|
import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
import MkRadios from '@/components/MkRadios.vue';
|
import MkRadios from '@/components/MkRadios.vue';
|
||||||
|
|
||||||
|
type AdminAnnouncementType = Misskey.entities.AdminAnnouncementsCreateRequest & { id: string; }
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
user: Misskey.entities.User,
|
user: Misskey.entities.User,
|
||||||
announcement?: Misskey.entities.Announcement,
|
announcement?: Required<AdminAnnouncementType>,
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'done', v: { deleted?: boolean; updated?: AdminAnnouncementType; created?: AdminAnnouncementType; }): void,
|
||||||
|
(ev: 'closed'): void
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const dialog = ref<InstanceType<typeof MkModalWindow> | null>(null);
|
const dialog = ref<InstanceType<typeof MkModalWindow> | null>(null);
|
||||||
|
@ -74,11 +81,6 @@ const icon = ref(props.announcement ? props.announcement.icon : 'info');
|
||||||
const display = ref(props.announcement ? props.announcement.display : 'dialog');
|
const display = ref(props.announcement ? props.announcement.display : 'dialog');
|
||||||
const needConfirmationToRead = ref(props.announcement ? props.announcement.needConfirmationToRead : false);
|
const needConfirmationToRead = ref(props.announcement ? props.announcement.needConfirmationToRead : false);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void,
|
|
||||||
(ev: 'closed'): void
|
|
||||||
}>();
|
|
||||||
|
|
||||||
async function done() {
|
async function done() {
|
||||||
const params = {
|
const params = {
|
||||||
title: title.value,
|
title: title.value,
|
||||||
|
@ -88,7 +90,7 @@ async function done() {
|
||||||
display: display.value,
|
display: display.value,
|
||||||
needConfirmationToRead: needConfirmationToRead.value,
|
needConfirmationToRead: needConfirmationToRead.value,
|
||||||
userId: props.user.id,
|
userId: props.user.id,
|
||||||
};
|
} satisfies Misskey.entities.AdminAnnouncementsCreateRequest;
|
||||||
|
|
||||||
if (props.announcement) {
|
if (props.announcement) {
|
||||||
await os.apiWithDialog('admin/announcements/update', {
|
await os.apiWithDialog('admin/announcements/update', {
|
||||||
|
|
|
@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
@click="cancel()"
|
@click="cancel()"
|
||||||
@close="cancel()"
|
@close="cancel()"
|
||||||
@ok="ok()"
|
@ok="ok()"
|
||||||
@closed="$emit('closed')"
|
@closed="emit('closed')"
|
||||||
>
|
>
|
||||||
<template #header>{{ i18n.ts.selectUser }}</template>
|
<template #header>{{ i18n.ts.selectUser }}</template>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -16,12 +16,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import * as Misskey from 'misskey-js';
|
||||||
import MkTooltip from './MkTooltip.vue';
|
import MkTooltip from './MkTooltip.vue';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
showing: boolean;
|
showing: boolean;
|
||||||
users: any[]; // TODO
|
users: Misskey.entities.UserLite[];
|
||||||
count: number;
|
count: number;
|
||||||
targetElement: HTMLElement;
|
targetElement: HTMLElement;
|
||||||
}>();
|
}>();
|
||||||
|
|
|
@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ i18n.ts._widgets[widget] }}</option>
|
<option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ i18n.ts._widgets[widget] }}</option>
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<MkButton inline primary data-cy-widget-add @click="addWidget"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
|
<MkButton inline primary data-cy-widget-add @click="addWidget"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
|
||||||
<MkButton inline @click="$emit('exit')">{{ i18n.ts.close }}</MkButton>
|
<MkButton inline @click="emit('exit')">{{ i18n.ts.close }}</MkButton>
|
||||||
</header>
|
</header>
|
||||||
<Sortable
|
<Sortable
|
||||||
:modelValue="props.widgets"
|
:modelValue="props.widgets"
|
||||||
|
|
|
@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
:enterFromClass="defaultStore.state.animation ? $style.transition_window_enterFrom : ''"
|
:enterFromClass="defaultStore.state.animation ? $style.transition_window_enterFrom : ''"
|
||||||
:leaveToClass="defaultStore.state.animation ? $style.transition_window_leaveTo : ''"
|
:leaveToClass="defaultStore.state.animation ? $style.transition_window_leaveTo : ''"
|
||||||
appear
|
appear
|
||||||
@afterLeave="$emit('closed')"
|
@afterLeave="emit('closed')"
|
||||||
>
|
>
|
||||||
<div v-if="showing" ref="rootEl" :class="[$style.root, { [$style.maximized]: maximized }]">
|
<div v-if="showing" ref="rootEl" :class="[$style.root, { [$style.maximized]: maximized }]">
|
||||||
<div :class="$style.body" class="_shadow" @mousedown="onBodyMousedown" @keydown="onKeydown">
|
<div :class="$style.body" class="_shadow" @mousedown="onBodyMousedown" @keydown="onKeydown">
|
||||||
|
@ -60,6 +60,13 @@ import * as os from '@/os.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
|
|
||||||
|
type WindowButton = {
|
||||||
|
title: string;
|
||||||
|
icon: string;
|
||||||
|
onClick: () => void;
|
||||||
|
highlighted?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
const minHeight = 50;
|
const minHeight = 50;
|
||||||
const minWidth = 250;
|
const minWidth = 250;
|
||||||
|
|
||||||
|
@ -87,8 +94,8 @@ const props = withDefaults(defineProps<{
|
||||||
mini?: boolean;
|
mini?: boolean;
|
||||||
front?: boolean;
|
front?: boolean;
|
||||||
contextmenu?: MenuItem[] | null;
|
contextmenu?: MenuItem[] | null;
|
||||||
buttonsLeft?: any[];
|
buttonsLeft?: WindowButton[];
|
||||||
buttonsRight?: any[];
|
buttonsRight?: WindowButton[];
|
||||||
}>(), {
|
}>(), {
|
||||||
initialWidth: 400,
|
initialWidth: 400,
|
||||||
initialHeight: null,
|
initialHeight: null,
|
||||||
|
|
|
@ -18,19 +18,19 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup generic="T extends unknown">
|
||||||
import { ref, watch } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
p: () => Promise<any>;
|
p: () => Promise<T>;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const pending = ref(true);
|
const pending = ref(true);
|
||||||
const resolved = ref(false);
|
const resolved = ref(false);
|
||||||
const rejected = ref(false);
|
const rejected = ref(false);
|
||||||
const result = ref<any>(null);
|
const result = ref<T | null>(null);
|
||||||
|
|
||||||
const process = () => {
|
const process = () => {
|
||||||
if (props.p == null) {
|
if (props.p == null) {
|
||||||
|
|
|
@ -467,8 +467,8 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
|
||||||
}
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// @ts-expect-error 存在しないASTタイプ
|
||||||
console.error('unrecognized ast type:', (token as any).type);
|
console.error('unrecognized ast type:', token.type);
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,8 @@ interface RouteDefWithRedirect extends RouteDefBase {
|
||||||
|
|
||||||
export type RouteDef = RouteDefWithComponent | RouteDefWithRedirect;
|
export type RouteDef = RouteDefWithComponent | RouteDefWithRedirect;
|
||||||
|
|
||||||
|
export type RouterFlag = 'forcePage';
|
||||||
|
|
||||||
type ParsedPath = (string | {
|
type ParsedPath = (string | {
|
||||||
name: string;
|
name: string;
|
||||||
startsWith?: string;
|
startsWith?: string;
|
||||||
|
@ -107,7 +109,7 @@ export interface IRouter extends EventEmitter<RouterEvent> {
|
||||||
current: Resolved;
|
current: Resolved;
|
||||||
currentRef: ShallowRef<Resolved>;
|
currentRef: ShallowRef<Resolved>;
|
||||||
currentRoute: ShallowRef<RouteDef>;
|
currentRoute: ShallowRef<RouteDef>;
|
||||||
navHook: ((path: string, flag?: any) => boolean) | null;
|
navHook: ((path: string, flag?: RouterFlag) => boolean) | null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ルートの初期化(eventListenerの定義後に必ず呼び出すこと)
|
* ルートの初期化(eventListenerの定義後に必ず呼び出すこと)
|
||||||
|
@ -116,11 +118,11 @@ export interface IRouter extends EventEmitter<RouterEvent> {
|
||||||
|
|
||||||
resolve(path: string): Resolved | null;
|
resolve(path: string): Resolved | null;
|
||||||
|
|
||||||
getCurrentPath(): any;
|
getCurrentPath(): string;
|
||||||
|
|
||||||
getCurrentKey(): string;
|
getCurrentKey(): string;
|
||||||
|
|
||||||
push(path: string, flag?: any): void;
|
push(path: string, flag?: RouterFlag): void;
|
||||||
|
|
||||||
replace(path: string, key?: string | null): void;
|
replace(path: string, key?: string | null): void;
|
||||||
|
|
||||||
|
@ -197,7 +199,7 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
|
||||||
private currentKey = Date.now().toString();
|
private currentKey = Date.now().toString();
|
||||||
private redirectCount = 0;
|
private redirectCount = 0;
|
||||||
|
|
||||||
public navHook: ((path: string, flag?: any) => boolean) | null = null;
|
public navHook: ((path: string, flag?: RouterFlag) => boolean) | null = null;
|
||||||
|
|
||||||
constructor(routes: Router['routes'], currentPath: Router['currentPath'], isLoggedIn: boolean, notFoundPageComponent: Component) {
|
constructor(routes: Router['routes'], currentPath: Router['currentPath'], isLoggedIn: boolean, notFoundPageComponent: Component) {
|
||||||
super();
|
super();
|
||||||
|
@ -404,7 +406,7 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
|
||||||
return this.currentKey;
|
return this.currentKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
public push(path: string, flag?: any) {
|
public push(path: string, flag?: RouterFlag) {
|
||||||
const beforePath = this.currentPath;
|
const beforePath = this.currentPath;
|
||||||
if (path === beforePath) {
|
if (path === beforePath) {
|
||||||
this.emit('same');
|
this.emit('same');
|
||||||
|
|
|
@ -28,12 +28,13 @@ import { pleaseLogin } from '@/scripts/please-login.js';
|
||||||
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
|
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
|
||||||
import { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js';
|
import { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js';
|
||||||
import { focusParent } from '@/scripts/focus.js';
|
import { focusParent } from '@/scripts/focus.js';
|
||||||
|
import type { PostFormProps } from '@/types/post-form.js';
|
||||||
|
|
||||||
export const openingWindowsCount = ref(0);
|
export const openingWindowsCount = ref(0);
|
||||||
|
|
||||||
export const apiWithDialog = (<E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req']>(
|
export const apiWithDialog = (<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req']>(
|
||||||
endpoint: E,
|
endpoint: E,
|
||||||
data: P = {} as any,
|
data: P,
|
||||||
token?: string | null | undefined,
|
token?: string | null | undefined,
|
||||||
customErrors?: Record<string, { title?: string; text: string; }>,
|
customErrors?: Record<string, { title?: string; text: string; }>,
|
||||||
) => {
|
) => {
|
||||||
|
@ -94,7 +95,7 @@ export const apiWithDialog = (<E extends keyof Misskey.Endpoints = keyof Misskey
|
||||||
|
|
||||||
export function promiseDialog<T extends Promise<any>>(
|
export function promiseDialog<T extends Promise<any>>(
|
||||||
promise: T,
|
promise: T,
|
||||||
onSuccess?: ((res: any) => void) | null,
|
onSuccess?: ((res: Awaited<T>) => void) | null,
|
||||||
onFailure?: ((err: Misskey.api.APIError) => void) | null,
|
onFailure?: ((err: Misskey.api.APIError) => void) | null,
|
||||||
text?: string,
|
text?: string,
|
||||||
): T {
|
): T {
|
||||||
|
@ -136,12 +137,12 @@ export function promiseDialog<T extends Promise<any>>(
|
||||||
}
|
}
|
||||||
|
|
||||||
let popupIdCount = 0;
|
let popupIdCount = 0;
|
||||||
export const popups = ref([]) as Ref<{
|
export const popups = ref<{
|
||||||
id: number;
|
id: number;
|
||||||
component: Component;
|
component: Component;
|
||||||
props: Record<string, any>;
|
props: Record<string, any>;
|
||||||
events: Record<string, any>;
|
events: Record<string, any>;
|
||||||
}[]>;
|
}[]>([]);
|
||||||
|
|
||||||
const zIndexes = {
|
const zIndexes = {
|
||||||
veryLow: 500000,
|
veryLow: 500000,
|
||||||
|
@ -458,7 +459,7 @@ type SelectItem<C> = {
|
||||||
};
|
};
|
||||||
|
|
||||||
// default が指定されていたら result は null になり得ないことを保証する overload function
|
// default が指定されていたら result は null になり得ないことを保証する overload function
|
||||||
export function select<C = any>(props: {
|
export function select<C = unknown>(props: {
|
||||||
title?: string;
|
title?: string;
|
||||||
text?: string;
|
text?: string;
|
||||||
default: string;
|
default: string;
|
||||||
|
@ -471,7 +472,7 @@ export function select<C = any>(props: {
|
||||||
} | {
|
} | {
|
||||||
canceled: false; result: C;
|
canceled: false; result: C;
|
||||||
}>;
|
}>;
|
||||||
export function select<C = any>(props: {
|
export function select<C = unknown>(props: {
|
||||||
title?: string;
|
title?: string;
|
||||||
text?: string;
|
text?: string;
|
||||||
default?: string | null;
|
default?: string | null;
|
||||||
|
@ -484,7 +485,7 @@ export function select<C = any>(props: {
|
||||||
} | {
|
} | {
|
||||||
canceled: false; result: C | null;
|
canceled: false; result: C | null;
|
||||||
}>;
|
}>;
|
||||||
export function select<C = any>(props: {
|
export function select<C = unknown>(props: {
|
||||||
title?: string;
|
title?: string;
|
||||||
text?: string;
|
text?: string;
|
||||||
default?: string | null;
|
default?: string | null;
|
||||||
|
@ -687,13 +688,13 @@ export function contextMenu(items: MenuItem[], ev: MouseEvent): Promise<void> {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function post(props: Record<string, any> = {}): Promise<void> {
|
export function post(props: PostFormProps = {}): Promise<void> {
|
||||||
pleaseLogin({
|
pleaseLogin({
|
||||||
openOnRemote: (props.initialText || props.initialNote ? {
|
openOnRemote: (props.initialText || props.initialNote ? {
|
||||||
type: 'share',
|
type: 'share',
|
||||||
params: {
|
params: {
|
||||||
text: props.initialText ?? props.initialNote.text,
|
text: props.initialText ?? props.initialNote?.text ?? '',
|
||||||
visibility: props.initialVisibility ?? props.initialNote?.visibility,
|
visibility: props.initialVisibility ?? props.initialNote?.visibility ?? 'public',
|
||||||
localOnly: (props.initialLocalOnly || props.initialNote?.localOnly) ? '1' : '0',
|
localOnly: (props.initialLocalOnly || props.initialNote?.localOnly) ? '1' : '0',
|
||||||
},
|
},
|
||||||
} : undefined),
|
} : undefined),
|
||||||
|
|
|
@ -99,19 +99,19 @@ async function addUser() {
|
||||||
const { canceled: canceled1, result: username } = await os.inputText({
|
const { canceled: canceled1, result: username } = await os.inputText({
|
||||||
title: i18n.ts.username,
|
title: i18n.ts.username,
|
||||||
});
|
});
|
||||||
if (canceled1) return;
|
if (canceled1 || username == null) return;
|
||||||
|
|
||||||
const { canceled: canceled2, result: password } = await os.inputText({
|
const { canceled: canceled2, result: password } = await os.inputText({
|
||||||
title: i18n.ts.password,
|
title: i18n.ts.password,
|
||||||
type: 'password',
|
type: 'password',
|
||||||
});
|
});
|
||||||
if (canceled2) return;
|
if (canceled2 || password == null) return;
|
||||||
|
|
||||||
os.apiWithDialog('admin/accounts/create', {
|
os.apiWithDialog('admin/accounts/create', {
|
||||||
username: username,
|
username: username,
|
||||||
password: password,
|
password: password,
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
paginationComponent.value.reload();
|
paginationComponent.value?.reload();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -103,7 +103,7 @@ const headerActions = computed(() => []);
|
||||||
const headerTabs = computed(() => []);
|
const headerTabs = computed(() => []);
|
||||||
|
|
||||||
definePageMetadata(() => ({
|
definePageMetadata(() => ({
|
||||||
title: announcement.value ? `${i18n.ts.announcements}: ${announcement.value.title}` : i18n.ts.announcements,
|
title: announcement.value ? announcement.value.title : i18n.ts.announcements,
|
||||||
icon: 'ti ti-speakerphone',
|
icon: 'ti ti-speakerphone',
|
||||||
}));
|
}));
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -62,7 +62,7 @@ function accepted() {
|
||||||
state.value = 'accepted';
|
state.value = 'accepted';
|
||||||
if (session.value && session.value.app.callbackUrl) {
|
if (session.value && session.value.app.callbackUrl) {
|
||||||
const url = new URL(session.value.app.callbackUrl);
|
const url = new URL(session.value.app.callbackUrl);
|
||||||
if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:'].includes(url.protocol)) throw new Error('invalid url');
|
if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:', 'vbscript:'].includes(url.protocol)) throw new Error('invalid url');
|
||||||
location.href = `${session.value.app.callbackUrl}?token=${session.value.token}`;
|
location.href = `${session.value.app.callbackUrl}?token=${session.value.token}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -116,7 +116,7 @@ const selectAll = () => {
|
||||||
if (selectedEmojis.value.length > 0) {
|
if (selectedEmojis.value.length > 0) {
|
||||||
selectedEmojis.value = [];
|
selectedEmojis.value = [];
|
||||||
} else {
|
} else {
|
||||||
selectedEmojis.value = Array.from(emojisPaginationComponent.value.items.values(), item => item.id);
|
selectedEmojis.value = Array.from(emojisPaginationComponent.value?.items.values(), item => item.id);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -133,7 +133,7 @@ const add = async (ev: MouseEvent) => {
|
||||||
}, {
|
}, {
|
||||||
done: result => {
|
done: result => {
|
||||||
if (result.created) {
|
if (result.created) {
|
||||||
emojisPaginationComponent.value.prepend(result.created);
|
emojisPaginationComponent.value?.prepend(result.created);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
closed: () => dispose(),
|
closed: () => dispose(),
|
||||||
|
@ -146,12 +146,12 @@ const edit = (emoji) => {
|
||||||
}, {
|
}, {
|
||||||
done: result => {
|
done: result => {
|
||||||
if (result.updated) {
|
if (result.updated) {
|
||||||
emojisPaginationComponent.value.updateItem(result.updated.id, (oldEmoji: any) => ({
|
emojisPaginationComponent.value?.updateItem(result.updated.id, (oldEmoji) => ({
|
||||||
...oldEmoji,
|
...oldEmoji,
|
||||||
...result.updated,
|
...result.updated,
|
||||||
}));
|
}));
|
||||||
} else if (result.deleted) {
|
} else if (result.deleted) {
|
||||||
emojisPaginationComponent.value.removeItem(emoji.id);
|
emojisPaginationComponent.value?.removeItem(emoji.id);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
closed: () => dispose(),
|
closed: () => dispose(),
|
||||||
|
@ -226,7 +226,7 @@ const setCategoryBulk = async () => {
|
||||||
ids: selectedEmojis.value,
|
ids: selectedEmojis.value,
|
||||||
category: result,
|
category: result,
|
||||||
});
|
});
|
||||||
emojisPaginationComponent.value.reload();
|
emojisPaginationComponent.value?.reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
const setLicenseBulk = async () => {
|
const setLicenseBulk = async () => {
|
||||||
|
@ -238,43 +238,43 @@ const setLicenseBulk = async () => {
|
||||||
ids: selectedEmojis.value,
|
ids: selectedEmojis.value,
|
||||||
license: result,
|
license: result,
|
||||||
});
|
});
|
||||||
emojisPaginationComponent.value.reload();
|
emojisPaginationComponent.value?.reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
const addTagBulk = async () => {
|
const addTagBulk = async () => {
|
||||||
const { canceled, result } = await os.inputText({
|
const { canceled, result } = await os.inputText({
|
||||||
title: 'Tag',
|
title: 'Tag',
|
||||||
});
|
});
|
||||||
if (canceled) return;
|
if (canceled || result == null) return;
|
||||||
await os.apiWithDialog('admin/emoji/add-aliases-bulk', {
|
await os.apiWithDialog('admin/emoji/add-aliases-bulk', {
|
||||||
ids: selectedEmojis.value,
|
ids: selectedEmojis.value,
|
||||||
aliases: result.split(' '),
|
aliases: result.split(' '),
|
||||||
});
|
});
|
||||||
emojisPaginationComponent.value.reload();
|
emojisPaginationComponent.value?.reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeTagBulk = async () => {
|
const removeTagBulk = async () => {
|
||||||
const { canceled, result } = await os.inputText({
|
const { canceled, result } = await os.inputText({
|
||||||
title: 'Tag',
|
title: 'Tag',
|
||||||
});
|
});
|
||||||
if (canceled) return;
|
if (canceled || result == null) return;
|
||||||
await os.apiWithDialog('admin/emoji/remove-aliases-bulk', {
|
await os.apiWithDialog('admin/emoji/remove-aliases-bulk', {
|
||||||
ids: selectedEmojis.value,
|
ids: selectedEmojis.value,
|
||||||
aliases: result.split(' '),
|
aliases: result.split(' '),
|
||||||
});
|
});
|
||||||
emojisPaginationComponent.value.reload();
|
emojisPaginationComponent.value?.reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
const setTagBulk = async () => {
|
const setTagBulk = async () => {
|
||||||
const { canceled, result } = await os.inputText({
|
const { canceled, result } = await os.inputText({
|
||||||
title: 'Tag',
|
title: 'Tag',
|
||||||
});
|
});
|
||||||
if (canceled) return;
|
if (canceled || result == null) return;
|
||||||
await os.apiWithDialog('admin/emoji/set-aliases-bulk', {
|
await os.apiWithDialog('admin/emoji/set-aliases-bulk', {
|
||||||
ids: selectedEmojis.value,
|
ids: selectedEmojis.value,
|
||||||
aliases: result.split(' '),
|
aliases: result.split(' '),
|
||||||
});
|
});
|
||||||
emojisPaginationComponent.value.reload();
|
emojisPaginationComponent.value?.reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
const delBulk = async () => {
|
const delBulk = async () => {
|
||||||
|
@ -286,7 +286,7 @@ const delBulk = async () => {
|
||||||
await os.apiWithDialog('admin/emoji/delete-bulk', {
|
await os.apiWithDialog('admin/emoji/delete-bulk', {
|
||||||
ids: selectedEmojis.value,
|
ids: selectedEmojis.value,
|
||||||
});
|
});
|
||||||
emojisPaginationComponent.value.reload();
|
emojisPaginationComponent.value?.reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
const headerActions = computed(() => [{
|
const headerActions = computed(() => [{
|
||||||
|
|
|
@ -9,8 +9,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
:initialWidth="400"
|
:initialWidth="400"
|
||||||
:initialHeight="500"
|
:initialHeight="500"
|
||||||
:canResize="true"
|
:canResize="true"
|
||||||
@close="windowEl.close()"
|
@close="windowEl?.close()"
|
||||||
@closed="$emit('closed')"
|
@closed="emit('closed')"
|
||||||
>
|
>
|
||||||
<template v-if="emoji" #header>:{{ emoji.name }}:</template>
|
<template v-if="emoji" #header>:{{ emoji.name }}:</template>
|
||||||
<template v-else #header>New emoji</template>
|
<template v-else #header>New emoji</template>
|
||||||
|
@ -95,14 +95,19 @@ import { selectFile } from '@/scripts/select-file.js';
|
||||||
import MkRolePreview from '@/components/MkRolePreview.vue';
|
import MkRolePreview from '@/components/MkRolePreview.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
emoji?: any,
|
emoji?: Misskey.entities.EmojiDetailed,
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'done', v: { deleted?: boolean; updated?: Misskey.entities.AdminEmojiUpdateRequest; created?: Misskey.entities.AdminEmojiUpdateRequest }): void,
|
||||||
|
(ev: 'closed'): void
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const windowEl = ref<InstanceType<typeof MkWindow> | null>(null);
|
const windowEl = ref<InstanceType<typeof MkWindow> | null>(null);
|
||||||
const name = ref<string>(props.emoji ? props.emoji.name : '');
|
const name = ref<string>(props.emoji ? props.emoji.name : '');
|
||||||
const category = ref<string>(props.emoji ? props.emoji.category : '');
|
const category = ref<string>(props.emoji?.category ? props.emoji.category : '');
|
||||||
const aliases = ref<string>(props.emoji ? props.emoji.aliases.join(' ') : '');
|
const aliases = ref<string>(props.emoji ? props.emoji.aliases.join(' ') : '');
|
||||||
const license = ref<string>(props.emoji ? (props.emoji.license ?? '') : '');
|
const license = ref<string>(props.emoji?.license ? props.emoji.license : '');
|
||||||
const isSensitive = ref(props.emoji ? props.emoji.isSensitive : false);
|
const isSensitive = ref(props.emoji ? props.emoji.isSensitive : false);
|
||||||
const localOnly = ref(props.emoji ? props.emoji.localOnly : false);
|
const localOnly = ref(props.emoji ? props.emoji.localOnly : false);
|
||||||
const roleIdsThatCanBeUsedThisEmojiAsReaction = ref(props.emoji ? props.emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : []);
|
const roleIdsThatCanBeUsedThisEmojiAsReaction = ref(props.emoji ? props.emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : []);
|
||||||
|
@ -115,12 +120,7 @@ watch(roleIdsThatCanBeUsedThisEmojiAsReaction, async () => {
|
||||||
|
|
||||||
const imgUrl = computed(() => file.value ? file.value.url : props.emoji ? `/emoji/${props.emoji.name}.webp` : null);
|
const imgUrl = computed(() => file.value ? file.value.url : props.emoji ? `/emoji/${props.emoji.name}.webp` : null);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
async function changeImage(ev: Event) {
|
||||||
(ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void,
|
|
||||||
(ev: 'closed'): void
|
|
||||||
}>();
|
|
||||||
|
|
||||||
async function changeImage(ev) {
|
|
||||||
file.value = await selectFile(ev.currentTarget ?? ev.target, null);
|
file.value = await selectFile(ev.currentTarget ?? ev.target, null);
|
||||||
const candidate = file.value.name.replace(/\.(.+)$/, '');
|
const candidate = file.value.name.replace(/\.(.+)$/, '');
|
||||||
if (candidate.match(/^[a-z0-9_]+$/)) {
|
if (candidate.match(/^[a-z0-9_]+$/)) {
|
||||||
|
@ -140,7 +140,7 @@ async function addRole() {
|
||||||
rolesThatCanBeUsedThisEmojiAsReaction.value.push(role);
|
rolesThatCanBeUsedThisEmojiAsReaction.value.push(role);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeRole(role, ev) {
|
async function removeRole(role: Misskey.entities.RoleLite, ev: Event) {
|
||||||
rolesThatCanBeUsedThisEmojiAsReaction.value = rolesThatCanBeUsedThisEmojiAsReaction.value.filter(x => x.id !== role.id);
|
rolesThatCanBeUsedThisEmojiAsReaction.value = rolesThatCanBeUsedThisEmojiAsReaction.value.filter(x => x.id !== role.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,7 +172,7 @@ async function done() {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
windowEl.value.close();
|
windowEl.value?.close();
|
||||||
} else {
|
} else {
|
||||||
const created = await os.apiWithDialog('admin/emoji/add', params);
|
const created = await os.apiWithDialog('admin/emoji/add', params);
|
||||||
|
|
||||||
|
@ -180,11 +180,12 @@ async function done() {
|
||||||
created: created,
|
created: created,
|
||||||
});
|
});
|
||||||
|
|
||||||
windowEl.value.close();
|
windowEl.value?.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function del() {
|
async function del() {
|
||||||
|
if (!props.emoji) return;
|
||||||
const { canceled } = await os.confirm({
|
const { canceled } = await os.confirm({
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
text: i18n.tsx.removeAreYouSure({ x: name.value }),
|
text: i18n.tsx.removeAreYouSure({ x: name.value }),
|
||||||
|
@ -197,7 +198,7 @@ async function del() {
|
||||||
emit('done', {
|
emit('done', {
|
||||||
deleted: true,
|
deleted: true,
|
||||||
});
|
});
|
||||||
windowEl.value.close();
|
windowEl.value?.close();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -55,13 +55,13 @@ const pagination = {
|
||||||
|
|
||||||
function accept(user) {
|
function accept(user) {
|
||||||
misskeyApi('following/requests/accept', { userId: user.id }).then(() => {
|
misskeyApi('following/requests/accept', { userId: user.id }).then(() => {
|
||||||
paginationComponent.value.reload();
|
paginationComponent.value?.reload();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function reject(user) {
|
function reject(user) {
|
||||||
misskeyApi('following/requests/reject', { userId: user.id }).then(() => {
|
misskeyApi('following/requests/reject', { userId: user.id }).then(() => {
|
||||||
paginationComponent.value.reload();
|
paginationComponent.value?.reload();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -40,7 +40,7 @@ function fetch() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let promise: Promise<any>;
|
let promise: Promise<unknown>;
|
||||||
|
|
||||||
if (uri.startsWith('https://')) {
|
if (uri.startsWith('https://')) {
|
||||||
promise = misskeyApi('ap/show', {
|
promise = misskeyApi('ap/show', {
|
||||||
|
|
|
@ -65,7 +65,7 @@ async function onAccept(token: string) {
|
||||||
|
|
||||||
if (props.callback && props.callback !== '') {
|
if (props.callback && props.callback !== '') {
|
||||||
const cbUrl = new URL(props.callback);
|
const cbUrl = new URL(props.callback);
|
||||||
if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:'].includes(cbUrl.protocol)) throw new Error('invalid url');
|
if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:', 'vbscript:'].includes(cbUrl.protocol)) throw new Error('invalid url');
|
||||||
cbUrl.searchParams.set('session', props.session);
|
cbUrl.searchParams.set('session', props.session);
|
||||||
location.href = cbUrl.toString();
|
location.href = cbUrl.toString();
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -77,15 +77,15 @@ async function create() {
|
||||||
|
|
||||||
clipsCache.delete();
|
clipsCache.delete();
|
||||||
|
|
||||||
pagingComponent.value.reload();
|
pagingComponent.value?.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onClipCreated() {
|
function onClipCreated() {
|
||||||
pagingComponent.value.reload();
|
pagingComponent.value?.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onClipDeleted() {
|
function onClipDeleted() {
|
||||||
pagingComponent.value.reload();
|
pagingComponent.value?.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
const headerActions = computed(() => []);
|
const headerActions = computed(() => []);
|
||||||
|
|
|
@ -110,7 +110,7 @@ function addUser() {
|
||||||
listId: list.value.id,
|
listId: list.value.id,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
paginationEl.value.reload();
|
paginationEl.value?.reload();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -126,7 +126,7 @@ async function removeUser(item, ev) {
|
||||||
listId: list.value.id,
|
listId: list.value.id,
|
||||||
userId: item.userId,
|
userId: item.userId,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
paginationEl.value.removeItem(item.id);
|
paginationEl.value?.removeItem(item.id);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}], ev.currentTarget ?? ev.target);
|
}], ev.currentTarget ?? ev.target);
|
||||||
|
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- eslint-disable vue/no-mutating-props -->
|
<!-- eslint-disable vue/no-mutating-props -->
|
||||||
<XContainer :draggable="true" @remove="() => $emit('remove')">
|
<XContainer :draggable="true" @remove="() => emit('remove')">
|
||||||
<template #header><i class="ti ti-photo"></i> {{ i18n.ts._pages.blocks.image }}</template>
|
<template #header><i class="ti ti-photo"></i> {{ i18n.ts._pages.blocks.image }}</template>
|
||||||
<template #func>
|
<template #func>
|
||||||
<button @click="choose()">
|
<button @click="choose()">
|
||||||
|
@ -30,11 +30,12 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: any
|
modelValue: Misskey.entities.PageBlock & { type: 'image' };
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'update:modelValue', value: any): void;
|
(ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'image' }): void;
|
||||||
|
(ev: 'remove'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const file = ref<Misskey.entities.DriveFile | null>(null);
|
const file = ref<Misskey.entities.DriveFile | null>(null);
|
||||||
|
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- eslint-disable vue/no-mutating-props -->
|
<!-- eslint-disable vue/no-mutating-props -->
|
||||||
<XContainer :draggable="true" @remove="() => $emit('remove')">
|
<XContainer :draggable="true" @remove="() => emit('remove')">
|
||||||
<template #header><i class="ti ti-note"></i> {{ i18n.ts._pages.blocks.note }}</template>
|
<template #header><i class="ti ti-note"></i> {{ i18n.ts._pages.blocks.note }}</template>
|
||||||
|
|
||||||
<section style="padding: 16px;" class="_gaps_s">
|
<section style="padding: 16px;" class="_gaps_s">
|
||||||
|
@ -34,19 +34,24 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: any
|
modelValue: Misskey.entities.PageBlock & { type: 'note' };
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'update:modelValue', value: any): void;
|
(ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'note' }): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const id = ref<any>(props.modelValue.note);
|
const id = ref(props.modelValue.note);
|
||||||
const note = ref<Misskey.entities.Note | null>(null);
|
const note = ref<Misskey.entities.Note | null>(null);
|
||||||
|
|
||||||
watch(id, async () => {
|
watch(id, async () => {
|
||||||
if (id.value && (id.value.startsWith('http://') || id.value.startsWith('https://'))) {
|
if (id.value && (id.value.startsWith('http://') || id.value.startsWith('https://'))) {
|
||||||
id.value = (id.value.endsWith('/') ? id.value.slice(0, -1) : id.value).split('/').pop();
|
id.value = (id.value.endsWith('/') ? id.value.slice(0, -1) : id.value).split('/').pop() ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!id.value) {
|
||||||
|
note.value = null;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
emit('update:modelValue', {
|
emit('update:modelValue', {
|
||||||
|
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- eslint-disable vue/no-mutating-props -->
|
<!-- eslint-disable vue/no-mutating-props -->
|
||||||
<XContainer :draggable="true" @remove="() => $emit('remove')">
|
<XContainer :draggable="true" @remove="() => emit('remove')">
|
||||||
<template #header><i class="ti ti-note"></i> {{ props.modelValue.title }}</template>
|
<template #header><i class="ti ti-note"></i> {{ props.modelValue.title }}</template>
|
||||||
<template #func>
|
<template #func>
|
||||||
<button class="_button" @click="rename()">
|
<button class="_button" @click="rename()">
|
||||||
|
@ -21,8 +21,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
/* eslint-disable vue/no-mutating-props */
|
|
||||||
import { defineAsyncComponent, inject, onMounted, watch, ref } from 'vue';
|
import { defineAsyncComponent, inject, onMounted, watch, ref } from 'vue';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import XContainer from '../page-editor.container.vue';
|
import XContainer from '../page-editor.container.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
|
@ -33,14 +34,13 @@ import { getPageBlockList } from '@/pages/page-editor/common.js';
|
||||||
|
|
||||||
const XBlocks = defineAsyncComponent(() => import('../page-editor.blocks.vue'));
|
const XBlocks = defineAsyncComponent(() => import('../page-editor.blocks.vue'));
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: any,
|
modelValue: Misskey.entities.PageBlock & { type: 'section'; },
|
||||||
}>(), {
|
}>();
|
||||||
modelValue: {},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'update:modelValue', value: any): void;
|
(ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'section' }): void;
|
||||||
|
(ev: 'remove'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const children = ref(deepClone(props.modelValue.children ?? []));
|
const children = ref(deepClone(props.modelValue.children ?? []));
|
||||||
|
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- eslint-disable vue/no-mutating-props -->
|
<!-- eslint-disable vue/no-mutating-props -->
|
||||||
<XContainer :draggable="true" @remove="() => $emit('remove')">
|
<XContainer :draggable="true" @remove="() => emit('remove')">
|
||||||
<template #header><i class="ti ti-align-left"></i> {{ i18n.ts._pages.blocks.text }}</template>
|
<template #header><i class="ti ti-align-left"></i> {{ i18n.ts._pages.blocks.text }}</template>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
|
@ -15,18 +15,19 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
/* eslint-disable vue/no-mutating-props */
|
|
||||||
import { watch, ref, shallowRef, onMounted, onUnmounted } from 'vue';
|
import { watch, ref, shallowRef, onMounted, onUnmounted } from 'vue';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
import XContainer from '../page-editor.container.vue';
|
import XContainer from '../page-editor.container.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { Autocomplete } from '@/scripts/autocomplete.js';
|
import { Autocomplete } from '@/scripts/autocomplete.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: any
|
modelValue: Misskey.entities.PageBlock & { type: 'text' }
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'update:modelValue', value: any): void;
|
(ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'text' }): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let autocomplete: Autocomplete;
|
let autocomplete: Autocomplete;
|
||||||
|
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Sortable :modelValue="modelValue" tag="div" itemKey="id" handle=".drag-handle" :group="{ name: 'blocks' }" :animation="150" :swapThreshold="0.5" @update:modelValue="v => $emit('update:modelValue', v)">
|
<Sortable :modelValue="modelValue" tag="div" itemKey="id" handle=".drag-handle" :group="{ name: 'blocks' }" :animation="150" :swapThreshold="0.5" @update:modelValue="v => emit('update:modelValue', v)">
|
||||||
<template #item="{element}">
|
<template #item="{element}">
|
||||||
<div :class="$style.item">
|
<div :class="$style.item">
|
||||||
<!-- divが無いとエラーになる https://github.com/SortableJS/vue.draggable.next/issues/189 -->
|
<!-- divが無いとエラーになる https://github.com/SortableJS/vue.draggable.next/issues/189 -->
|
||||||
|
|
|
@ -52,7 +52,7 @@ const props = defineProps<{
|
||||||
|
|
||||||
const scope = computed(() => props.path ? props.path.split('/') : []);
|
const scope = computed(() => props.path ? props.path.split('/') : []);
|
||||||
|
|
||||||
const keys = ref<any>(null);
|
const keys = ref<[string, string][]>([]);
|
||||||
|
|
||||||
function fetchKeys() {
|
function fetchKeys() {
|
||||||
misskeyApi('i/registry/keys-with-type', {
|
misskeyApi('i/registry/keys-with-type', {
|
||||||
|
|
|
@ -132,7 +132,7 @@ const mapCategories = Array.from(new Set(Object.values(Reversi.maps).map(x => x.
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
game: Misskey.entities.ReversiGameDetailed;
|
game: Misskey.entities.ReversiGameDetailed;
|
||||||
connection: Misskey.ChannelConnection;
|
connection: Misskey.ChannelConnection<Misskey.Channels['reversiGame']>;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const shareWhenStart = defineModel<boolean>('shareWhenStart', { default: false });
|
const shareWhenStart = defineModel<boolean>('shareWhenStart', { default: false });
|
||||||
|
@ -217,14 +217,14 @@ function onChangeReadyStates(states) {
|
||||||
game.value.user2Ready = states.user2;
|
game.value.user2Ready = states.user2;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSettings(key: keyof Misskey.entities.ReversiGameDetailed) {
|
function updateSettings(key: typeof Misskey.reversiUpdateKeys[number]) {
|
||||||
props.connection.send('updateSettings', {
|
props.connection.send('updateSettings', {
|
||||||
key: key,
|
key: key,
|
||||||
value: game.value[key],
|
value: game.value[key],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function onUpdateSettings({ userId, key, value }: { userId: string; key: keyof Misskey.entities.ReversiGameDetailed; value: any; }) {
|
function onUpdateSettings<K extends typeof Misskey.reversiUpdateKeys[number]>({ userId, key, value }: { userId: string; key: K; value: Misskey.entities.ReversiGameDetailed[K]; }) {
|
||||||
if (userId === $i.id) return;
|
if (userId === $i.id) return;
|
||||||
if (game.value[key] === value) return;
|
if (game.value[key] === value) return;
|
||||||
game.value[key] = value;
|
game.value[key] = value;
|
||||||
|
|
|
@ -76,7 +76,11 @@ import { claimAchievement } from '@/scripts/achievements.js';
|
||||||
const parser = new Parser();
|
const parser = new Parser();
|
||||||
let aiscript: Interpreter;
|
let aiscript: Interpreter;
|
||||||
const code = ref('');
|
const code = ref('');
|
||||||
const logs = ref<any[]>([]);
|
const logs = ref<{
|
||||||
|
id: number;
|
||||||
|
text: string;
|
||||||
|
print: boolean;
|
||||||
|
}[]>([]);
|
||||||
const root = ref<AsUiRoot>();
|
const root = ref<AsUiRoot>();
|
||||||
const components = ref<Ref<AsUiComponent>[]>([]);
|
const components = ref<Ref<AsUiComponent>[]>([]);
|
||||||
const uiKey = ref(0);
|
const uiKey = ref(0);
|
||||||
|
|
|
@ -138,12 +138,13 @@ const token = ref<string | number | null>(null);
|
||||||
const backupCodes = ref<string[]>();
|
const backupCodes = ref<string[]>();
|
||||||
|
|
||||||
function cancel() {
|
function cancel() {
|
||||||
dialog.value.close();
|
dialog.value?.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function tokenDone() {
|
async function tokenDone() {
|
||||||
|
if (token.value == null) return;
|
||||||
const res = await os.apiWithDialog('i/2fa/done', {
|
const res = await os.apiWithDialog('i/2fa/done', {
|
||||||
token: token.value.toString(),
|
token: typeof token.value === 'string' ? token.value : token.value.toString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
backupCodes.value = res.backupCodes;
|
backupCodes.value = res.backupCodes;
|
||||||
|
@ -166,7 +167,7 @@ function downloadBackupCodes() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function allDone() {
|
function allDone() {
|
||||||
dialog.value.close();
|
dialog.value?.close();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -90,7 +90,7 @@ function createAccount() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function switchAccount(account: any) {
|
async function switchAccount(account: Misskey.entities.UserDetailed) {
|
||||||
const fetchedAccounts = await getAccounts();
|
const fetchedAccounts = await getAccounts();
|
||||||
const token = fetchedAccounts.find(x => x.id === account.id)!.token;
|
const token = fetchedAccounts.find(x => x.id === account.id)!.token;
|
||||||
switchAccountWithToken(token);
|
switchAccountWithToken(token);
|
||||||
|
|
|
@ -55,6 +55,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
import FormPagination from '@/components/MkPagination.vue';
|
import FormPagination from '@/components/MkPagination.vue';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
@ -77,7 +78,7 @@ const pagination = {
|
||||||
|
|
||||||
function revoke(token) {
|
function revoke(token) {
|
||||||
misskeyApi('i/revoke-token', { tokenId: token.id }).then(() => {
|
misskeyApi('i/revoke-token', { tokenId: token.id }).then(() => {
|
||||||
list.value.reload();
|
list.value?.reload();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -110,7 +110,7 @@ const decorationsForPreview = computed(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
function cancel() {
|
function cancel() {
|
||||||
dialog.value.close();
|
dialog.value?.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function update() {
|
async function update() {
|
||||||
|
@ -120,7 +120,7 @@ async function update() {
|
||||||
offsetX: offsetX.value,
|
offsetX: offsetX.value,
|
||||||
offsetY: offsetY.value,
|
offsetY: offsetY.value,
|
||||||
});
|
});
|
||||||
dialog.value.close();
|
dialog.value?.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function attach() {
|
async function attach() {
|
||||||
|
@ -130,12 +130,12 @@ async function attach() {
|
||||||
offsetX: offsetX.value,
|
offsetX: offsetX.value,
|
||||||
offsetY: offsetY.value,
|
offsetY: offsetY.value,
|
||||||
});
|
});
|
||||||
dialog.value.close();
|
dialog.value?.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function detach() {
|
async function detach() {
|
||||||
emit('detach');
|
emit('detach');
|
||||||
dialog.value.close();
|
dialog.value?.close();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div :class="$style.tl">
|
<div :class="$style.tl">
|
||||||
<MkTimeline
|
<MkTimeline
|
||||||
ref="tlComponent"
|
ref="tlComponent"
|
||||||
:key="src + withRenotes + withReplies + onlyFiles"
|
:key="src + withRenotes + withReplies + onlyFiles + withSensitive"
|
||||||
:src="src.split(':')[0]"
|
:src="src.split(':')[0]"
|
||||||
:list="src.split(':')[1]"
|
:list="src.split(':')[1]"
|
||||||
:withRenotes="withRenotes"
|
:withRenotes="withRenotes"
|
||||||
|
|
|
@ -257,7 +257,7 @@ function parallaxLoop() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function parallax() {
|
function parallax() {
|
||||||
const banner = bannerEl.value as any;
|
const banner = bannerEl.value;
|
||||||
if (banner == null) return;
|
if (banner == null) return;
|
||||||
|
|
||||||
const top = getScrollPosition(rootEl.value);
|
const top = getScrollPosition(rootEl.value);
|
||||||
|
|
|
@ -241,9 +241,13 @@ export class Storage<T extends StateDef> {
|
||||||
* 特定のキーの、簡易的なgetter/setterを作ります
|
* 特定のキーの、簡易的なgetter/setterを作ります
|
||||||
* 主にvue上で設定コントロールのmodelとして使う用
|
* 主にvue上で設定コントロールのmodelとして使う用
|
||||||
*/
|
*/
|
||||||
public makeGetterSetter<K extends keyof T>(key: K, getter?: (v: T[K]) => unknown, setter?: (v: unknown) => T[K]): {
|
public makeGetterSetter<K extends keyof T, R = T[K]['default']>(
|
||||||
get: () => T[K]['default'];
|
key: K,
|
||||||
set: (value: T[K]['default']) => void;
|
getter?: (v: T[K]['default']) => R,
|
||||||
|
setter?: (v: R) => T[K]['default'],
|
||||||
|
): {
|
||||||
|
get: () => R;
|
||||||
|
set: (value: R) => void;
|
||||||
} {
|
} {
|
||||||
const valueRef = ref(this.state[key]);
|
const valueRef = ref(this.state[key]);
|
||||||
|
|
||||||
|
@ -265,7 +269,7 @@ export class Storage<T extends StateDef> {
|
||||||
return valueRef.value;
|
return valueRef.value;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
set: (value: unknown) => {
|
set: (value) => {
|
||||||
const val = setter ? setter(value) : value;
|
const val = setter ? setter(value) : value;
|
||||||
this.set(key, val);
|
this.set(key, val);
|
||||||
valueRef.value = val;
|
valueRef.value = val;
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { $i, iAmModerator } from '@/account.js';
|
||||||
import MkLoading from '@/pages/_loading_.vue';
|
import MkLoading from '@/pages/_loading_.vue';
|
||||||
import MkError from '@/pages/_error_.vue';
|
import MkError from '@/pages/_error_.vue';
|
||||||
|
|
||||||
export const page = (loader: AsyncComponentLoader<any>) => defineAsyncComponent({
|
export const page = (loader: AsyncComponentLoader) => defineAsyncComponent({
|
||||||
loader: loader,
|
loader: loader,
|
||||||
loadingComponent: MkLoading,
|
loadingComponent: MkLoading,
|
||||||
errorComponent: MkError,
|
errorComponent: MkError,
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EventEmitter } from 'eventemitter3';
|
import { EventEmitter } from 'eventemitter3';
|
||||||
import { IRouter, Resolved, RouteDef, RouterEvent } from '@/nirax.js';
|
import { IRouter, Resolved, RouteDef, RouterEvent, RouterFlag } from '@/nirax.js';
|
||||||
|
|
||||||
import type { App, ShallowRef } from 'vue';
|
import type { App, ShallowRef } from 'vue';
|
||||||
|
|
||||||
|
@ -79,7 +79,7 @@ class MainRouterProxy implements IRouter {
|
||||||
return this.supplier().currentRoute;
|
return this.supplier().currentRoute;
|
||||||
}
|
}
|
||||||
|
|
||||||
get navHook(): ((path: string, flag?: any) => boolean) | null {
|
get navHook(): ((path: string, flag?: RouterFlag) => boolean) | null {
|
||||||
return this.supplier().navHook;
|
return this.supplier().navHook;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,11 +91,11 @@ class MainRouterProxy implements IRouter {
|
||||||
return this.supplier().getCurrentKey();
|
return this.supplier().getCurrentKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
getCurrentPath(): any {
|
getCurrentPath(): string {
|
||||||
return this.supplier().getCurrentPath();
|
return this.supplier().getCurrentPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
push(path: string, flag?: any): void {
|
push(path: string, flag?: RouterFlag): void {
|
||||||
this.supplier().push(path, flag);
|
this.supplier().push(path, flag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,9 @@
|
||||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
|
|
||||||
export function checkWordMute(note: Record<string, any>, me: Record<string, any> | null | undefined, mutedWords: Array<string | string[]>): boolean {
|
export function checkWordMute(note: Misskey.entities.Note, me: Misskey.entities.UserLite | null | undefined, mutedWords: Array<string | string[]>): boolean {
|
||||||
// 自分自身
|
// 自分自身
|
||||||
if (me && (note.userId === me.id)) return false;
|
if (me && (note.userId === me.id)) return false;
|
||||||
|
|
||||||
|
|
|
@ -3,22 +3,22 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defaultStore } from '@/store.js';
|
export type DeviceKind = 'smartphone' | 'tablet' | 'desktop';
|
||||||
|
|
||||||
await defaultStore.ready;
|
|
||||||
|
|
||||||
const ua = navigator.userAgent.toLowerCase();
|
const ua = navigator.userAgent.toLowerCase();
|
||||||
const isTablet = /ipad/.test(ua) || (/mobile|iphone|android/.test(ua) && window.innerWidth > 700);
|
const isTablet = /ipad/.test(ua) || (/mobile|iphone|android/.test(ua) && window.innerWidth > 700);
|
||||||
const isSmartphone = !isTablet && /mobile|iphone|android/.test(ua);
|
const isSmartphone = !isTablet && /mobile|iphone|android/.test(ua);
|
||||||
|
|
||||||
const isIPhone = /iphone|ipod/gi.test(ua) && navigator.maxTouchPoints > 1;
|
export const DEFAULT_DEVICE_KIND: DeviceKind = (
|
||||||
// navigator.platform may be deprecated but this check is still required
|
isSmartphone
|
||||||
const isIPadOS = navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1;
|
? 'smartphone'
|
||||||
const isIos = /ipad|iphone|ipod/gi.test(ua) && navigator.maxTouchPoints > 1;
|
: isTablet
|
||||||
|
? 'tablet'
|
||||||
|
: 'desktop'
|
||||||
|
);
|
||||||
|
|
||||||
export const isFullscreenNotSupported = isIPhone || isIos;
|
export let deviceKind: DeviceKind = DEFAULT_DEVICE_KIND;
|
||||||
|
|
||||||
export const deviceKind: 'smartphone' | 'tablet' | 'desktop' = defaultStore.state.overridedDeviceKind ? defaultStore.state.overridedDeviceKind
|
export function updateDeviceKind(kind: DeviceKind | null) {
|
||||||
: isSmartphone ? 'smartphone'
|
deviceKind = kind ?? DEFAULT_DEVICE_KIND;
|
||||||
: isTablet ? 'tablet'
|
}
|
||||||
: 'desktop';
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ type Hidden = boolean | ((v: any) => boolean);
|
||||||
export type FormItem = {
|
export type FormItem = {
|
||||||
label?: string;
|
label?: string;
|
||||||
type: 'string';
|
type: 'string';
|
||||||
default: string | null;
|
default?: string | null;
|
||||||
description?: string;
|
description?: string;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
hidden?: Hidden;
|
hidden?: Hidden;
|
||||||
|
@ -24,7 +24,7 @@ export type FormItem = {
|
||||||
} | {
|
} | {
|
||||||
label?: string;
|
label?: string;
|
||||||
type: 'number';
|
type: 'number';
|
||||||
default: number | null;
|
default?: number | null;
|
||||||
description?: string;
|
description?: string;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
hidden?: Hidden;
|
hidden?: Hidden;
|
||||||
|
@ -32,20 +32,20 @@ export type FormItem = {
|
||||||
} | {
|
} | {
|
||||||
label?: string;
|
label?: string;
|
||||||
type: 'boolean';
|
type: 'boolean';
|
||||||
default: boolean | null;
|
default?: boolean | null;
|
||||||
description?: string;
|
description?: string;
|
||||||
hidden?: Hidden;
|
hidden?: Hidden;
|
||||||
} | {
|
} | {
|
||||||
label?: string;
|
label?: string;
|
||||||
type: 'enum';
|
type: 'enum';
|
||||||
default: string | null;
|
default?: string | null;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
hidden?: Hidden;
|
hidden?: Hidden;
|
||||||
enum: EnumItem[];
|
enum: EnumItem[];
|
||||||
} | {
|
} | {
|
||||||
label?: string;
|
label?: string;
|
||||||
type: 'radio';
|
type: 'radio';
|
||||||
default: unknown | null;
|
default?: unknown | null;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
hidden?: Hidden;
|
hidden?: Hidden;
|
||||||
options: {
|
options: {
|
||||||
|
@ -55,7 +55,7 @@ export type FormItem = {
|
||||||
} | {
|
} | {
|
||||||
label?: string;
|
label?: string;
|
||||||
type: 'range';
|
type: 'range';
|
||||||
default: number | null;
|
default?: number | null;
|
||||||
description?: string;
|
description?: string;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
step?: number;
|
step?: number;
|
||||||
|
@ -66,12 +66,12 @@ export type FormItem = {
|
||||||
} | {
|
} | {
|
||||||
label?: string;
|
label?: string;
|
||||||
type: 'object';
|
type: 'object';
|
||||||
default: Record<string, unknown> | null;
|
default?: Record<string, unknown> | null;
|
||||||
hidden: Hidden;
|
hidden: Hidden;
|
||||||
} | {
|
} | {
|
||||||
label?: string;
|
label?: string;
|
||||||
type: 'array';
|
type: 'array';
|
||||||
default: unknown[] | null;
|
default?: unknown[] | null;
|
||||||
hidden: Hidden;
|
hidden: Hidden;
|
||||||
} | {
|
} | {
|
||||||
type: 'button';
|
type: 'button';
|
||||||
|
|
46
packages/frontend/src/scripts/fullscreen.ts
Normal file
46
packages/frontend/src/scripts/fullscreen.ts
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
type PartiallyPartial<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
|
||||||
|
|
||||||
|
type VideoEl = PartiallyPartial<HTMLVideoElement, 'requestFullscreen'> & {
|
||||||
|
webkitEnterFullscreen?(): void;
|
||||||
|
webkitExitFullscreen?(): void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PlayerEl = PartiallyPartial<HTMLElement, 'requestFullscreen'>;
|
||||||
|
|
||||||
|
type RequestFullscreenProps = {
|
||||||
|
readonly videoEl: VideoEl;
|
||||||
|
readonly playerEl: PlayerEl;
|
||||||
|
readonly options?: FullscreenOptions | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ExitFullscreenProps = {
|
||||||
|
readonly videoEl: VideoEl;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const requestFullscreen = ({ videoEl, playerEl, options }: RequestFullscreenProps) => {
|
||||||
|
if (playerEl.requestFullscreen != null) {
|
||||||
|
playerEl.requestFullscreen(options ?? undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (videoEl.webkitEnterFullscreen != null) {
|
||||||
|
videoEl.webkitEnterFullscreen();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const exitFullscreen = ({ videoEl }: ExitFullscreenProps) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
|
if (document.exitFullscreen != null) {
|
||||||
|
document.exitFullscreen();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (videoEl.webkitExitFullscreen != null) {
|
||||||
|
videoEl.webkitExitFullscreen();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
|
@ -17,7 +17,7 @@ export function misskeyApi<
|
||||||
_ResT = ResT extends void ? Misskey.api.SwitchCaseResponseType<E, P> : ResT,
|
_ResT = ResT extends void ? Misskey.api.SwitchCaseResponseType<E, P> : ResT,
|
||||||
>(
|
>(
|
||||||
endpoint: E,
|
endpoint: E,
|
||||||
data: P = {} as any,
|
data: P & { i?: string | null; } = {} as any,
|
||||||
token?: string | null | undefined,
|
token?: string | null | undefined,
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
): Promise<_ResT> {
|
): Promise<_ResT> {
|
||||||
|
@ -30,8 +30,8 @@ export function misskeyApi<
|
||||||
|
|
||||||
const promise = new Promise<_ResT>((resolve, reject) => {
|
const promise = new Promise<_ResT>((resolve, reject) => {
|
||||||
// Append a credential
|
// Append a credential
|
||||||
if ($i) (data as any).i = $i.token;
|
if ($i) data.i = $i.token;
|
||||||
if (token !== undefined) (data as any).i = token;
|
if (token !== undefined) data.i = token;
|
||||||
|
|
||||||
// Send request
|
// Send request
|
||||||
window.fetch(`${apiUrl}/${endpoint}`, {
|
window.fetch(`${apiUrl}/${endpoint}`, {
|
||||||
|
|
|
@ -80,7 +80,7 @@ export function chooseFileFromUrl(): Promise<Misskey.entities.DriveFile> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function select(src: any, label: string | null, multiple: boolean): Promise<Misskey.entities.DriveFile[]> {
|
function select(src: HTMLElement | EventTarget | null, label: string | null, multiple: boolean): Promise<Misskey.entities.DriveFile[]> {
|
||||||
return new Promise((res, rej) => {
|
return new Promise((res, rej) => {
|
||||||
const keepOriginal = ref(defaultStore.state.keepOriginalUploading);
|
const keepOriginal = ref(defaultStore.state.keepOriginalUploading);
|
||||||
|
|
||||||
|
@ -107,10 +107,10 @@ function select(src: any, label: string | null, multiple: boolean): Promise<Miss
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function selectFile(src: any, label: string | null = null): Promise<Misskey.entities.DriveFile> {
|
export function selectFile(src: HTMLElement | EventTarget | null, label: string | null = null): Promise<Misskey.entities.DriveFile> {
|
||||||
return select(src, label, false).then(files => files[0]);
|
return select(src, label, false).then(files => files[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function selectFiles(src: any, label: string | null = null): Promise<Misskey.entities.DriveFile[]> {
|
export function selectFiles(src: HTMLElement | EventTarget | null, label: string | null = null): Promise<Misskey.entities.DriveFile[]> {
|
||||||
return select(src, label, true);
|
return select(src, label, true);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,9 @@
|
||||||
/**
|
/**
|
||||||
* 配列をシャッフル (破壊的)
|
* 配列をシャッフル (破壊的)
|
||||||
*/
|
*/
|
||||||
export function shuffle<T extends any[]>(array: T): T {
|
export function shuffle<T extends unknown[]>(array: T): T {
|
||||||
let currentIndex = array.length, randomIndex;
|
let currentIndex = array.length;
|
||||||
|
let randomIndex: number;
|
||||||
|
|
||||||
// While there remain elements to shuffle.
|
// While there remain elements to shuffle.
|
||||||
while (currentIndex !== 0) {
|
while (currentIndex !== 0) {
|
||||||
|
|
|
@ -32,13 +32,13 @@ const mimeTypeMap = {
|
||||||
|
|
||||||
export function uploadFile(
|
export function uploadFile(
|
||||||
file: File,
|
file: File,
|
||||||
folder?: any,
|
folder?: string | Misskey.entities.DriveFolder,
|
||||||
name?: string,
|
name?: string,
|
||||||
keepOriginal: boolean = defaultStore.state.keepOriginalUploading,
|
keepOriginal: boolean = defaultStore.state.keepOriginalUploading,
|
||||||
): Promise<Misskey.entities.DriveFile> {
|
): Promise<Misskey.entities.DriveFile> {
|
||||||
if ($i == null) throw new Error('Not logged in');
|
if ($i == null) throw new Error('Not logged in');
|
||||||
|
|
||||||
if (folder && typeof folder === 'object') folder = folder.id;
|
const _folder = typeof folder === 'string' ? folder : folder?.id;
|
||||||
|
|
||||||
if (file.size > instance.maxFileSize) {
|
if (file.size > instance.maxFileSize) {
|
||||||
alert({
|
alert({
|
||||||
|
@ -89,11 +89,11 @@ export function uploadFile(
|
||||||
}
|
}
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('i', $i.token);
|
formData.append('i', $i!.token);
|
||||||
formData.append('force', 'true');
|
formData.append('force', 'true');
|
||||||
formData.append('file', resizedImage ?? file);
|
formData.append('file', resizedImage ?? file);
|
||||||
formData.append('name', ctx.name);
|
formData.append('name', ctx.name);
|
||||||
if (folder) formData.append('folderId', folder);
|
if (_folder) formData.append('folderId', _folder);
|
||||||
|
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
xhr.open('POST', apiUrl + '/drive/files/create', true);
|
xhr.open('POST', apiUrl + '/drive/files/create', true);
|
||||||
|
|
|
@ -8,9 +8,11 @@ import * as Misskey from 'misskey-js';
|
||||||
import { hemisphere } from '@@/js/intl-const.js';
|
import { hemisphere } from '@@/js/intl-const.js';
|
||||||
import lightTheme from '@@/themes/l-light.json5';
|
import lightTheme from '@@/themes/l-light.json5';
|
||||||
import darkTheme from '@@/themes/d-green-lime.json5';
|
import darkTheme from '@@/themes/d-green-lime.json5';
|
||||||
import { miLocalStorage } from './local-storage.js';
|
|
||||||
import type { SoundType } from '@/scripts/sound.js';
|
import type { SoundType } from '@/scripts/sound.js';
|
||||||
|
import { DEFAULT_DEVICE_KIND, type DeviceKind } from '@/scripts/device-kind.js';
|
||||||
|
import { miLocalStorage } from '@/local-storage.js';
|
||||||
import { Storage } from '@/pizzax.js';
|
import { Storage } from '@/pizzax.js';
|
||||||
|
import type { Ast } from '@syuilo/aiscript';
|
||||||
|
|
||||||
interface PostFormAction {
|
interface PostFormAction {
|
||||||
title: string,
|
title: string,
|
||||||
|
@ -206,7 +208,7 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||||
|
|
||||||
overridedDeviceKind: {
|
overridedDeviceKind: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
default: null as null | 'smartphone' | 'tablet' | 'desktop',
|
default: null as DeviceKind | null,
|
||||||
},
|
},
|
||||||
serverDisconnectedBehavior: {
|
serverDisconnectedBehavior: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
|
@ -262,11 +264,11 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||||
},
|
},
|
||||||
useBlurEffectForModal: {
|
useBlurEffectForModal: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
default: !/mobile|iphone|android/.test(navigator.userAgent.toLowerCase()), // 循環参照するのでdevice-kind.tsは参照できない
|
default: DEFAULT_DEVICE_KIND === 'desktop',
|
||||||
},
|
},
|
||||||
useBlurEffect: {
|
useBlurEffect: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
default: !/mobile|iphone|android/.test(navigator.userAgent.toLowerCase()), // 循環参照するのでdevice-kind.tsは参照できない
|
default: DEFAULT_DEVICE_KIND === 'desktop',
|
||||||
},
|
},
|
||||||
showFixedPostForm: {
|
showFixedPostForm: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
|
@ -516,7 +518,7 @@ export type Plugin = {
|
||||||
token: string;
|
token: string;
|
||||||
src: string | null;
|
src: string | null;
|
||||||
version: string;
|
version: string;
|
||||||
ast: any[];
|
ast: Ast.Node[];
|
||||||
author?: string;
|
author?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
permissions?: string[];
|
permissions?: string[];
|
||||||
|
@ -554,13 +556,13 @@ export class ColdDeviceStorage {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static getAll(): Partial<typeof this.default> {
|
public static getAll(): Partial<typeof this.default> {
|
||||||
return (Object.keys(this.default) as (keyof typeof this.default)[]).reduce((acc, key) => {
|
return (Object.keys(this.default) as (keyof typeof this.default)[]).reduce<Partial<typeof this.default>>((acc, key) => {
|
||||||
const value = localStorage.getItem(PREFIX + key);
|
const value = localStorage.getItem(PREFIX + key);
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
acc[key] = JSON.parse(value);
|
acc[key] = JSON.parse(value);
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as any);
|
}, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static set<T extends keyof typeof ColdDeviceStorage.default>(key: T, value: typeof ColdDeviceStorage.default[T]): void {
|
public static set<T extends keyof typeof ColdDeviceStorage.default>(key: T, value: typeof ColdDeviceStorage.default[T]): void {
|
||||||
|
@ -605,7 +607,7 @@ export class ColdDeviceStorage {
|
||||||
get: () => {
|
get: () => {
|
||||||
return valueRef.value;
|
return valueRef.value;
|
||||||
},
|
},
|
||||||
set: (value: unknown) => {
|
set: (value: typeof ColdDeviceStorage.default[K]) => {
|
||||||
const val = value;
|
const val = value;
|
||||||
ColdDeviceStorage.set(key, val);
|
ColdDeviceStorage.set(key, val);
|
||||||
},
|
},
|
||||||
|
|
22
packages/frontend/src/types/post-form.ts
Normal file
22
packages/frontend/src/types/post-form.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
|
|
||||||
|
export interface PostFormProps {
|
||||||
|
reply?: Misskey.entities.Note;
|
||||||
|
renote?: Misskey.entities.Note;
|
||||||
|
channel?: Misskey.entities.Channel; // TODO
|
||||||
|
mention?: Misskey.entities.User;
|
||||||
|
specified?: Misskey.entities.UserDetailed;
|
||||||
|
initialText?: string;
|
||||||
|
initialCw?: string;
|
||||||
|
initialVisibility?: (typeof Misskey.noteVisibilities)[number];
|
||||||
|
initialFiles?: Misskey.entities.DriveFile[];
|
||||||
|
initialLocalOnly?: boolean;
|
||||||
|
initialVisibleUsers?: Misskey.entities.UserDetailed[];
|
||||||
|
initialNote?: Misskey.entities.Note;
|
||||||
|
instant?: boolean;
|
||||||
|
};
|
|
@ -68,10 +68,10 @@ const onDriveFileCreated = (file) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const thumbnail = (image: any): string => {
|
const thumbnail = (image: Misskey.entities.DriveFile): string => {
|
||||||
return defaultStore.state.disableShowingAnimatedImages
|
return defaultStore.state.disableShowingAnimatedImages
|
||||||
? getStaticImageUrl(image.url)
|
? getStaticImageUrl(image.url)
|
||||||
: image.thumbnailUrl;
|
: image.thumbnailUrl ?? image.url;
|
||||||
};
|
};
|
||||||
|
|
||||||
misskeyApi('drive/stream', {
|
misskeyApi('drive/stream', {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"name": "misskey-js",
|
"name": "misskey-js",
|
||||||
"version": "2024.10.2-alpha.2",
|
"version": "2024.11.0-alpha.0",
|
||||||
"description": "Misskey SDK for JavaScript",
|
"description": "Misskey SDK for JavaScript",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "./built/index.js",
|
"main": "./built/index.js",
|
||||||
|
|
2
yume-mods/misskey-auto-deploy/.gitignore
vendored
Normal file
2
yume-mods/misskey-auto-deploy/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
/target
|
||||||
|
|
1147
yume-mods/misskey-auto-deploy/Cargo.lock
generated
Normal file
1147
yume-mods/misskey-auto-deploy/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
15
yume-mods/misskey-auto-deploy/Cargo.toml
Normal file
15
yume-mods/misskey-auto-deploy/Cargo.toml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
[package]
|
||||||
|
name = "misskey-auto-deploy"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
axum = { version = "0.7.7", features = ["macros"] }
|
||||||
|
bcrypt = "0.15.1"
|
||||||
|
chrono = { version = "0.4.38", features = ["serde"] }
|
||||||
|
clap = { version = "4.5.20", features = ["derive"] }
|
||||||
|
libc = "0.2.162"
|
||||||
|
serde = { version = "1.0.214", features = ["derive"] }
|
||||||
|
serde_json = "1.0.132"
|
||||||
|
tokio = { version = "1.41.1", features = ["rt", "rt-multi-thread", "macros", "process", "signal"] }
|
||||||
|
toml = "0.8.19"
|
316
yume-mods/misskey-auto-deploy/src/lib.rs
Normal file
316
yume-mods/misskey-auto-deploy/src/lib.rs
Normal file
|
@ -0,0 +1,316 @@
|
||||||
|
use std::{collections::HashMap, fs::OpenOptions, path::PathBuf, process::Stdio, sync::Arc};
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
extract::{Json, Query, State},
|
||||||
|
http::{HeaderMap, StatusCode},
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
};
|
||||||
|
use chrono::NaiveDateTime;
|
||||||
|
use libc::SIGTERM;
|
||||||
|
use tokio::{process::Command, sync::RwLock};
|
||||||
|
|
||||||
|
pub struct App;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AppState {
|
||||||
|
pub bearer_secret: String,
|
||||||
|
pub refs: HashMap<String, Arc<RefState>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RefState {
|
||||||
|
pub config: RefConfig,
|
||||||
|
pub status: RwLock<RefStatus>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct RefStatus {
|
||||||
|
pub status: Status,
|
||||||
|
pub triggered: Option<NaiveDateTime>,
|
||||||
|
pub succeeded: Option<NaiveDateTime>,
|
||||||
|
pub last_payload: Option<WebhookPayload>,
|
||||||
|
pub child_pid: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum Status {
|
||||||
|
Never,
|
||||||
|
Deploying,
|
||||||
|
Validating,
|
||||||
|
Success,
|
||||||
|
Fatal(String),
|
||||||
|
Error(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct RefConfig {
|
||||||
|
#[serde(rename = "ref")]
|
||||||
|
pub ref_: String,
|
||||||
|
pub stdout: Option<PathBuf>,
|
||||||
|
pub stderr: Option<PathBuf>,
|
||||||
|
pub working_dir: PathBuf,
|
||||||
|
pub compose_flags: Option<Vec<String>>,
|
||||||
|
pub env: Option<HashMap<String, String>>,
|
||||||
|
pub uid: u32,
|
||||||
|
pub gid: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct ApiError {
|
||||||
|
pub code: u16,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for ApiError {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
(StatusCode::from_u16(self.code).unwrap(), Json(&self)).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct WebhookPayload {
|
||||||
|
#[serde(rename = "ref")]
|
||||||
|
pub ref_: String,
|
||||||
|
|
||||||
|
pub before: String,
|
||||||
|
pub after: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
pub struct GetStatusQuery {
|
||||||
|
#[serde(rename = "ref")]
|
||||||
|
pub ref_: Option<String>,
|
||||||
|
|
||||||
|
pub branch: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn require_bearer(bcrypted: &str, headers: &HeaderMap) -> Result<(), ApiError> {
|
||||||
|
let bearer = headers
|
||||||
|
.get("authorization")
|
||||||
|
.ok_or_else(|| ApiError {
|
||||||
|
code: 401,
|
||||||
|
message: "Unauthorized".to_string(),
|
||||||
|
})?
|
||||||
|
.to_str()
|
||||||
|
.map_err(|_| ApiError {
|
||||||
|
code: 400,
|
||||||
|
message: "Invalid Authorization header".to_string(),
|
||||||
|
})?;
|
||||||
|
let bearer = bearer
|
||||||
|
.trim_start_matches("Bearer ")
|
||||||
|
.trim_start_matches("bearer ");
|
||||||
|
|
||||||
|
if !bcrypt::verify(bearer, bcrypted).map_err(|_| ApiError {
|
||||||
|
code: 401,
|
||||||
|
message: "Unauthorized".to_string(),
|
||||||
|
})? {
|
||||||
|
return Err(ApiError {
|
||||||
|
code: 401,
|
||||||
|
message: "Unauthorized".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
pub async fn post_deploy(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
Json(payload): Json<WebhookPayload>,
|
||||||
|
) -> Result<StatusCode, ApiError> {
|
||||||
|
require_bearer(&state.bearer_secret, &headers)?;
|
||||||
|
|
||||||
|
let matched_ref = state
|
||||||
|
.refs
|
||||||
|
.into_iter()
|
||||||
|
.find(|(_, ref_state)| ref_state.config.ref_ == payload.ref_)
|
||||||
|
.ok_or_else(|| ApiError {
|
||||||
|
code: 404,
|
||||||
|
message: "Ref not found".to_string(),
|
||||||
|
})?
|
||||||
|
.1
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
let mut status = matched_ref.status.write().await;
|
||||||
|
|
||||||
|
status.last_payload = Some(payload.clone());
|
||||||
|
|
||||||
|
if status.status == Status::Deploying {
|
||||||
|
if status.child_pid == 0 {
|
||||||
|
return Err(ApiError {
|
||||||
|
code: 500,
|
||||||
|
message: "Child PID is 0 while status is InProgress".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if unsafe { libc::kill(status.child_pid as i32, SIGTERM) } != 0 {
|
||||||
|
return Err(ApiError {
|
||||||
|
code: 500,
|
||||||
|
message: "Failed to send SIGTERM to existing process".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if !Command::new("git")
|
||||||
|
.arg("fetch")
|
||||||
|
.arg("origin")
|
||||||
|
.uid(matched_ref.config.uid)
|
||||||
|
.gid(matched_ref.config.gid)
|
||||||
|
.current_dir(matched_ref.config.working_dir.clone())
|
||||||
|
.spawn()
|
||||||
|
.map_err(|_| ApiError {
|
||||||
|
code: 500,
|
||||||
|
message: "Failed to spawn command".to_string(),
|
||||||
|
})?
|
||||||
|
.wait()
|
||||||
|
.await
|
||||||
|
.map_err(|_| ApiError {
|
||||||
|
code: 500,
|
||||||
|
message: "Failed to wait for command".to_string(),
|
||||||
|
})?
|
||||||
|
.success()
|
||||||
|
{
|
||||||
|
status.status = Status::Error("Failed to fetch".to_string());
|
||||||
|
return Ok(StatusCode::OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !Command::new("git")
|
||||||
|
.arg("reset")
|
||||||
|
.arg("--hard")
|
||||||
|
.arg(payload.after)
|
||||||
|
.uid(matched_ref.config.uid)
|
||||||
|
.gid(matched_ref.config.gid)
|
||||||
|
.current_dir(matched_ref.config.working_dir.clone())
|
||||||
|
.spawn()
|
||||||
|
.map_err(|_| ApiError {
|
||||||
|
code: 500,
|
||||||
|
message: "Failed to spawn command".to_string(),
|
||||||
|
})?
|
||||||
|
.wait()
|
||||||
|
.await
|
||||||
|
.map_err(|_| ApiError {
|
||||||
|
code: 500,
|
||||||
|
message: "Failed to wait for command".to_string(),
|
||||||
|
})?
|
||||||
|
.success()
|
||||||
|
{
|
||||||
|
status.status = Status::Error("Failed to reset".to_string());
|
||||||
|
return Ok(StatusCode::OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut command = Command::new("docker")
|
||||||
|
.arg("compose")
|
||||||
|
.arg("up")
|
||||||
|
.arg("--detach")
|
||||||
|
.arg("--build")
|
||||||
|
.args(
|
||||||
|
matched_ref
|
||||||
|
.config
|
||||||
|
.compose_flags
|
||||||
|
.as_ref()
|
||||||
|
.unwrap_or(&Vec::new()),
|
||||||
|
)
|
||||||
|
.current_dir(matched_ref.config.working_dir.clone())
|
||||||
|
.kill_on_drop(true)
|
||||||
|
.envs(
|
||||||
|
matched_ref
|
||||||
|
.config
|
||||||
|
.env
|
||||||
|
.as_ref()
|
||||||
|
.unwrap_or(&HashMap::new())
|
||||||
|
.iter(),
|
||||||
|
)
|
||||||
|
.stdout(match matched_ref.config.stdout.as_ref() {
|
||||||
|
Some(path) => {
|
||||||
|
let file = OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.append(true)
|
||||||
|
.open(path)
|
||||||
|
.map_err(|_| ApiError {
|
||||||
|
code: 500,
|
||||||
|
message: "Failed to open stdout file".to_string(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Stdio::from(file)
|
||||||
|
}
|
||||||
|
None => Stdio::null(),
|
||||||
|
})
|
||||||
|
.spawn()
|
||||||
|
.map_err(|_| ApiError {
|
||||||
|
code: 500,
|
||||||
|
message: "Failed to spawn command".to_string(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
status.status = Status::Deploying;
|
||||||
|
status.triggered = Some(chrono::Utc::now().naive_utc());
|
||||||
|
|
||||||
|
status.child_pid = command.id().ok_or_else(|| ApiError {
|
||||||
|
code: 500,
|
||||||
|
message: "Failed to get child PID".to_string(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let matched_ref = matched_ref.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
match command.wait().await {
|
||||||
|
Ok(exit) => {
|
||||||
|
let mut status = matched_ref.status.write().await;
|
||||||
|
status.status = if exit.success() {
|
||||||
|
status.succeeded = Some(chrono::Utc::now().naive_utc());
|
||||||
|
Status::Success
|
||||||
|
} else {
|
||||||
|
Status::Error(format!("Exit code: {}", exit.code().unwrap_or(-1)))
|
||||||
|
};
|
||||||
|
status.child_pid = 0;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let mut status = matched_ref.status.write().await;
|
||||||
|
status.status = Status::Fatal(format!("Subprocess error: {}", e));
|
||||||
|
status.child_pid = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(StatusCode::OK)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn summary(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
) -> Result<Json<HashMap<String, RefStatus>>, ApiError> {
|
||||||
|
let mut statuses = HashMap::new();
|
||||||
|
for (ref_, ref_state) in state.refs.iter() {
|
||||||
|
let status = ref_state.status.read().await;
|
||||||
|
statuses.insert(ref_.clone(), status.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Json(statuses))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_status(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Query(query): Query<GetStatusQuery>,
|
||||||
|
) -> Result<Json<RefStatus>, ApiError> {
|
||||||
|
let ref_ = match (&query.ref_, &query.branch) {
|
||||||
|
(Some(ref_), None) => ref_.clone(),
|
||||||
|
(None, Some(branch)) => format!("refs/heads/{}", branch),
|
||||||
|
_ => {
|
||||||
|
return Err(ApiError {
|
||||||
|
code: 400,
|
||||||
|
message: "Either ref or branch must be specified".to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let matched_ref = state
|
||||||
|
.refs
|
||||||
|
.get(&ref_)
|
||||||
|
.ok_or_else(|| ApiError {
|
||||||
|
code: 404,
|
||||||
|
message: "Ref not found".to_string(),
|
||||||
|
})?
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
let status = matched_ref.status.read().await;
|
||||||
|
|
||||||
|
Ok(Json(status.clone()))
|
||||||
|
}
|
||||||
|
}
|
87
yume-mods/misskey-auto-deploy/src/main.rs
Normal file
87
yume-mods/misskey-auto-deploy/src/main.rs
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
use axum::Router;
|
||||||
|
use clap::Parser;
|
||||||
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
use tokio::{net::TcpListener, sync::RwLock};
|
||||||
|
|
||||||
|
use misskey_auto_deploy::{App, AppState, RefConfig, RefState, RefStatus, Status};
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
struct Args {
|
||||||
|
#[clap(short, long)]
|
||||||
|
listen: Option<String>,
|
||||||
|
#[clap(short, long)]
|
||||||
|
config: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
pub struct Config {
|
||||||
|
pub service: ServiceConfig,
|
||||||
|
pub auth: AuthConfig,
|
||||||
|
pub refs: HashMap<String, RefConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
pub struct AuthConfig {
|
||||||
|
pub secret: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
pub struct ServiceConfig {
|
||||||
|
pub listen: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Config> for AppState {
|
||||||
|
fn from(val: Config) -> Self {
|
||||||
|
let refs = val
|
||||||
|
.refs
|
||||||
|
.into_iter()
|
||||||
|
.map(|(ref_, config)| {
|
||||||
|
let status = RefStatus {
|
||||||
|
status: Status::Never,
|
||||||
|
last_payload: None,
|
||||||
|
triggered: None,
|
||||||
|
succeeded: None,
|
||||||
|
child_pid: 0,
|
||||||
|
};
|
||||||
|
let status = RwLock::new(status);
|
||||||
|
|
||||||
|
(ref_, Arc::new(RefState { config, status }))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
AppState {
|
||||||
|
bearer_secret: val.auth.secret,
|
||||||
|
refs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let args = Args::parse();
|
||||||
|
|
||||||
|
let config: Config =
|
||||||
|
toml::from_str(&std::fs::read_to_string(args.config).expect("Failed to read config file"))
|
||||||
|
.expect("Failed to parse config file");
|
||||||
|
|
||||||
|
let listen = match (&args.listen, &config.service.listen) {
|
||||||
|
(Some(listen), _) => listen.clone(),
|
||||||
|
(None, Some(listen)) => listen.clone(),
|
||||||
|
(None, None) => panic!("No listen address provided"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let app_state: AppState = config.into();
|
||||||
|
|
||||||
|
let app = Router::new()
|
||||||
|
.route(
|
||||||
|
"/deploy",
|
||||||
|
axum::routing::post(App::post_deploy).get(App::get_status),
|
||||||
|
)
|
||||||
|
.route("/summary", axum::routing::get(App::summary))
|
||||||
|
.with_state(app_state);
|
||||||
|
|
||||||
|
let listener = TcpListener::bind(listen)
|
||||||
|
.await
|
||||||
|
.expect("Failed to bind listener");
|
||||||
|
|
||||||
|
axum::serve(listener, app).await.unwrap();
|
||||||
|
}
|
2
yume-mods/nyuukyou/.gitignore
vendored
2
yume-mods/nyuukyou/.gitignore
vendored
|
@ -1 +1 @@
|
||||||
target/
|
/target
|
||||||
|
|
Loading…
Reference in a new issue