Merge branch 'develop' into reversi

This commit is contained in:
syuilo 2024-01-19 11:11:57 +09:00
commit 4c43ee4b53
32 changed files with 1112 additions and 604 deletions

View file

@ -24,6 +24,7 @@
- Feat: 絵文字の詳細ダイアログを追加 - Feat: 絵文字の詳細ダイアログを追加
- Feat: 枠線をつけるMFM`$[border.width=1,style=solid,color=fff,radius=0 ...]`を追加 - Feat: 枠線をつけるMFM`$[border.width=1,style=solid,color=fff,radius=0 ...]`を追加
- デフォルトで枠線からはみ出る部分が隠されるようにしました。初期と同じ挙動にするには`$[border.noclip`が必要です - デフォルトで枠線からはみ出る部分が隠されるようにしました。初期と同じ挙動にするには`$[border.noclip`が必要です
- Feat: スワイプでタブを切り替えられるように
- Enhance: MFM等のコードブロックに全文コピー用のボタンを追加 - Enhance: MFM等のコードブロックに全文コピー用のボタンを追加
- Enhance: ハッシュタグ入力時に、本文の末尾の行に何も書かれていない場合は新たにスペースを追加しないように - Enhance: ハッシュタグ入力時に、本文の末尾の行に何も書かれていない場合は新たにスペースを追加しないように
- Enhance: チャンネルノートのピン留めをノートのメニューからできるように - Enhance: チャンネルノートのピン留めをノートのメニューからできるように

View file

@ -6,54 +6,171 @@ import ts from 'typescript';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); const __dirname = dirname(__filename);
const parameterRegExp = /\{(\w+)\}/g;
function createMemberType(item) {
if (typeof item !== 'string') {
return ts.factory.createTypeLiteralNode(createMembers(item));
}
const parameters = Array.from(
item.matchAll(parameterRegExp),
([, parameter]) => parameter,
);
if (!parameters.length) {
return ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
}
return ts.factory.createTypeReferenceNode(
ts.factory.createIdentifier('ParameterizedString'),
[
ts.factory.createUnionTypeNode(
parameters.map((parameter) =>
ts.factory.createLiteralTypeNode(
ts.factory.createStringLiteral(parameter),
),
),
),
],
);
}
function createMembers(record) { function createMembers(record) {
return Object.entries(record) return Object.entries(record).map(([k, v]) =>
.map(([k, v]) => ts.factory.createPropertySignature( ts.factory.createPropertySignature(
undefined, undefined,
ts.factory.createStringLiteral(k), ts.factory.createStringLiteral(k),
undefined, undefined,
typeof v === 'string' createMemberType(v),
? ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword) ),
: ts.factory.createTypeLiteralNode(createMembers(v)), );
));
} }
export default function generateDTS() { export default function generateDTS() {
const locale = yaml.load(fs.readFileSync(`${__dirname}/ja-JP.yml`, 'utf-8')); const locale = yaml.load(fs.readFileSync(`${__dirname}/ja-JP.yml`, 'utf-8'));
const members = createMembers(locale); const members = createMembers(locale);
const elements = [ const elements = [
ts.factory.createVariableStatement(
[ts.factory.createToken(ts.SyntaxKind.DeclareKeyword)],
ts.factory.createVariableDeclarationList(
[
ts.factory.createVariableDeclaration(
ts.factory.createIdentifier('kParameters'),
undefined,
ts.factory.createTypeOperatorNode(
ts.SyntaxKind.UniqueKeyword,
ts.factory.createKeywordTypeNode(ts.SyntaxKind.SymbolKeyword),
),
undefined,
),
],
ts.NodeFlags.Const,
),
),
ts.factory.createInterfaceDeclaration(
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
ts.factory.createIdentifier('ParameterizedString'),
[
ts.factory.createTypeParameterDeclaration(
undefined,
ts.factory.createIdentifier('T'),
ts.factory.createTypeReferenceNode(
ts.factory.createIdentifier('string'),
undefined,
),
),
],
undefined,
[
ts.factory.createPropertySignature(
undefined,
ts.factory.createComputedPropertyName(
ts.factory.createIdentifier('kParameters'),
),
undefined,
ts.factory.createTypeReferenceNode(
ts.factory.createIdentifier('T'),
undefined,
),
),
],
),
ts.factory.createInterfaceDeclaration(
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
ts.factory.createIdentifier('ILocale'),
undefined,
undefined,
[
ts.factory.createIndexSignature(
undefined,
[
ts.factory.createParameterDeclaration(
undefined,
undefined,
ts.factory.createIdentifier('_'),
undefined,
ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
undefined,
),
],
ts.factory.createUnionTypeNode([
ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
ts.factory.createTypeReferenceNode(
ts.factory.createIdentifier('ParameterizedString'),
[ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword)],
),
ts.factory.createTypeReferenceNode(
ts.factory.createIdentifier('ILocale'),
undefined,
),
]),
),
],
),
ts.factory.createInterfaceDeclaration( ts.factory.createInterfaceDeclaration(
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
ts.factory.createIdentifier('Locale'), ts.factory.createIdentifier('Locale'),
undefined, undefined,
undefined, [
ts.factory.createHeritageClause(ts.SyntaxKind.ExtendsKeyword, [
ts.factory.createExpressionWithTypeArguments(
ts.factory.createIdentifier('ILocale'),
undefined,
),
]),
],
members, members,
), ),
ts.factory.createVariableStatement( ts.factory.createVariableStatement(
[ts.factory.createToken(ts.SyntaxKind.DeclareKeyword)], [ts.factory.createToken(ts.SyntaxKind.DeclareKeyword)],
ts.factory.createVariableDeclarationList( ts.factory.createVariableDeclarationList(
[ts.factory.createVariableDeclaration( [
ts.factory.createIdentifier('locales'), ts.factory.createVariableDeclaration(
undefined, ts.factory.createIdentifier('locales'),
ts.factory.createTypeLiteralNode([ts.factory.createIndexSignature(
undefined, undefined,
[ts.factory.createParameterDeclaration( ts.factory.createTypeLiteralNode([
undefined, ts.factory.createIndexSignature(
undefined, undefined,
ts.factory.createIdentifier('lang'), [
undefined, ts.factory.createParameterDeclaration(
ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), undefined,
undefined, undefined,
)], ts.factory.createIdentifier('lang'),
ts.factory.createTypeReferenceNode( undefined,
ts.factory.createIdentifier('Locale'), ts.factory.createKeywordTypeNode(
undefined, ts.SyntaxKind.StringKeyword,
), ),
)]), undefined,
undefined, ),
)], ],
ts.NodeFlags.Const | ts.NodeFlags.Ambient | ts.NodeFlags.ContextFlags, ts.factory.createTypeReferenceNode(
ts.factory.createIdentifier('Locale'),
undefined,
),
),
]),
undefined,
),
],
ts.NodeFlags.Const,
), ),
), ),
ts.factory.createFunctionDeclaration( ts.factory.createFunctionDeclaration(
@ -70,16 +187,28 @@ export default function generateDTS() {
), ),
ts.factory.createExportDefault(ts.factory.createIdentifier('locales')), ts.factory.createExportDefault(ts.factory.createIdentifier('locales')),
]; ];
const printed = ts.createPrinter({ const printed = ts
newLine: ts.NewLineKind.LineFeed, .createPrinter({
}).printList( newLine: ts.NewLineKind.LineFeed,
ts.ListFormat.MultiLine, })
ts.factory.createNodeArray(elements), .printList(
ts.createSourceFile('index.d.ts', '', ts.ScriptTarget.ESNext, true, ts.ScriptKind.TS), ts.ListFormat.MultiLine,
); ts.factory.createNodeArray(elements),
ts.createSourceFile(
'index.d.ts',
'',
ts.ScriptTarget.ESNext,
true,
ts.ScriptKind.TS,
),
);
fs.writeFileSync(`${__dirname}/index.d.ts`, `/* eslint-disable */ fs.writeFileSync(
`${__dirname}/index.d.ts`,
`/* eslint-disable */
// This file is generated by locales/generateDTS.js // This file is generated by locales/generateDTS.js
// Do not edit this file directly. // Do not edit this file directly.
${printed}`, 'utf-8'); ${printed}`,
'utf-8',
);
} }

230
locales/index.d.ts vendored
View file

@ -1,12 +1,19 @@
/* eslint-disable */ /* eslint-disable */
// This file is generated by locales/generateDTS.js // This file is generated by locales/generateDTS.js
// Do not edit this file directly. // Do not edit this file directly.
export interface Locale { declare const kParameters: unique symbol;
export interface ParameterizedString<T extends string> {
[kParameters]: T;
}
export interface ILocale {
[_: string]: string | ParameterizedString<string> | ILocale;
}
export interface Locale extends ILocale {
"_lang_": string; "_lang_": string;
"headlineMisskey": string; "headlineMisskey": string;
"introMisskey": string; "introMisskey": string;
"poweredByMisskeyDescription": string; "poweredByMisskeyDescription": ParameterizedString<"name">;
"monthAndDay": string; "monthAndDay": ParameterizedString<"month" | "day">;
"search": string; "search": string;
"notifications": string; "notifications": string;
"username": string; "username": string;
@ -18,7 +25,7 @@ export interface Locale {
"cancel": string; "cancel": string;
"noThankYou": string; "noThankYou": string;
"enterUsername": string; "enterUsername": string;
"renotedBy": string; "renotedBy": ParameterizedString<"user">;
"noNotes": string; "noNotes": string;
"noNotifications": string; "noNotifications": string;
"instance": string; "instance": string;
@ -78,8 +85,8 @@ export interface Locale {
"export": string; "export": string;
"files": string; "files": string;
"download": string; "download": string;
"driveFileDeleteConfirm": string; "driveFileDeleteConfirm": ParameterizedString<"name">;
"unfollowConfirm": string; "unfollowConfirm": ParameterizedString<"name">;
"exportRequested": string; "exportRequested": string;
"importRequested": string; "importRequested": string;
"lists": string; "lists": string;
@ -183,9 +190,9 @@ export interface Locale {
"wallpaper": string; "wallpaper": string;
"setWallpaper": string; "setWallpaper": string;
"removeWallpaper": string; "removeWallpaper": string;
"searchWith": string; "searchWith": ParameterizedString<"q">;
"youHaveNoLists": string; "youHaveNoLists": string;
"followConfirm": string; "followConfirm": ParameterizedString<"name">;
"proxyAccount": string; "proxyAccount": string;
"proxyAccountDescription": string; "proxyAccountDescription": string;
"host": string; "host": string;
@ -208,7 +215,7 @@ export interface Locale {
"software": string; "software": string;
"version": string; "version": string;
"metadata": string; "metadata": string;
"withNFiles": string; "withNFiles": ParameterizedString<"n">;
"monitor": string; "monitor": string;
"jobQueue": string; "jobQueue": string;
"cpuAndMemory": string; "cpuAndMemory": string;
@ -237,7 +244,7 @@ export interface Locale {
"processing": string; "processing": string;
"preview": string; "preview": string;
"default": string; "default": string;
"defaultValueIs": string; "defaultValueIs": ParameterizedString<"value">;
"noCustomEmojis": string; "noCustomEmojis": string;
"noJobs": string; "noJobs": string;
"federating": string; "federating": string;
@ -266,8 +273,8 @@ export interface Locale {
"imageUrl": string; "imageUrl": string;
"remove": string; "remove": string;
"removed": string; "removed": string;
"removeAreYouSure": string; "removeAreYouSure": ParameterizedString<"x">;
"deleteAreYouSure": string; "deleteAreYouSure": ParameterizedString<"x">;
"resetAreYouSure": string; "resetAreYouSure": string;
"areYouSure": string; "areYouSure": string;
"saved": string; "saved": string;
@ -285,8 +292,8 @@ export interface Locale {
"messageRead": string; "messageRead": string;
"noMoreHistory": string; "noMoreHistory": string;
"startMessaging": string; "startMessaging": string;
"nUsersRead": string; "nUsersRead": ParameterizedString<"n">;
"agreeTo": string; "agreeTo": ParameterizedString<"0">;
"agree": string; "agree": string;
"agreeBelow": string; "agreeBelow": string;
"basicNotesBeforeCreateAccount": string; "basicNotesBeforeCreateAccount": string;
@ -298,7 +305,7 @@ export interface Locale {
"images": string; "images": string;
"image": string; "image": string;
"birthday": string; "birthday": string;
"yearsOld": string; "yearsOld": ParameterizedString<"age">;
"registeredDate": string; "registeredDate": string;
"location": string; "location": string;
"theme": string; "theme": string;
@ -353,9 +360,9 @@ export interface Locale {
"thisYear": string; "thisYear": string;
"thisMonth": string; "thisMonth": string;
"today": string; "today": string;
"dayX": string; "dayX": ParameterizedString<"day">;
"monthX": string; "monthX": ParameterizedString<"month">;
"yearX": string; "yearX": ParameterizedString<"year">;
"pages": string; "pages": string;
"integration": string; "integration": string;
"connectService": string; "connectService": string;
@ -420,7 +427,7 @@ export interface Locale {
"recentlyUpdatedUsers": string; "recentlyUpdatedUsers": string;
"recentlyRegisteredUsers": string; "recentlyRegisteredUsers": string;
"recentlyDiscoveredUsers": string; "recentlyDiscoveredUsers": string;
"exploreUsersCount": string; "exploreUsersCount": ParameterizedString<"count">;
"exploreFediverse": string; "exploreFediverse": string;
"popularTags": string; "popularTags": string;
"userList": string; "userList": string;
@ -437,16 +444,16 @@ export interface Locale {
"moderationNote": string; "moderationNote": string;
"addModerationNote": string; "addModerationNote": string;
"moderationLogs": string; "moderationLogs": string;
"nUsersMentioned": string; "nUsersMentioned": ParameterizedString<"n">;
"securityKeyAndPasskey": string; "securityKeyAndPasskey": string;
"securityKey": string; "securityKey": string;
"lastUsed": string; "lastUsed": string;
"lastUsedAt": string; "lastUsedAt": ParameterizedString<"t">;
"unregister": string; "unregister": string;
"passwordLessLogin": string; "passwordLessLogin": string;
"passwordLessLoginDescription": string; "passwordLessLoginDescription": string;
"resetPassword": string; "resetPassword": string;
"newPasswordIs": string; "newPasswordIs": ParameterizedString<"password">;
"reduceUiAnimation": string; "reduceUiAnimation": string;
"share": string; "share": string;
"notFound": string; "notFound": string;
@ -466,7 +473,7 @@ export interface Locale {
"enable": string; "enable": string;
"next": string; "next": string;
"retype": string; "retype": string;
"noteOf": string; "noteOf": ParameterizedString<"user">;
"quoteAttached": string; "quoteAttached": string;
"quoteQuestion": string; "quoteQuestion": string;
"noMessagesYet": string; "noMessagesYet": string;
@ -486,12 +493,12 @@ export interface Locale {
"strongPassword": string; "strongPassword": string;
"passwordMatched": string; "passwordMatched": string;
"passwordNotMatched": string; "passwordNotMatched": string;
"signinWith": string; "signinWith": ParameterizedString<"x">;
"signinFailed": string; "signinFailed": string;
"or": string; "or": string;
"language": string; "language": string;
"uiLanguage": string; "uiLanguage": string;
"aboutX": string; "aboutX": ParameterizedString<"x">;
"emojiStyle": string; "emojiStyle": string;
"native": string; "native": string;
"disableDrawer": string; "disableDrawer": string;
@ -509,7 +516,7 @@ export interface Locale {
"regenerate": string; "regenerate": string;
"fontSize": string; "fontSize": string;
"mediaListWithOneImageAppearance": string; "mediaListWithOneImageAppearance": string;
"limitTo": string; "limitTo": ParameterizedString<"x">;
"noFollowRequests": string; "noFollowRequests": string;
"openImageInNewTab": string; "openImageInNewTab": string;
"dashboard": string; "dashboard": string;
@ -587,7 +594,7 @@ export interface Locale {
"deleteAllFiles": string; "deleteAllFiles": string;
"deleteAllFilesConfirm": string; "deleteAllFilesConfirm": string;
"removeAllFollowing": string; "removeAllFollowing": string;
"removeAllFollowingDescription": string; "removeAllFollowingDescription": ParameterizedString<"host">;
"userSuspended": string; "userSuspended": string;
"userSilenced": string; "userSilenced": string;
"yourAccountSuspendedTitle": string; "yourAccountSuspendedTitle": string;
@ -658,9 +665,9 @@ export interface Locale {
"wordMute": string; "wordMute": string;
"hardWordMute": string; "hardWordMute": string;
"regexpError": string; "regexpError": string;
"regexpErrorDescription": string; "regexpErrorDescription": ParameterizedString<"tab" | "line">;
"instanceMute": string; "instanceMute": string;
"userSaysSomething": string; "userSaysSomething": ParameterizedString<"name">;
"makeActive": string; "makeActive": string;
"display": string; "display": string;
"copy": string; "copy": string;
@ -686,7 +693,7 @@ export interface Locale {
"abuseReports": string; "abuseReports": string;
"reportAbuse": string; "reportAbuse": string;
"reportAbuseRenote": string; "reportAbuseRenote": string;
"reportAbuseOf": string; "reportAbuseOf": ParameterizedString<"name">;
"fillAbuseReportDescription": string; "fillAbuseReportDescription": string;
"abuseReported": string; "abuseReported": string;
"reporter": string; "reporter": string;
@ -701,7 +708,7 @@ export interface Locale {
"defaultNavigationBehaviour": string; "defaultNavigationBehaviour": string;
"editTheseSettingsMayBreakAccount": string; "editTheseSettingsMayBreakAccount": string;
"instanceTicker": string; "instanceTicker": string;
"waitingFor": string; "waitingFor": ParameterizedString<"x">;
"random": string; "random": string;
"system": string; "system": string;
"switchUi": string; "switchUi": string;
@ -711,10 +718,10 @@ export interface Locale {
"optional": string; "optional": string;
"createNewClip": string; "createNewClip": string;
"unclip": string; "unclip": string;
"confirmToUnclipAlreadyClippedNote": string; "confirmToUnclipAlreadyClippedNote": ParameterizedString<"name">;
"public": string; "public": string;
"private": string; "private": string;
"i18nInfo": string; "i18nInfo": ParameterizedString<"link">;
"manageAccessTokens": string; "manageAccessTokens": string;
"accountInfo": string; "accountInfo": string;
"notesCount": string; "notesCount": string;
@ -764,9 +771,9 @@ export interface Locale {
"needReloadToApply": string; "needReloadToApply": string;
"showTitlebar": string; "showTitlebar": string;
"clearCache": string; "clearCache": string;
"onlineUsersCount": string; "onlineUsersCount": ParameterizedString<"n">;
"nUsers": string; "nUsers": ParameterizedString<"n">;
"nNotes": string; "nNotes": ParameterizedString<"n">;
"sendErrorReports": string; "sendErrorReports": string;
"sendErrorReportsDescription": string; "sendErrorReportsDescription": string;
"myTheme": string; "myTheme": string;
@ -798,7 +805,7 @@ export interface Locale {
"publish": string; "publish": string;
"inChannelSearch": string; "inChannelSearch": string;
"useReactionPickerForContextMenu": string; "useReactionPickerForContextMenu": string;
"typingUsers": string; "typingUsers": ParameterizedString<"users">;
"jumpToSpecifiedDate": string; "jumpToSpecifiedDate": string;
"showingPastTimeline": string; "showingPastTimeline": string;
"clear": string; "clear": string;
@ -865,7 +872,7 @@ export interface Locale {
"misskeyUpdated": string; "misskeyUpdated": string;
"whatIsNew": string; "whatIsNew": string;
"translate": string; "translate": string;
"translatedFrom": string; "translatedFrom": ParameterizedString<"x">;
"accountDeletionInProgress": string; "accountDeletionInProgress": string;
"usernameInfo": string; "usernameInfo": string;
"aiChanMode": string; "aiChanMode": string;
@ -896,11 +903,11 @@ export interface Locale {
"continueThread": string; "continueThread": string;
"deleteAccountConfirm": string; "deleteAccountConfirm": string;
"incorrectPassword": string; "incorrectPassword": string;
"voteConfirm": string; "voteConfirm": ParameterizedString<"choice">;
"hide": string; "hide": string;
"useDrawerReactionPickerForMobile": string; "useDrawerReactionPickerForMobile": string;
"welcomeBackWithName": string; "welcomeBackWithName": ParameterizedString<"name">;
"clickToFinishEmailVerification": string; "clickToFinishEmailVerification": ParameterizedString<"ok">;
"overridedDeviceKind": string; "overridedDeviceKind": string;
"smartphone": string; "smartphone": string;
"tablet": string; "tablet": string;
@ -928,8 +935,8 @@ export interface Locale {
"cropYes": string; "cropYes": string;
"cropNo": string; "cropNo": string;
"file": string; "file": string;
"recentNHours": string; "recentNHours": ParameterizedString<"n">;
"recentNDays": string; "recentNDays": ParameterizedString<"n">;
"noEmailServerWarning": string; "noEmailServerWarning": string;
"thereIsUnresolvedAbuseReportWarning": string; "thereIsUnresolvedAbuseReportWarning": string;
"recommended": string; "recommended": string;
@ -938,7 +945,7 @@ export interface Locale {
"driveCapOverrideCaption": string; "driveCapOverrideCaption": string;
"requireAdminForView": string; "requireAdminForView": string;
"isSystemAccount": string; "isSystemAccount": string;
"typeToConfirm": string; "typeToConfirm": ParameterizedString<"x">;
"deleteAccount": string; "deleteAccount": string;
"document": string; "document": string;
"numberOfPageCache": string; "numberOfPageCache": string;
@ -992,7 +999,7 @@ export interface Locale {
"neverShow": string; "neverShow": string;
"remindMeLater": string; "remindMeLater": string;
"didYouLikeMisskey": string; "didYouLikeMisskey": string;
"pleaseDonate": string; "pleaseDonate": ParameterizedString<"host">;
"roles": string; "roles": string;
"role": string; "role": string;
"noRole": string; "noRole": string;
@ -1090,7 +1097,7 @@ export interface Locale {
"preservedUsernamesDescription": string; "preservedUsernamesDescription": string;
"createNoteFromTheFile": string; "createNoteFromTheFile": string;
"archive": string; "archive": string;
"channelArchiveConfirmTitle": string; "channelArchiveConfirmTitle": ParameterizedString<"name">;
"channelArchiveConfirmDescription": string; "channelArchiveConfirmDescription": string;
"thisChannelArchived": string; "thisChannelArchived": string;
"displayOfNote": string; "displayOfNote": string;
@ -1120,8 +1127,8 @@ export interface Locale {
"createCount": string; "createCount": string;
"inviteCodeCreated": string; "inviteCodeCreated": string;
"inviteLimitExceeded": string; "inviteLimitExceeded": string;
"createLimitRemaining": string; "createLimitRemaining": ParameterizedString<"limit">;
"inviteLimitResetCycle": string; "inviteLimitResetCycle": ParameterizedString<"time" | "limit">;
"expirationDate": string; "expirationDate": string;
"noExpirationDate": string; "noExpirationDate": string;
"inviteCodeUsedAt": string; "inviteCodeUsedAt": string;
@ -1134,7 +1141,7 @@ export interface Locale {
"expired": string; "expired": string;
"doYouAgree": string; "doYouAgree": string;
"beSureToReadThisAsItIsImportant": string; "beSureToReadThisAsItIsImportant": string;
"iHaveReadXCarefullyAndAgree": string; "iHaveReadXCarefullyAndAgree": ParameterizedString<"x">;
"dialog": string; "dialog": string;
"icon": string; "icon": string;
"forYou": string; "forYou": string;
@ -1189,7 +1196,7 @@ export interface Locale {
"doReaction": string; "doReaction": string;
"code": string; "code": string;
"reloadRequiredToApplySettings": string; "reloadRequiredToApplySettings": string;
"remainingN": string; "remainingN": ParameterizedString<"n">;
"overwriteContentConfirm": string; "overwriteContentConfirm": string;
"seasonalScreenEffect": string; "seasonalScreenEffect": string;
"decorate": string; "decorate": string;
@ -1202,8 +1209,9 @@ export interface Locale {
"replay": string; "replay": string;
"replaying": string; "replaying": string;
"ranking": string; "ranking": string;
"lastNDays": string; "lastNDays": ParameterizedString<"n">;
"backToTitle": string; "backToTitle": string;
"enableHorizontalSwipe": string;
"_bubbleGame": { "_bubbleGame": {
"howToPlay": string; "howToPlay": string;
"_howToPlay": { "_howToPlay": {
@ -1220,7 +1228,7 @@ export interface Locale {
"end": string; "end": string;
"tooManyActiveAnnouncementDescription": string; "tooManyActiveAnnouncementDescription": string;
"readConfirmTitle": string; "readConfirmTitle": string;
"readConfirmText": string; "readConfirmText": ParameterizedString<"title">;
"shouldNotBeUsedToPresentPermanentInfo": string; "shouldNotBeUsedToPresentPermanentInfo": string;
"dialogAnnouncementUxWarn": string; "dialogAnnouncementUxWarn": string;
"silence": string; "silence": string;
@ -1235,10 +1243,10 @@ export interface Locale {
"theseSettingsCanEditLater": string; "theseSettingsCanEditLater": string;
"youCanEditMoreSettingsInSettingsPageLater": string; "youCanEditMoreSettingsInSettingsPageLater": string;
"followUsers": string; "followUsers": string;
"pushNotificationDescription": string; "pushNotificationDescription": ParameterizedString<"name">;
"initialAccountSettingCompleted": string; "initialAccountSettingCompleted": string;
"haveFun": string; "haveFun": ParameterizedString<"name">;
"youCanContinueTutorial": string; "youCanContinueTutorial": ParameterizedString<"name">;
"startTutorial": string; "startTutorial": string;
"skipAreYouSure": string; "skipAreYouSure": string;
"laterAreYouSure": string; "laterAreYouSure": string;
@ -1276,7 +1284,7 @@ export interface Locale {
"social": string; "social": string;
"global": string; "global": string;
"description2": string; "description2": string;
"description3": string; "description3": ParameterizedString<"link">;
}; };
"_postNote": { "_postNote": {
"title": string; "title": string;
@ -1314,7 +1322,7 @@ export interface Locale {
}; };
"_done": { "_done": {
"title": string; "title": string;
"description": string; "description": ParameterizedString<"link">;
}; };
}; };
"_timelineDescription": { "_timelineDescription": {
@ -1328,10 +1336,10 @@ export interface Locale {
}; };
"_serverSettings": { "_serverSettings": {
"iconUrl": string; "iconUrl": string;
"appIconDescription": string; "appIconDescription": ParameterizedString<"host">;
"appIconUsageExample": string; "appIconUsageExample": string;
"appIconStyleRecommendation": string; "appIconStyleRecommendation": string;
"appIconResolutionMustBe": string; "appIconResolutionMustBe": ParameterizedString<"resolution">;
"manifestJsonOverride": string; "manifestJsonOverride": string;
"shortName": string; "shortName": string;
"shortNameDescription": string; "shortNameDescription": string;
@ -1342,7 +1350,7 @@ export interface Locale {
"_accountMigration": { "_accountMigration": {
"moveFrom": string; "moveFrom": string;
"moveFromSub": string; "moveFromSub": string;
"moveFromLabel": string; "moveFromLabel": ParameterizedString<"n">;
"moveFromDescription": string; "moveFromDescription": string;
"moveTo": string; "moveTo": string;
"moveToLabel": string; "moveToLabel": string;
@ -1350,7 +1358,7 @@ export interface Locale {
"moveAccountDescription": string; "moveAccountDescription": string;
"moveAccountHowTo": string; "moveAccountHowTo": string;
"startMigration": string; "startMigration": string;
"migrationConfirm": string; "migrationConfirm": ParameterizedString<"account">;
"movedAndCannotBeUndone": string; "movedAndCannotBeUndone": string;
"postMigrationNote": string; "postMigrationNote": string;
"movedTo": string; "movedTo": string;
@ -1792,7 +1800,7 @@ export interface Locale {
"_signup": { "_signup": {
"almostThere": string; "almostThere": string;
"emailAddressInfo": string; "emailAddressInfo": string;
"emailSent": string; "emailSent": ParameterizedString<"email">;
}; };
"_accountDelete": { "_accountDelete": {
"accountDelete": string; "accountDelete": string;
@ -1845,14 +1853,14 @@ export interface Locale {
"save": string; "save": string;
"inputName": string; "inputName": string;
"cannotSave": string; "cannotSave": string;
"nameAlreadyExists": string; "nameAlreadyExists": ParameterizedString<"name">;
"applyConfirm": string; "applyConfirm": ParameterizedString<"name">;
"saveConfirm": string; "saveConfirm": ParameterizedString<"name">;
"deleteConfirm": string; "deleteConfirm": ParameterizedString<"name">;
"renameConfirm": string; "renameConfirm": ParameterizedString<"old" | "new">;
"noBackups": string; "noBackups": string;
"createdAt": string; "createdAt": ParameterizedString<"date" | "time">;
"updatedAt": string; "updatedAt": ParameterizedString<"date" | "time">;
"cannotLoad": string; "cannotLoad": string;
"invalidFile": string; "invalidFile": string;
}; };
@ -1897,8 +1905,8 @@ export interface Locale {
"featured": string; "featured": string;
"owned": string; "owned": string;
"following": string; "following": string;
"usersCount": string; "usersCount": ParameterizedString<"n">;
"notesCount": string; "notesCount": ParameterizedString<"n">;
"nameAndDescription": string; "nameAndDescription": string;
"nameOnly": string; "nameOnly": string;
"allowRenoteToExternal": string; "allowRenoteToExternal": string;
@ -1926,7 +1934,7 @@ export interface Locale {
"manage": string; "manage": string;
"code": string; "code": string;
"description": string; "description": string;
"installed": string; "installed": ParameterizedString<"name">;
"installedThemes": string; "installedThemes": string;
"builtinThemes": string; "builtinThemes": string;
"alreadyInstalled": string; "alreadyInstalled": string;
@ -1949,7 +1957,7 @@ export interface Locale {
"lighten": string; "lighten": string;
"inputConstantName": string; "inputConstantName": string;
"importInfo": string; "importInfo": string;
"deleteConstantConfirm": string; "deleteConstantConfirm": ParameterizedString<"const">;
"keys": { "keys": {
"accent": string; "accent": string;
"bg": string; "bg": string;
@ -2012,23 +2020,23 @@ export interface Locale {
"_ago": { "_ago": {
"future": string; "future": string;
"justNow": string; "justNow": string;
"secondsAgo": string; "secondsAgo": ParameterizedString<"n">;
"minutesAgo": string; "minutesAgo": ParameterizedString<"n">;
"hoursAgo": string; "hoursAgo": ParameterizedString<"n">;
"daysAgo": string; "daysAgo": ParameterizedString<"n">;
"weeksAgo": string; "weeksAgo": ParameterizedString<"n">;
"monthsAgo": string; "monthsAgo": ParameterizedString<"n">;
"yearsAgo": string; "yearsAgo": ParameterizedString<"n">;
"invalid": string; "invalid": string;
}; };
"_timeIn": { "_timeIn": {
"seconds": string; "seconds": ParameterizedString<"n">;
"minutes": string; "minutes": ParameterizedString<"n">;
"hours": string; "hours": ParameterizedString<"n">;
"days": string; "days": ParameterizedString<"n">;
"weeks": string; "weeks": ParameterizedString<"n">;
"months": string; "months": ParameterizedString<"n">;
"years": string; "years": ParameterizedString<"n">;
}; };
"_time": { "_time": {
"second": string; "second": string;
@ -2039,7 +2047,7 @@ export interface Locale {
"_2fa": { "_2fa": {
"alreadyRegistered": string; "alreadyRegistered": string;
"registerTOTP": string; "registerTOTP": string;
"step1": string; "step1": ParameterizedString<"a" | "b">;
"step2": string; "step2": string;
"step2Click": string; "step2Click": string;
"step2Uri": string; "step2Uri": string;
@ -2054,7 +2062,7 @@ export interface Locale {
"securityKeyName": string; "securityKeyName": string;
"tapSecurityKey": string; "tapSecurityKey": string;
"removeKey": string; "removeKey": string;
"removeKeyConfirm": string; "removeKeyConfirm": ParameterizedString<"name">;
"whyTOTPOnlyRenew": string; "whyTOTPOnlyRenew": string;
"renewTOTP": string; "renewTOTP": string;
"renewTOTPConfirm": string; "renewTOTPConfirm": string;
@ -2155,9 +2163,9 @@ export interface Locale {
}; };
"_auth": { "_auth": {
"shareAccessTitle": string; "shareAccessTitle": string;
"shareAccess": string; "shareAccess": ParameterizedString<"name">;
"shareAccessAsk": string; "shareAccessAsk": string;
"permission": string; "permission": ParameterizedString<"name">;
"permissionAsk": string; "permissionAsk": string;
"pleaseGoBack": string; "pleaseGoBack": string;
"callback": string; "callback": string;
@ -2216,12 +2224,12 @@ export interface Locale {
"_cw": { "_cw": {
"hide": string; "hide": string;
"show": string; "show": string;
"chars": string; "chars": ParameterizedString<"count">;
"files": string; "files": ParameterizedString<"count">;
}; };
"_poll": { "_poll": {
"noOnlyOneChoice": string; "noOnlyOneChoice": string;
"choiceN": string; "choiceN": ParameterizedString<"n">;
"noMore": string; "noMore": string;
"canMultipleVote": string; "canMultipleVote": string;
"expiration": string; "expiration": string;
@ -2231,16 +2239,16 @@ export interface Locale {
"deadlineDate": string; "deadlineDate": string;
"deadlineTime": string; "deadlineTime": string;
"duration": string; "duration": string;
"votesCount": string; "votesCount": ParameterizedString<"n">;
"totalVotes": string; "totalVotes": ParameterizedString<"n">;
"vote": string; "vote": string;
"showResult": string; "showResult": string;
"voted": string; "voted": string;
"closed": string; "closed": string;
"remainingDays": string; "remainingDays": ParameterizedString<"d" | "h">;
"remainingHours": string; "remainingHours": ParameterizedString<"h" | "m">;
"remainingMinutes": string; "remainingMinutes": ParameterizedString<"m" | "s">;
"remainingSeconds": string; "remainingSeconds": ParameterizedString<"s">;
}; };
"_visibility": { "_visibility": {
"public": string; "public": string;
@ -2280,7 +2288,7 @@ export interface Locale {
"changeAvatar": string; "changeAvatar": string;
"changeBanner": string; "changeBanner": string;
"verifiedLinkDescription": string; "verifiedLinkDescription": string;
"avatarDecorationMax": string; "avatarDecorationMax": ParameterizedString<"max">;
}; };
"_exportOrImport": { "_exportOrImport": {
"allNotes": string; "allNotes": string;
@ -2403,16 +2411,16 @@ export interface Locale {
}; };
"_notification": { "_notification": {
"fileUploaded": string; "fileUploaded": string;
"youGotMention": string; "youGotMention": ParameterizedString<"name">;
"youGotReply": string; "youGotReply": ParameterizedString<"name">;
"youGotQuote": string; "youGotQuote": ParameterizedString<"name">;
"youRenoted": string; "youRenoted": ParameterizedString<"name">;
"youWereFollowed": string; "youWereFollowed": string;
"youReceivedFollowRequest": string; "youReceivedFollowRequest": string;
"yourFollowRequestAccepted": string; "yourFollowRequestAccepted": string;
"pollEnded": string; "pollEnded": string;
"newNote": string; "newNote": string;
"unreadAntennaNote": string; "unreadAntennaNote": ParameterizedString<"name">;
"roleAssigned": string; "roleAssigned": string;
"emptyPushNotificationMessage": string; "emptyPushNotificationMessage": string;
"achievementEarned": string; "achievementEarned": string;
@ -2420,9 +2428,9 @@ export interface Locale {
"checkNotificationBehavior": string; "checkNotificationBehavior": string;
"sendTestNotification": string; "sendTestNotification": string;
"notificationWillBeDisplayedLikeThis": string; "notificationWillBeDisplayedLikeThis": string;
"reactedBySomeUsers": string; "reactedBySomeUsers": ParameterizedString<"n">;
"renotedBySomeUsers": string; "renotedBySomeUsers": ParameterizedString<"n">;
"followedBySomeUsers": string; "followedBySomeUsers": ParameterizedString<"n">;
"_types": { "_types": {
"all": string; "all": string;
"note": string; "note": string;
@ -2479,8 +2487,8 @@ export interface Locale {
}; };
}; };
"_dialog": { "_dialog": {
"charactersExceeded": string; "charactersExceeded": ParameterizedString<"current" | "max">;
"charactersBelow": string; "charactersBelow": ParameterizedString<"current" | "min">;
}; };
"_disabledTimeline": { "_disabledTimeline": {
"title": string; "title": string;

View file

@ -1201,6 +1201,7 @@ replaying: "リプレイ中"
ranking: "ランキング" ranking: "ランキング"
lastNDays: "直近{n}日" lastNDays: "直近{n}日"
backToTitle: "タイトルへ" backToTitle: "タイトルへ"
enableHorizontalSwipe: "スワイプしてタブを切り替える"
_bubbleGame: _bubbleGame:
howToPlay: "遊び方" howToPlay: "遊び方"

View file

@ -4,8 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <template>
<code v-if="inline" :class="$style.codeInlineRoot">{{ code }}</code> <div :class="$style.codeBlockRoot">
<div v-else :class="$style.codeBlockRoot">
<button :class="$style.codeBlockCopyButton" class="_button" @click="copy"> <button :class="$style.codeBlockCopyButton" class="_button" @click="copy">
<i class="ti ti-copy"></i> <i class="ti ti-copy"></i>
</button> </button>
@ -36,7 +35,6 @@ import copyToClipboard from '@/scripts/copy-to-clipboard.js';
const props = defineProps<{ const props = defineProps<{
code: string; code: string;
lang?: string; lang?: string;
inline?: boolean;
}>(); }>();
const show = ref(!defaultStore.state.dataSaver.code); const show = ref(!defaultStore.state.dataSaver.code);
@ -66,16 +64,6 @@ function copy() {
} }
} }
.codeInlineRoot {
display: inline-block;
font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
overflow-wrap: anywhere;
color: #D4D4D4;
background: #1E1E1E;
padding: .1em;
border-radius: .3em;
}
.codeBlockFallbackRoot { .codeBlockFallbackRoot {
display: block; display: block;
overflow-wrap: anywhere; overflow-wrap: anywhere;

View file

@ -0,0 +1,26 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<code :class="$style.root">{{ code }}</code>
</template>
<script lang="ts" setup>
const props = defineProps<{
code: string;
}>();
</script>
<style module lang="scss">
.root {
display: inline-block;
font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
overflow-wrap: anywhere;
color: #D4D4D4;
background: #1E1E1E;
padding: .1em;
border-radius: .3em;
}
</style>

View file

@ -0,0 +1,209 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div
ref="rootEl"
:class="[$style.transitionRoot, (defaultStore.state.animation && $style.enableAnimation)]"
@touchstart="touchStart"
@touchmove="touchMove"
@touchend="touchEnd"
>
<Transition
:class="[$style.transitionChildren, { [$style.swiping]: isSwipingForClass }]"
:enterActiveClass="$style.swipeAnimation_enterActive"
:leaveActiveClass="$style.swipeAnimation_leaveActive"
:enterFromClass="transitionName === 'swipeAnimationLeft' ? $style.swipeAnimationLeft_enterFrom : $style.swipeAnimationRight_enterFrom"
:leaveToClass="transitionName === 'swipeAnimationLeft' ? $style.swipeAnimationLeft_leaveTo : $style.swipeAnimationRight_leaveTo"
:style="`--swipe: ${pullDistance}px;`"
>
<!-- 注意slot内の最上位要素に動的にkeyを設定すること -->
<!-- 各最上位要素にユニークなkeyの指定がないとTransitionがうまく動きません -->
<slot></slot>
</Transition>
</div>
</template>
<script lang="ts" setup>
import { ref, shallowRef, computed, nextTick, watch } from 'vue';
import type { Tab } from '@/components/global/MkPageHeader.tabs.vue';
import { defaultStore } from '@/store.js';
const rootEl = shallowRef<HTMLDivElement>();
// eslint-disable-next-line no-undef
const tabModel = defineModel<string>('tab');
const props = defineProps<{
tabs: Tab[];
}>();
const emit = defineEmits<{
(ev: 'swiped', newKey: string, direction: 'left' | 'right'): void;
}>();
// //
//
const MIN_SWIPE_DISTANCE = 50;
//
const SWIPE_DISTANCE_THRESHOLD = 125;
// Y
const SWIPE_ABORT_Y_THRESHOLD = 75;
//
const MAX_SWIPE_DISTANCE = 150;
// //
let startScreenX: number | null = null;
let startScreenY: number | null = null;
const currentTabIndex = computed(() => props.tabs.findIndex(tab => tab.key === tabModel.value));
const pullDistance = ref(0);
const isSwiping = ref(false);
const isSwipingForClass = ref(false);
let swipeAborted = false;
function touchStart(event: TouchEvent) {
if (!defaultStore.reactiveState.enableHorizontalSwipe.value) return;
if (event.touches.length !== 1) return;
startScreenX = event.touches[0].screenX;
startScreenY = event.touches[0].screenY;
}
function touchMove(event: TouchEvent) {
if (!defaultStore.reactiveState.enableHorizontalSwipe.value) return;
if (event.touches.length !== 1) return;
if (startScreenX == null || startScreenY == null) return;
if (swipeAborted) return;
let distanceX = event.touches[0].screenX - startScreenX;
let distanceY = event.touches[0].screenY - startScreenY;
if (Math.abs(distanceY) > SWIPE_ABORT_Y_THRESHOLD) {
swipeAborted = true;
pullDistance.value = 0;
isSwiping.value = false;
setTimeout(() => {
isSwipingForClass.value = false;
}, 400);
return;
}
if (Math.abs(distanceX) < MIN_SWIPE_DISTANCE) return;
if (Math.abs(distanceX) > MAX_SWIPE_DISTANCE) return;
if (currentTabIndex.value === 0 || props.tabs[currentTabIndex.value - 1].onClick) {
distanceX = Math.min(distanceX, 0);
}
if (currentTabIndex.value === props.tabs.length - 1 || props.tabs[currentTabIndex.value + 1].onClick) {
distanceX = Math.max(distanceX, 0);
}
if (distanceX === 0) return;
isSwiping.value = true;
isSwipingForClass.value = true;
nextTick(() => {
// 1.5px
if (Math.abs(distanceX - pullDistance.value) < 1.5) return;
pullDistance.value = distanceX;
});
}
function touchEnd(event: TouchEvent) {
if (swipeAborted) {
swipeAborted = false;
return;
}
if (!defaultStore.reactiveState.enableHorizontalSwipe.value) return;
if (event.touches.length !== 0) return;
if (startScreenX == null) return;
if (!isSwiping.value) return;
const distance = event.changedTouches[0].screenX - startScreenX;
if (Math.abs(distance) > SWIPE_DISTANCE_THRESHOLD) {
if (distance > 0) {
if (props.tabs[currentTabIndex.value - 1] && !props.tabs[currentTabIndex.value - 1].onClick) {
tabModel.value = props.tabs[currentTabIndex.value - 1].key;
emit('swiped', props.tabs[currentTabIndex.value - 1].key, 'right');
}
} else {
if (props.tabs[currentTabIndex.value + 1] && !props.tabs[currentTabIndex.value + 1].onClick) {
tabModel.value = props.tabs[currentTabIndex.value + 1].key;
emit('swiped', props.tabs[currentTabIndex.value + 1].key, 'left');
}
}
}
pullDistance.value = 0;
isSwiping.value = false;
setTimeout(() => {
isSwipingForClass.value = false;
}, 400);
}
const transitionName = ref<'swipeAnimationLeft' | 'swipeAnimationRight' | undefined>(undefined);
watch(tabModel, (newTab, oldTab) => {
const newIndex = props.tabs.findIndex(tab => tab.key === newTab);
const oldIndex = props.tabs.findIndex(tab => tab.key === oldTab);
if (oldIndex >= 0 && newIndex && oldIndex < newIndex) {
transitionName.value = 'swipeAnimationLeft';
} else {
transitionName.value = 'swipeAnimationRight';
}
window.setTimeout(() => {
transitionName.value = undefined;
}, 400);
});
</script>
<style lang="scss" module>
.transitionRoot.enableAnimation {
display: grid;
overflow: clip;
.transitionChildren {
grid-area: 1 / 1 / 2 / 2;
transform: translateX(var(--swipe));
&.swipeAnimation_enterActive,
&.swipeAnimation_leaveActive {
transition: transform .3s cubic-bezier(0.65, 0.05, 0.36, 1);
}
&.swipeAnimationRight_leaveTo,
&.swipeAnimationLeft_enterFrom {
transform: translateX(calc(100% + 24px));
}
&.swipeAnimationRight_enterFrom,
&.swipeAnimationLeft_leaveTo {
transform: translateX(calc(-100% - 24px));
}
}
}
.swiping {
transition: transform .2s ease-out;
}
</style>

View file

@ -138,7 +138,7 @@ const rangePercent = computed({
audioEl.value.currentTime = to * durationMs.value / 1000; audioEl.value.currentTime = to * durationMs.value / 1000;
}, },
}); });
const volume = ref(.5); const volume = ref(.25);
const bufferedEnd = ref(0); const bufferedEnd = ref(0);
const bufferedDataRatio = computed(() => { const bufferedDataRatio = computed(() => {
if (!audioEl.value) return 0; if (!audioEl.value) return 0;
@ -161,7 +161,7 @@ function togglePlayPause() {
function toggleMute() { function toggleMute() {
if (volume.value === 0) { if (volume.value === 0) {
volume.value = .5; volume.value = .25;
} else { } else {
volume.value = 0; volume.value = 0;
} }

View file

@ -5,9 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<!-- Media系専用のinput range --> <!-- Media系専用のinput range -->
<template> <template>
<div :class="$style.controlsSeekbar" :style="sliderBgWhite ? '--sliderBg: rgba(255,255,255,.25);' : '--sliderBg: var(--scrollbarHandle);'"> <div :style="sliderBgWhite ? '--sliderBg: rgba(255,255,255,.25);' : '--sliderBg: var(--scrollbarHandle);'">
<progress v-if="buffer !== undefined" :class="$style.buffer" :value="isNaN(buffer) ? 0 : buffer" min="0" max="1">{{ Math.round(buffer * 100) }}% buffered</progress> <div :class="$style.controlsSeekbar">
<input v-model="model" :class="$style.seek" :style="`--value: ${modelValue * 100}%;`" type="range" min="0" max="1" step="any" @change="emit('dragEnded', modelValue)"/> <progress v-if="buffer !== undefined" :class="$style.buffer" :value="isNaN(buffer) ? 0 : buffer" min="0" max="1">{{ Math.round(buffer * 100) }}% buffered</progress>
<input v-model="model" :class="$style.seek" :style="`--value: ${modelValue * 100}%;`" type="range" min="0" max="1" step="any" @change="emit('dragEnded', modelValue)"/>
</div>
</div> </div>
</template> </template>

View file

@ -176,7 +176,7 @@ const rangePercent = computed({
videoEl.value.currentTime = to * durationMs.value / 1000; videoEl.value.currentTime = to * durationMs.value / 1000;
}, },
}); });
const volume = ref(.5); const volume = ref(.25);
const bufferedEnd = ref(0); const bufferedEnd = ref(0);
const bufferedDataRatio = computed(() => { const bufferedDataRatio = computed(() => {
if (!videoEl.value) return 0; if (!videoEl.value) return 0;
@ -236,7 +236,7 @@ function toggleFullscreen() {
function toggleMute() { function toggleMute() {
if (volume.value === 0) { if (volume.value === 0) {
volume.value = .5; volume.value = .25;
} else { } else {
volume.value = 0; volume.value = 0;
} }
@ -535,6 +535,9 @@ onDeactivated(() => {
.seekbarRoot { .seekbarRoot {
grid-area: seekbar; grid-area: seekbar;
/* ▼シークバー操作をやりやすくするためにクリックイベントが伝播されないエリアを拡張する */
margin: -10px;
padding: 10px;
} }
@container (min-width: 500px) { @container (min-width: 500px) {

View file

@ -13,6 +13,7 @@ import MkMention from '@/components/MkMention.vue';
import MkEmoji from '@/components/global/MkEmoji.vue'; import MkEmoji from '@/components/global/MkEmoji.vue';
import MkCustomEmoji from '@/components/global/MkCustomEmoji.vue'; import MkCustomEmoji from '@/components/global/MkCustomEmoji.vue';
import MkCode from '@/components/MkCode.vue'; import MkCode from '@/components/MkCode.vue';
import MkCodeInline from '@/components/MkCodeInline.vue';
import MkGoogle from '@/components/MkGoogle.vue'; import MkGoogle from '@/components/MkGoogle.vue';
import MkSparkle from '@/components/MkSparkle.vue'; import MkSparkle from '@/components/MkSparkle.vue';
import MkA from '@/components/global/MkA.vue'; import MkA from '@/components/global/MkA.vue';
@ -373,10 +374,9 @@ export default function(props: MfmProps, context: SetupContext<MfmEvents>) {
} }
case 'inlineCode': { case 'inlineCode': {
return [h(MkCode, { return [h(MkCodeInline, {
key: Math.random(), key: Math.random(),
code: token.props.code, code: token.props.code,
inline: true,
})]; })];
} }

View file

@ -5,7 +5,7 @@
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
export function hms(ms: number, options: { export function hms(ms: number, options?: {
textFormat?: 'colon' | 'locale'; textFormat?: 'colon' | 'locale';
enableSeconds?: boolean; enableSeconds?: boolean;
enableMs?: boolean; enableMs?: boolean;

View file

@ -10,6 +10,7 @@ import { I18n } from '@/scripts/i18n.js';
export const i18n = markRaw(new I18n<Locale>(locale)); export const i18n = markRaw(new I18n<Locale>(locale));
export function updateI18n(newLocale) { export function updateI18n(newLocale: Locale) {
i18n.ts = newLocale; // @ts-expect-error -- private field
i18n.locale = newLocale;
} }

View file

@ -6,98 +6,100 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<MkStickyContainer> <MkStickyContainer>
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer v-if="tab === 'overview'" :contentMax="600" :marginMin="20"> <MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
<div class="_gaps_m"> <MkSpacer v-if="tab === 'overview'" :contentMax="600" :marginMin="20">
<div :class="$style.banner" :style="{ backgroundImage: `url(${ instance.bannerUrl })` }"> <div class="_gaps_m">
<div style="overflow: clip;"> <div :class="$style.banner" :style="{ backgroundImage: `url(${ instance.bannerUrl })` }">
<img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" alt="" :class="$style.bannerIcon"/> <div style="overflow: clip;">
<div :class="$style.bannerName"> <img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" alt="" :class="$style.bannerIcon"/>
<b>{{ instance.name ?? host }}</b> <div :class="$style.bannerName">
<b>{{ instance.name ?? host }}</b>
</div>
</div> </div>
</div> </div>
</div>
<MkKeyValue> <MkKeyValue>
<template #key>{{ i18n.ts.description }}</template> <template #key>{{ i18n.ts.description }}</template>
<template #value><div v-html="instance.description"></div></template> <template #value><div v-html="instance.description"></div></template>
</MkKeyValue> </MkKeyValue>
<FormSection>
<div class="_gaps_m">
<MkKeyValue :copy="version">
<template #key>Misskey</template>
<template #value>{{ version }}</template>
</MkKeyValue>
<div v-html="i18n.t('poweredByMisskeyDescription', { name: instance.name ?? host })">
</div>
<FormLink to="/about-misskey">{{ i18n.ts.aboutMisskey }}</FormLink>
</div>
</FormSection>
<FormSection>
<div class="_gaps_m">
<FormSplit>
<MkKeyValue>
<template #key>{{ i18n.ts.administrator }}</template>
<template #value>{{ instance.maintainerName }}</template>
</MkKeyValue>
<MkKeyValue>
<template #key>{{ i18n.ts.contact }}</template>
<template #value>{{ instance.maintainerEmail }}</template>
</MkKeyValue>
</FormSplit>
<FormLink v-if="instance.impressumUrl" :to="instance.impressumUrl" external>{{ i18n.ts.impressum }}</FormLink>
<div class="_gaps_s">
<MkFolder v-if="instance.serverRules.length > 0">
<template #label>{{ i18n.ts.serverRules }}</template>
<ol class="_gaps_s" :class="$style.rules">
<li v-for="item, index in instance.serverRules" :key="index" :class="$style.rule"><div :class="$style.ruleText" v-html="item"></div></li>
</ol>
</MkFolder>
<FormLink v-if="instance.tosUrl" :to="instance.tosUrl" external>{{ i18n.ts.termsOfService }}</FormLink>
<FormLink v-if="instance.privacyPolicyUrl" :to="instance.privacyPolicyUrl" external>{{ i18n.ts.privacyPolicy }}</FormLink>
</div>
</div>
</FormSection>
<FormSuspense :p="initStats">
<FormSection> <FormSection>
<template #label>{{ i18n.ts.statistics }}</template> <div class="_gaps_m">
<FormSplit> <MkKeyValue :copy="version">
<MkKeyValue> <template #key>Misskey</template>
<template #key>{{ i18n.ts.users }}</template> <template #value>{{ version }}</template>
<template #value>{{ number(stats.originalUsersCount) }}</template>
</MkKeyValue> </MkKeyValue>
<MkKeyValue> <div v-html="i18n.t('poweredByMisskeyDescription', { name: instance.name ?? host })">
<template #key>{{ i18n.ts.notes }}</template> </div>
<template #value>{{ number(stats.originalNotesCount) }}</template> <FormLink to="/about-misskey">{{ i18n.ts.aboutMisskey }}</FormLink>
</MkKeyValue> </div>
</FormSplit>
</FormSection> </FormSection>
</FormSuspense>
<FormSection> <FormSection>
<template #label>Well-known resources</template> <div class="_gaps_m">
<div class="_gaps_s"> <FormSplit>
<FormLink :to="`/.well-known/host-meta`" external>host-meta</FormLink> <MkKeyValue>
<FormLink :to="`/.well-known/host-meta.json`" external>host-meta.json</FormLink> <template #key>{{ i18n.ts.administrator }}</template>
<FormLink :to="`/.well-known/nodeinfo`" external>nodeinfo</FormLink> <template #value>{{ instance.maintainerName }}</template>
<FormLink :to="`/robots.txt`" external>robots.txt</FormLink> </MkKeyValue>
<FormLink :to="`/manifest.json`" external>manifest.json</FormLink> <MkKeyValue>
</div> <template #key>{{ i18n.ts.contact }}</template>
</FormSection> <template #value>{{ instance.maintainerEmail }}</template>
</div> </MkKeyValue>
</MkSpacer> </FormSplit>
<MkSpacer v-else-if="tab === 'emojis'" :contentMax="1000" :marginMin="20"> <FormLink v-if="instance.impressumUrl" :to="instance.impressumUrl" external>{{ i18n.ts.impressum }}</FormLink>
<XEmojis/> <div class="_gaps_s">
</MkSpacer> <MkFolder v-if="instance.serverRules.length > 0">
<MkSpacer v-else-if="tab === 'federation'" :contentMax="1000" :marginMin="20"> <template #label>{{ i18n.ts.serverRules }}</template>
<XFederation/>
</MkSpacer> <ol class="_gaps_s" :class="$style.rules">
<MkSpacer v-else-if="tab === 'charts'" :contentMax="1000" :marginMin="20"> <li v-for="item, index in instance.serverRules" :key="index" :class="$style.rule"><div :class="$style.ruleText" v-html="item"></div></li>
<MkInstanceStats/> </ol>
</MkSpacer> </MkFolder>
<FormLink v-if="instance.tosUrl" :to="instance.tosUrl" external>{{ i18n.ts.termsOfService }}</FormLink>
<FormLink v-if="instance.privacyPolicyUrl" :to="instance.privacyPolicyUrl" external>{{ i18n.ts.privacyPolicy }}</FormLink>
</div>
</div>
</FormSection>
<FormSuspense :p="initStats">
<FormSection>
<template #label>{{ i18n.ts.statistics }}</template>
<FormSplit>
<MkKeyValue>
<template #key>{{ i18n.ts.users }}</template>
<template #value>{{ number(stats.originalUsersCount) }}</template>
</MkKeyValue>
<MkKeyValue>
<template #key>{{ i18n.ts.notes }}</template>
<template #value>{{ number(stats.originalNotesCount) }}</template>
</MkKeyValue>
</FormSplit>
</FormSection>
</FormSuspense>
<FormSection>
<template #label>Well-known resources</template>
<div class="_gaps_s">
<FormLink :to="`/.well-known/host-meta`" external>host-meta</FormLink>
<FormLink :to="`/.well-known/host-meta.json`" external>host-meta.json</FormLink>
<FormLink :to="`/.well-known/nodeinfo`" external>nodeinfo</FormLink>
<FormLink :to="`/robots.txt`" external>robots.txt</FormLink>
<FormLink :to="`/manifest.json`" external>manifest.json</FormLink>
</div>
</FormSection>
</div>
</MkSpacer>
<MkSpacer v-else-if="tab === 'emojis'" :contentMax="1000" :marginMin="20">
<XEmojis/>
</MkSpacer>
<MkSpacer v-else-if="tab === 'federation'" :contentMax="1000" :marginMin="20">
<XFederation/>
</MkSpacer>
<MkSpacer v-else-if="tab === 'charts'" :contentMax="1000" :marginMin="20">
<MkInstanceStats/>
</MkSpacer>
</MkHorizontalSwipe>
</MkStickyContainer> </MkStickyContainer>
</template> </template>
@ -114,6 +116,7 @@ import FormSplit from '@/components/form/split.vue';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
import MkKeyValue from '@/components/MkKeyValue.vue'; import MkKeyValue from '@/components/MkKeyValue.vue';
import MkInstanceStats from '@/components/MkInstanceStats.vue'; import MkInstanceStats from '@/components/MkInstanceStats.vue';
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
import number from '@/filters/number.js'; import number from '@/filters/number.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';

View file

@ -7,34 +7,36 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkStickyContainer> <MkStickyContainer>
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="800"> <MkSpacer :contentMax="800">
<div class="_gaps"> <MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
<MkInfo v-if="$i && $i.hasUnreadAnnouncement && tab === 'current'" warn>{{ i18n.ts.youHaveUnreadAnnouncements }}</MkInfo> <div :key="tab" class="_gaps">
<MkPagination ref="paginationEl" :key="tab" v-slot="{items}" :pagination="tab === 'current' ? paginationCurrent : paginationPast" class="_gaps"> <MkInfo v-if="$i && $i.hasUnreadAnnouncement && tab === 'current'" warn>{{ i18n.ts.youHaveUnreadAnnouncements }}</MkInfo>
<section v-for="announcement in items" :key="announcement.id" class="_panel" :class="$style.announcement"> <MkPagination ref="paginationEl" :key="tab" v-slot="{items}" :pagination="tab === 'current' ? paginationCurrent : paginationPast" class="_gaps">
<div v-if="announcement.forYou" :class="$style.forYou"><i class="ti ti-pin"></i> {{ i18n.ts.forYou }}</div> <section v-for="announcement in items" :key="announcement.id" class="_panel" :class="$style.announcement">
<div :class="$style.header"> <div v-if="announcement.forYou" :class="$style.forYou"><i class="ti ti-pin"></i> {{ i18n.ts.forYou }}</div>
<span v-if="$i && !announcement.silence && !announcement.isRead" style="margin-right: 0.5em;">🆕</span> <div :class="$style.header">
<span style="margin-right: 0.5em;"> <span v-if="$i && !announcement.silence && !announcement.isRead" style="margin-right: 0.5em;">🆕</span>
<i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i> <span style="margin-right: 0.5em;">
<i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i> <i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i>
<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i> <i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i>
<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i> <i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i>
</span> <i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i>
<span>{{ announcement.title }}</span> </span>
</div> <span>{{ announcement.title }}</span>
<div :class="$style.content">
<Mfm :text="announcement.text"/>
<img v-if="announcement.imageUrl" :src="announcement.imageUrl"/>
<div style="opacity: 0.7; font-size: 85%;">
<MkTime :time="announcement.updatedAt ?? announcement.createdAt" mode="detail"/>
</div> </div>
</div> <div :class="$style.content">
<div v-if="tab !== 'past' && $i && !announcement.silence && !announcement.isRead" :class="$style.footer"> <Mfm :text="announcement.text"/>
<MkButton primary @click="read(announcement)"><i class="ti ti-check"></i> {{ i18n.ts.gotIt }}</MkButton> <img v-if="announcement.imageUrl" :src="announcement.imageUrl"/>
</div> <div style="opacity: 0.7; font-size: 85%;">
</section> <MkTime :time="announcement.updatedAt ?? announcement.createdAt" mode="detail"/>
</MkPagination> </div>
</div> </div>
<div v-if="tab !== 'past' && $i && !announcement.silence && !announcement.isRead" :class="$style.footer">
<MkButton primary @click="read(announcement)"><i class="ti ti-check"></i> {{ i18n.ts.gotIt }}</MkButton>
</div>
</section>
</MkPagination>
</div>
</MkHorizontalSwipe>
</MkSpacer> </MkSpacer>
</MkStickyContainer> </MkStickyContainer>
</template> </template>
@ -44,6 +46,7 @@ import { ref, computed } from 'vue';
import MkPagination from '@/components/MkPagination.vue'; import MkPagination from '@/components/MkPagination.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkInfo from '@/components/MkInfo.vue'; import MkInfo from '@/components/MkInfo.vue';
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';

View file

@ -7,53 +7,55 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkStickyContainer> <MkStickyContainer>
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="700" :class="$style.main"> <MkSpacer :contentMax="700" :class="$style.main">
<div v-if="channel && tab === 'overview'" class="_gaps"> <MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
<div class="_panel" :class="$style.bannerContainer"> <div v-if="channel && tab === 'overview'" key="overview" class="_gaps">
<XChannelFollowButton :channel="channel" :full="true" :class="$style.subscribe"/> <div class="_panel" :class="$style.bannerContainer">
<MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" asLike class="button" rounded primary :class="$style.favorite" @click="unfavorite()"><i class="ti ti-star"></i></MkButton> <XChannelFollowButton :channel="channel" :full="true" :class="$style.subscribe"/>
<MkButton v-else v-tooltip="i18n.ts.favorite" asLike class="button" rounded :class="$style.favorite" @click="favorite()"><i class="ti ti-star"></i></MkButton> <MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" asLike class="button" rounded primary :class="$style.favorite" @click="unfavorite()"><i class="ti ti-star"></i></MkButton>
<div :style="{ backgroundImage: channel.bannerUrl ? `url(${channel.bannerUrl})` : undefined }" :class="$style.banner"> <MkButton v-else v-tooltip="i18n.ts.favorite" asLike class="button" rounded :class="$style.favorite" @click="favorite()"><i class="ti ti-star"></i></MkButton>
<div :class="$style.bannerStatus"> <div :style="{ backgroundImage: channel.bannerUrl ? `url(${channel.bannerUrl})` : undefined }" :class="$style.banner">
<div><i class="ti ti-users ti-fw"></i><I18n :src="i18n.ts._channel.usersCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.usersCount }}</b></template></I18n></div> <div :class="$style.bannerStatus">
<div><i class="ti ti-pencil ti-fw"></i><I18n :src="i18n.ts._channel.notesCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.notesCount }}</b></template></I18n></div> <div><i class="ti ti-users ti-fw"></i><I18n :src="i18n.ts._channel.usersCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.usersCount }}</b></template></I18n></div>
<div><i class="ti ti-pencil ti-fw"></i><I18n :src="i18n.ts._channel.notesCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.notesCount }}</b></template></I18n></div>
</div>
<div v-if="channel.isSensitive" :class="$style.sensitiveIndicator">{{ i18n.ts.sensitive }}</div>
<div :class="$style.bannerFade"></div>
</div>
<div v-if="channel.description" :class="$style.description">
<Mfm :text="channel.description" :isNote="false"/>
</div> </div>
<div v-if="channel.isSensitive" :class="$style.sensitiveIndicator">{{ i18n.ts.sensitive }}</div>
<div :class="$style.bannerFade"></div>
</div> </div>
<div v-if="channel.description" :class="$style.description">
<Mfm :text="channel.description" :isNote="false"/> <MkFoldableSection>
<template #header><i class="ti ti-pin ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.pinnedNotes }}</template>
<div v-if="channel.pinnedNotes && channel.pinnedNotes.length > 0" class="_gaps">
<MkNote v-for="note in channel.pinnedNotes" :key="note.id" class="_panel" :note="note"/>
</div>
</MkFoldableSection>
</div>
<div v-if="channel && tab === 'timeline'" key="timeline" class="_gaps">
<MkInfo v-if="channel.isArchived" warn>{{ i18n.ts.thisChannelArchived }}</MkInfo>
<!-- スマホタブレットの場合キーボードが表示されると投稿が見づらくなるのでデスクトップ場合のみ自動でフォーカスを当てる -->
<MkPostForm v-if="$i && defaultStore.reactiveState.showFixedPostFormInChannel.value" :channel="channel" class="post-form _panel" fixed :autofocus="deviceKind === 'desktop'"/>
<MkTimeline :key="channelId" src="channel" :channel="channelId" @before="before" @after="after" @note="miLocalStorage.setItemAsJson(`channelLastReadedAt:${channel.id}`, Date.now())"/>
</div>
<div v-else-if="tab === 'featured'" key="featured">
<MkNotes :pagination="featuredPagination"/>
</div>
<div v-else-if="tab === 'search'" key="search">
<div class="_gaps">
<div>
<MkInput v-model="searchQuery" @enter="search()">
<template #prefix><i class="ti ti-search"></i></template>
</MkInput>
<MkButton primary rounded style="margin-top: 8px;" @click="search()">{{ i18n.ts.search }}</MkButton>
</div>
<MkNotes v-if="searchPagination" :key="searchKey" :pagination="searchPagination"/>
</div> </div>
</div> </div>
</MkHorizontalSwipe>
<MkFoldableSection>
<template #header><i class="ti ti-pin ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.pinnedNotes }}</template>
<div v-if="channel.pinnedNotes && channel.pinnedNotes.length > 0" class="_gaps">
<MkNote v-for="note in channel.pinnedNotes" :key="note.id" class="_panel" :note="note"/>
</div>
</MkFoldableSection>
</div>
<div v-if="channel && tab === 'timeline'" class="_gaps">
<MkInfo v-if="channel.isArchived" warn>{{ i18n.ts.thisChannelArchived }}</MkInfo>
<!-- スマホタブレットの場合キーボードが表示されると投稿が見づらくなるのでデスクトップ場合のみ自動でフォーカスを当てる -->
<MkPostForm v-if="$i && defaultStore.reactiveState.showFixedPostFormInChannel.value" :channel="channel" class="post-form _panel" fixed :autofocus="deviceKind === 'desktop'"/>
<MkTimeline :key="channelId" src="channel" :channel="channelId" @before="before" @after="after" @note="miLocalStorage.setItemAsJson(`channelLastReadedAt:${channel.id}`, Date.now())"/>
</div>
<div v-else-if="tab === 'featured'">
<MkNotes :pagination="featuredPagination"/>
</div>
<div v-else-if="tab === 'search'">
<div class="_gaps">
<div>
<MkInput v-model="searchQuery" @enter="search()">
<template #prefix><i class="ti ti-search"></i></template>
</MkInput>
<MkButton primary rounded style="margin-top: 8px;" @click="search()">{{ i18n.ts.search }}</MkButton>
</div>
<MkNotes v-if="searchPagination" :key="searchKey" :pagination="searchPagination"/>
</div>
</div>
</MkSpacer> </MkSpacer>
<template #footer> <template #footer>
<div :class="$style.footer"> <div :class="$style.footer">
@ -87,6 +89,7 @@ import { defaultStore } from '@/store.js';
import MkNote from '@/components/MkNote.vue'; import MkNote from '@/components/MkNote.vue';
import MkInfo from '@/components/MkInfo.vue'; import MkInfo from '@/components/MkInfo.vue';
import MkFoldableSection from '@/components/MkFoldableSection.vue'; import MkFoldableSection from '@/components/MkFoldableSection.vue';
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
import { PageHeaderItem } from '@/types/page-header.js'; import { PageHeaderItem } from '@/types/page-header.js';
import { isSupportShare } from '@/scripts/navigator.js'; import { isSupportShare } from '@/scripts/navigator.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import copyToClipboard from '@/scripts/copy-to-clipboard.js';
@ -100,6 +103,7 @@ const props = defineProps<{
}>(); }>();
const tab = ref('overview'); const tab = ref('overview');
const channel = ref<Misskey.entities.Channel | null>(null); const channel = ref<Misskey.entities.Channel | null>(null);
const favorited = ref(false); const favorited = ref(false);
const searchQuery = ref(''); const searchQuery = ref('');

View file

@ -7,44 +7,46 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkStickyContainer> <MkStickyContainer>
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="700"> <MkSpacer :contentMax="700">
<div v-if="tab === 'search'"> <MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
<div class="_gaps"> <div v-if="tab === 'search'" key="search">
<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @enter="search"> <div class="_gaps">
<template #prefix><i class="ti ti-search"></i></template> <MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @enter="search">
</MkInput> <template #prefix><i class="ti ti-search"></i></template>
<MkRadios v-model="searchType" @update:modelValue="search()"> </MkInput>
<option value="nameAndDescription">{{ i18n.ts._channel.nameAndDescription }}</option> <MkRadios v-model="searchType" @update:modelValue="search()">
<option value="nameOnly">{{ i18n.ts._channel.nameOnly }}</option> <option value="nameAndDescription">{{ i18n.ts._channel.nameAndDescription }}</option>
</MkRadios> <option value="nameOnly">{{ i18n.ts._channel.nameOnly }}</option>
<MkButton large primary gradate rounded @click="search">{{ i18n.ts.search }}</MkButton> </MkRadios>
</div> <MkButton large primary gradate rounded @click="search">{{ i18n.ts.search }}</MkButton>
</div>
<MkFoldableSection v-if="channelPagination"> <MkFoldableSection v-if="channelPagination">
<template #header>{{ i18n.ts.searchResult }}</template> <template #header>{{ i18n.ts.searchResult }}</template>
<MkChannelList :key="key" :pagination="channelPagination"/> <MkChannelList :key="key" :pagination="channelPagination"/>
</MkFoldableSection> </MkFoldableSection>
</div> </div>
<div v-if="tab === 'featured'"> <div v-if="tab === 'featured'" key="featured">
<MkPagination v-slot="{items}" :pagination="featuredPagination"> <MkPagination v-slot="{items}" :pagination="featuredPagination">
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/> <MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/>
</MkPagination> </MkPagination>
</div> </div>
<div v-else-if="tab === 'favorites'"> <div v-else-if="tab === 'favorites'" key="favorites">
<MkPagination v-slot="{items}" :pagination="favoritesPagination"> <MkPagination v-slot="{items}" :pagination="favoritesPagination">
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/> <MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/>
</MkPagination> </MkPagination>
</div> </div>
<div v-else-if="tab === 'following'"> <div v-else-if="tab === 'following'" key="following">
<MkPagination v-slot="{items}" :pagination="followingPagination"> <MkPagination v-slot="{items}" :pagination="followingPagination">
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/> <MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/>
</MkPagination> </MkPagination>
</div> </div>
<div v-else-if="tab === 'owned'"> <div v-else-if="tab === 'owned'" key="owned">
<MkButton class="new" @click="create()"><i class="ti ti-plus"></i></MkButton> <MkButton class="new" @click="create()"><i class="ti ti-plus"></i></MkButton>
<MkPagination v-slot="{items}" :pagination="ownedPagination"> <MkPagination v-slot="{items}" :pagination="ownedPagination">
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/> <MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/>
</MkPagination> </MkPagination>
</div> </div>
</MkHorizontalSwipe>
</MkSpacer> </MkSpacer>
</MkStickyContainer> </MkStickyContainer>
</template> </template>
@ -58,6 +60,7 @@ import MkInput from '@/components/MkInput.vue';
import MkRadios from '@/components/MkRadios.vue'; import MkRadios from '@/components/MkRadios.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkFoldableSection from '@/components/MkFoldableSection.vue'; import MkFoldableSection from '@/components/MkFoldableSection.vue';
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
import { definePageMetadata } from '@/scripts/page-metadata.js'; import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { useRouter } from '@/global/router/supplier.js'; import { useRouter } from '@/global/router/supplier.js';

View file

@ -9,13 +9,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/> <MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/>
</template> </template>
<MkSpacer v-if="tab === 'info'" :contentMax="800"> <MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
<XFileInfo :fileId="fileId"/> <MkSpacer v-if="tab === 'info'" key="info" :contentMax="800">
</MkSpacer> <XFileInfo :fileId="fileId"/>
</MkSpacer>
<MkSpacer v-else-if="tab === 'notes'" :contentMax="800"> <MkSpacer v-else-if="tab === 'notes'" key="notes" :contentMax="800">
<XNotes :fileId="fileId"/> <XNotes :fileId="fileId"/>
</MkSpacer> </MkSpacer>
</MkHorizontalSwipe>
</MkStickyContainer> </MkStickyContainer>
</template> </template>
@ -23,6 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { computed, ref, defineAsyncComponent } from 'vue'; import { computed, ref, defineAsyncComponent } from 'vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js'; import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
const props = defineProps<{ const props = defineProps<{
fileId: string; fileId: string;

View file

@ -6,17 +6,17 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<MkStickyContainer> <MkStickyContainer>
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
<div> <MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
<div v-if="tab === 'featured'"> <div v-if="tab === 'featured'" key="featured">
<XFeatured/> <XFeatured/>
</div> </div>
<div v-else-if="tab === 'users'"> <div v-else-if="tab === 'users'" key="users">
<XUsers/> <XUsers/>
</div> </div>
<div v-else-if="tab === 'roles'"> <div v-else-if="tab === 'roles'" key="roles">
<XRoles/> <XRoles/>
</div> </div>
</div> </MkHorizontalSwipe>
</MkStickyContainer> </MkStickyContainer>
</template> </template>
@ -26,6 +26,7 @@ import XFeatured from './explore.featured.vue';
import XUsers from './explore.users.vue'; import XUsers from './explore.users.vue';
import XRoles from './explore.roles.vue'; import XRoles from './explore.roles.vue';
import MkFoldableSection from '@/components/MkFoldableSection.vue'; import MkFoldableSection from '@/components/MkFoldableSection.vue';
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
import { definePageMetadata } from '@/scripts/page-metadata.js'; import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';

View file

@ -7,32 +7,34 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkStickyContainer> <MkStickyContainer>
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="700"> <MkSpacer :contentMax="700">
<div v-if="tab === 'featured'"> <MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
<MkPagination v-slot="{items}" :pagination="featuredFlashsPagination"> <div v-if="tab === 'featured'" key="featured">
<div class="_gaps_s"> <MkPagination v-slot="{items}" :pagination="featuredFlashsPagination">
<MkFlashPreview v-for="flash in items" :key="flash.id" :flash="flash"/>
</div>
</MkPagination>
</div>
<div v-else-if="tab === 'my'">
<div class="_gaps">
<MkButton gradate rounded style="margin: 0 auto;" @click="create()"><i class="ti ti-plus"></i></MkButton>
<MkPagination v-slot="{items}" :pagination="myFlashsPagination">
<div class="_gaps_s"> <div class="_gaps_s">
<MkFlashPreview v-for="flash in items" :key="flash.id" :flash="flash"/> <MkFlashPreview v-for="flash in items" :key="flash.id" :flash="flash"/>
</div> </div>
</MkPagination> </MkPagination>
</div> </div>
</div>
<div v-else-if="tab === 'liked'"> <div v-else-if="tab === 'my'" key="my">
<MkPagination v-slot="{items}" :pagination="likedFlashsPagination"> <div class="_gaps">
<div class="_gaps_s"> <MkButton gradate rounded style="margin: 0 auto;" @click="create()"><i class="ti ti-plus"></i></MkButton>
<MkFlashPreview v-for="like in items" :key="like.flash.id" :flash="like.flash"/> <MkPagination v-slot="{items}" :pagination="myFlashsPagination">
<div class="_gaps_s">
<MkFlashPreview v-for="flash in items" :key="flash.id" :flash="flash"/>
</div>
</MkPagination>
</div> </div>
</MkPagination> </div>
</div>
<div v-else-if="tab === 'liked'" key="liked">
<MkPagination v-slot="{items}" :pagination="likedFlashsPagination">
<div class="_gaps_s">
<MkFlashPreview v-for="like in items" :key="like.flash.id" :flash="like.flash"/>
</div>
</MkPagination>
</div>
</MkHorizontalSwipe>
</MkSpacer> </MkSpacer>
</MkStickyContainer> </MkStickyContainer>
</template> </template>
@ -42,6 +44,7 @@ import { computed, ref } from 'vue';
import MkFlashPreview from '@/components/MkFlashPreview.vue'; import MkFlashPreview from '@/components/MkFlashPreview.vue';
import MkPagination from '@/components/MkPagination.vue'; import MkPagination from '@/components/MkPagination.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js'; import { definePageMetadata } from '@/scripts/page-metadata.js';
import { useRouter } from '@/global/router/supplier.js'; import { useRouter } from '@/global/router/supplier.js';

View file

@ -37,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #icon><i class="ti ti-code"></i></template> <template #icon><i class="ti ti-code"></i></template>
<template #label>{{ i18n.ts._play.viewSource }}</template> <template #label>{{ i18n.ts._play.viewSource }}</template>
<MkCode :code="flash.script" lang="is" :inline="false" class="_monospace"/> <MkCode :code="flash.script" lang="is" class="_monospace"/>
</MkFolder> </MkFolder>
<div :class="$style.footer"> <div :class="$style.footer">
<Mfm :text="`By @${flash.user.username}`"/> <Mfm :text="`By @${flash.user.username}`"/>

View file

@ -7,8 +7,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkStickyContainer> <MkStickyContainer>
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="1400"> <MkSpacer :contentMax="1400">
<div class="_root"> <MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
<div v-if="tab === 'explore'"> <div v-if="tab === 'explore'" key="explore">
<MkFoldableSection class="_margin"> <MkFoldableSection class="_margin">
<template #header><i class="ti ti-clock"></i>{{ i18n.ts.recentPosts }}</template> <template #header><i class="ti ti-clock"></i>{{ i18n.ts.recentPosts }}</template>
<MkPagination v-slot="{items}" :pagination="recentPostsPagination" :disableAutoLoad="true"> <MkPagination v-slot="{items}" :pagination="recentPostsPagination" :disableAutoLoad="true">
@ -26,14 +26,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkPagination> </MkPagination>
</MkFoldableSection> </MkFoldableSection>
</div> </div>
<div v-else-if="tab === 'liked'"> <div v-else-if="tab === 'liked'" key="liked">
<MkPagination v-slot="{items}" :pagination="likedPostsPagination"> <MkPagination v-slot="{items}" :pagination="likedPostsPagination">
<div :class="$style.items"> <div :class="$style.items">
<MkGalleryPostPreview v-for="like in items" :key="like.id" :post="like.post" class="post"/> <MkGalleryPostPreview v-for="like in items" :key="like.id" :post="like.post" class="post"/>
</div> </div>
</MkPagination> </MkPagination>
</div> </div>
<div v-else-if="tab === 'my'"> <div v-else-if="tab === 'my'" key="my">
<MkA to="/gallery/new" class="_link" style="margin: 16px;"><i class="ti ti-plus"></i> {{ i18n.ts.postToGallery }}</MkA> <MkA to="/gallery/new" class="_link" style="margin: 16px;"><i class="ti ti-plus"></i> {{ i18n.ts.postToGallery }}</MkA>
<MkPagination v-slot="{items}" :pagination="myPostsPagination"> <MkPagination v-slot="{items}" :pagination="myPostsPagination">
<div :class="$style.items"> <div :class="$style.items">
@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</MkPagination> </MkPagination>
</div> </div>
</div> </MkHorizontalSwipe>
</MkSpacer> </MkSpacer>
</MkStickyContainer> </MkStickyContainer>
</template> </template>
@ -51,6 +51,7 @@ import { watch, ref, computed } from 'vue';
import MkFoldableSection from '@/components/MkFoldableSection.vue'; import MkFoldableSection from '@/components/MkFoldableSection.vue';
import MkPagination from '@/components/MkPagination.vue'; import MkPagination from '@/components/MkPagination.vue';
import MkGalleryPostPreview from '@/components/MkGalleryPostPreview.vue'; import MkGalleryPostPreview from '@/components/MkGalleryPostPreview.vue';
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
import { definePageMetadata } from '@/scripts/page-metadata.js'; import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { useRouter } from '@/global/router/supplier.js'; import { useRouter } from '@/global/router/supplier.js';

View file

@ -7,111 +7,113 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkStickyContainer> <MkStickyContainer>
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer v-if="instance" :contentMax="600" :marginMin="16" :marginMax="32"> <MkSpacer v-if="instance" :contentMax="600" :marginMin="16" :marginMax="32">
<div v-if="tab === 'overview'" class="_gaps_m"> <MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
<div class="fnfelxur"> <div v-if="tab === 'overview'" key="overview" class="_gaps_m">
<img :src="faviconUrl" alt="" class="icon"/> <div class="fnfelxur">
<span class="name">{{ instance.name || `(${i18n.ts.unknown})` }}</span> <img :src="faviconUrl" alt="" class="icon"/>
</div> <span class="name">{{ instance.name || `(${i18n.ts.unknown})` }}</span>
<div style="display: flex; flex-direction: column; gap: 1em;">
<MkKeyValue :copy="host" oneline>
<template #key>Host</template>
<template #value><span class="_monospace"><MkLink :url="`https://${host}`">{{ host }}</MkLink></span></template>
</MkKeyValue>
<MkKeyValue oneline>
<template #key>{{ i18n.ts.software }}</template>
<template #value><span class="_monospace">{{ instance.softwareName || `(${i18n.ts.unknown})` }} / {{ instance.softwareVersion || `(${i18n.ts.unknown})` }}</span></template>
</MkKeyValue>
<MkKeyValue oneline>
<template #key>{{ i18n.ts.administrator }}</template>
<template #value>{{ instance.maintainerName || `(${i18n.ts.unknown})` }} ({{ instance.maintainerEmail || `(${i18n.ts.unknown})` }})</template>
</MkKeyValue>
</div>
<MkKeyValue>
<template #key>{{ i18n.ts.description }}</template>
<template #value>{{ instance.description }}</template>
</MkKeyValue>
<FormSection v-if="iAmModerator">
<template #label>Moderation</template>
<div class="_gaps_s">
<MkSwitch v-model="suspended" :disabled="!instance" @update:modelValue="toggleSuspend">{{ i18n.ts.stopActivityDelivery }}</MkSwitch>
<MkSwitch v-model="isBlocked" :disabled="!meta || !instance" @update:modelValue="toggleBlock">{{ i18n.ts.blockThisInstance }}</MkSwitch>
<MkSwitch v-model="isSilenced" :disabled="!meta || !instance" @update:modelValue="toggleSilenced">{{ i18n.ts.silenceThisInstance }}</MkSwitch>
<MkButton @click="refreshMetadata"><i class="ti ti-refresh"></i> Refresh metadata</MkButton>
</div> </div>
</FormSection> <div style="display: flex; flex-direction: column; gap: 1em;">
<MkKeyValue :copy="host" oneline>
<FormSection> <template #key>Host</template>
<MkKeyValue oneline style="margin: 1em 0;"> <template #value><span class="_monospace"><MkLink :url="`https://${host}`">{{ host }}</MkLink></span></template>
<template #key>{{ i18n.ts.registeredAt }}</template> </MkKeyValue>
<template #value><MkTime mode="detail" :time="instance.firstRetrievedAt"/></template> <MkKeyValue oneline>
</MkKeyValue> <template #key>{{ i18n.ts.software }}</template>
<MkKeyValue oneline style="margin: 1em 0;"> <template #value><span class="_monospace">{{ instance.softwareName || `(${i18n.ts.unknown})` }} / {{ instance.softwareVersion || `(${i18n.ts.unknown})` }}</span></template>
<template #key>{{ i18n.ts.updatedAt }}</template> </MkKeyValue>
<template #value><MkTime mode="detail" :time="instance.infoUpdatedAt"/></template> <MkKeyValue oneline>
</MkKeyValue> <template #key>{{ i18n.ts.administrator }}</template>
<MkKeyValue oneline style="margin: 1em 0;"> <template #value>{{ instance.maintainerName || `(${i18n.ts.unknown})` }} ({{ instance.maintainerEmail || `(${i18n.ts.unknown})` }})</template>
<template #key>{{ i18n.ts.latestRequestReceivedAt }}</template> </MkKeyValue>
<template #value><MkTime v-if="instance.latestRequestReceivedAt" :time="instance.latestRequestReceivedAt"/><span v-else>N/A</span></template>
</MkKeyValue>
</FormSection>
<FormSection>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>Following (Pub)</template>
<template #value>{{ number(instance.followingCount) }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>Followers (Sub)</template>
<template #value>{{ number(instance.followersCount) }}</template>
</MkKeyValue>
</FormSection>
<FormSection>
<template #label>Well-known resources</template>
<FormLink :to="`https://${host}/.well-known/host-meta`" external style="margin-bottom: 8px;">host-meta</FormLink>
<FormLink :to="`https://${host}/.well-known/host-meta.json`" external style="margin-bottom: 8px;">host-meta.json</FormLink>
<FormLink :to="`https://${host}/.well-known/nodeinfo`" external style="margin-bottom: 8px;">nodeinfo</FormLink>
<FormLink :to="`https://${host}/robots.txt`" external style="margin-bottom: 8px;">robots.txt</FormLink>
<FormLink :to="`https://${host}/manifest.json`" external style="margin-bottom: 8px;">manifest.json</FormLink>
</FormSection>
</div>
<div v-else-if="tab === 'chart'" class="_gaps_m">
<div class="cmhjzshl">
<div class="selects">
<MkSelect v-model="chartSrc" style="margin: 0 10px 0 0; flex: 1;">
<option value="instance-requests">{{ i18n.ts._instanceCharts.requests }}</option>
<option value="instance-users">{{ i18n.ts._instanceCharts.users }}</option>
<option value="instance-users-total">{{ i18n.ts._instanceCharts.usersTotal }}</option>
<option value="instance-notes">{{ i18n.ts._instanceCharts.notes }}</option>
<option value="instance-notes-total">{{ i18n.ts._instanceCharts.notesTotal }}</option>
<option value="instance-ff">{{ i18n.ts._instanceCharts.ff }}</option>
<option value="instance-ff-total">{{ i18n.ts._instanceCharts.ffTotal }}</option>
<option value="instance-drive-usage">{{ i18n.ts._instanceCharts.cacheSize }}</option>
<option value="instance-drive-usage-total">{{ i18n.ts._instanceCharts.cacheSizeTotal }}</option>
<option value="instance-drive-files">{{ i18n.ts._instanceCharts.files }}</option>
<option value="instance-drive-files-total">{{ i18n.ts._instanceCharts.filesTotal }}</option>
</MkSelect>
</div> </div>
<div class="charts"> <MkKeyValue>
<div class="label">{{ i18n.t('recentNHours', { n: 90 }) }}</div> <template #key>{{ i18n.ts.description }}</template>
<MkChart class="chart" :src="chartSrc" span="hour" :limit="90" :args="{ host: host }" :detailed="true"></MkChart> <template #value>{{ instance.description }}</template>
<div class="label">{{ i18n.t('recentNDays', { n: 90 }) }}</div> </MkKeyValue>
<MkChart class="chart" :src="chartSrc" span="day" :limit="90" :args="{ host: host }" :detailed="true"></MkChart>
<FormSection v-if="iAmModerator">
<template #label>Moderation</template>
<div class="_gaps_s">
<MkSwitch v-model="suspended" :disabled="!instance" @update:modelValue="toggleSuspend">{{ i18n.ts.stopActivityDelivery }}</MkSwitch>
<MkSwitch v-model="isBlocked" :disabled="!meta || !instance" @update:modelValue="toggleBlock">{{ i18n.ts.blockThisInstance }}</MkSwitch>
<MkSwitch v-model="isSilenced" :disabled="!meta || !instance" @update:modelValue="toggleSilenced">{{ i18n.ts.silenceThisInstance }}</MkSwitch>
<MkButton @click="refreshMetadata"><i class="ti ti-refresh"></i> Refresh metadata</MkButton>
</div>
</FormSection>
<FormSection>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ i18n.ts.registeredAt }}</template>
<template #value><MkTime mode="detail" :time="instance.firstRetrievedAt"/></template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ i18n.ts.updatedAt }}</template>
<template #value><MkTime mode="detail" :time="instance.infoUpdatedAt"/></template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ i18n.ts.latestRequestReceivedAt }}</template>
<template #value><MkTime v-if="instance.latestRequestReceivedAt" :time="instance.latestRequestReceivedAt"/><span v-else>N/A</span></template>
</MkKeyValue>
</FormSection>
<FormSection>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>Following (Pub)</template>
<template #value>{{ number(instance.followingCount) }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>Followers (Sub)</template>
<template #value>{{ number(instance.followersCount) }}</template>
</MkKeyValue>
</FormSection>
<FormSection>
<template #label>Well-known resources</template>
<FormLink :to="`https://${host}/.well-known/host-meta`" external style="margin-bottom: 8px;">host-meta</FormLink>
<FormLink :to="`https://${host}/.well-known/host-meta.json`" external style="margin-bottom: 8px;">host-meta.json</FormLink>
<FormLink :to="`https://${host}/.well-known/nodeinfo`" external style="margin-bottom: 8px;">nodeinfo</FormLink>
<FormLink :to="`https://${host}/robots.txt`" external style="margin-bottom: 8px;">robots.txt</FormLink>
<FormLink :to="`https://${host}/manifest.json`" external style="margin-bottom: 8px;">manifest.json</FormLink>
</FormSection>
</div>
<div v-else-if="tab === 'chart'" key="chart" class="_gaps_m">
<div class="cmhjzshl">
<div class="selects">
<MkSelect v-model="chartSrc" style="margin: 0 10px 0 0; flex: 1;">
<option value="instance-requests">{{ i18n.ts._instanceCharts.requests }}</option>
<option value="instance-users">{{ i18n.ts._instanceCharts.users }}</option>
<option value="instance-users-total">{{ i18n.ts._instanceCharts.usersTotal }}</option>
<option value="instance-notes">{{ i18n.ts._instanceCharts.notes }}</option>
<option value="instance-notes-total">{{ i18n.ts._instanceCharts.notesTotal }}</option>
<option value="instance-ff">{{ i18n.ts._instanceCharts.ff }}</option>
<option value="instance-ff-total">{{ i18n.ts._instanceCharts.ffTotal }}</option>
<option value="instance-drive-usage">{{ i18n.ts._instanceCharts.cacheSize }}</option>
<option value="instance-drive-usage-total">{{ i18n.ts._instanceCharts.cacheSizeTotal }}</option>
<option value="instance-drive-files">{{ i18n.ts._instanceCharts.files }}</option>
<option value="instance-drive-files-total">{{ i18n.ts._instanceCharts.filesTotal }}</option>
</MkSelect>
</div>
<div class="charts">
<div class="label">{{ i18n.t('recentNHours', { n: 90 }) }}</div>
<MkChart class="chart" :src="chartSrc" span="hour" :limit="90" :args="{ host: host }" :detailed="true"></MkChart>
<div class="label">{{ i18n.t('recentNDays', { n: 90 }) }}</div>
<MkChart class="chart" :src="chartSrc" span="day" :limit="90" :args="{ host: host }" :detailed="true"></MkChart>
</div>
</div> </div>
</div> </div>
</div> <div v-else-if="tab === 'users'" key="users" class="_gaps_m">
<div v-else-if="tab === 'users'" class="_gaps_m"> <MkPagination v-slot="{items}" :pagination="usersPagination" style="display: grid; grid-template-columns: repeat(auto-fill,minmax(270px,1fr)); grid-gap: 12px;">
<MkPagination v-slot="{items}" :pagination="usersPagination" style="display: grid; grid-template-columns: repeat(auto-fill,minmax(270px,1fr)); grid-gap: 12px;"> <MkA v-for="user in items" :key="user.id" v-tooltip.mfm="`Last posted: ${dateString(user.updatedAt)}`" class="user" :to="`/admin/user/${user.id}`">
<MkA v-for="user in items" :key="user.id" v-tooltip.mfm="`Last posted: ${dateString(user.updatedAt)}`" class="user" :to="`/admin/user/${user.id}`"> <MkUserCardMini :user="user"/>
<MkUserCardMini :user="user"/> </MkA>
</MkA> </MkPagination>
</MkPagination> </div>
</div> <div v-else-if="tab === 'raw'" key="raw" class="_gaps_m">
<div v-else-if="tab === 'raw'" class="_gaps_m"> <MkObjectView tall :value="instance">
<MkObjectView tall :value="instance"> </MkObjectView>
</MkObjectView> </div>
</div> </MkHorizontalSwipe>
</MkSpacer> </MkSpacer>
</MkStickyContainer> </MkStickyContainer>
</template> </template>
@ -136,6 +138,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import MkUserCardMini from '@/components/MkUserCardMini.vue'; import MkUserCardMini from '@/components/MkUserCardMini.vue';
import MkPagination from '@/components/MkPagination.vue'; import MkPagination from '@/components/MkPagination.vue';
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js'; import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
import { dateString } from '@/filters/date.js'; import { dateString } from '@/filters/date.js';
@ -144,6 +147,7 @@ const props = defineProps<{
}>(); }>();
const tab = ref('overview'); const tab = ref('overview');
const chartSrc = ref('instance-requests'); const chartSrc = ref('instance-requests');
const meta = ref<Misskey.entities.AdminMetaResponse | null>(null); const meta = ref<Misskey.entities.AdminMetaResponse | null>(null);
const instance = ref<Misskey.entities.FederationInstance | null>(null); const instance = ref<Misskey.entities.FederationInstance | null>(null);

View file

@ -7,20 +7,22 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkStickyContainer> <MkStickyContainer>
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="700"> <MkSpacer :contentMax="700">
<div v-if="tab === 'my'" class="_gaps"> <MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
<MkButton primary rounded class="add" @click="create"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton> <div v-if="tab === 'my'" key="my" class="_gaps">
<MkButton primary rounded class="add" @click="create"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
<MkPagination v-slot="{items}" ref="pagingComponent" :pagination="pagination" class="_gaps"> <MkPagination v-slot="{items}" ref="pagingComponent" :pagination="pagination" class="_gaps">
<MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`"> <MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`">
<MkClipPreview :clip="item"/>
</MkA>
</MkPagination>
</div>
<div v-else-if="tab === 'favorites'" key="favorites" class="_gaps">
<MkA v-for="item in favorites" :key="item.id" :to="`/clips/${item.id}`">
<MkClipPreview :clip="item"/> <MkClipPreview :clip="item"/>
</MkA> </MkA>
</MkPagination> </div>
</div> </MkHorizontalSwipe>
<div v-else-if="tab === 'favorites'" class="_gaps">
<MkA v-for="item in favorites" :key="item.id" :to="`/clips/${item.id}`">
<MkClipPreview :clip="item"/>
</MkA>
</div>
</MkSpacer> </MkSpacer>
</MkStickyContainer> </MkStickyContainer>
</template> </template>
@ -36,6 +38,7 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js'; import { definePageMetadata } from '@/scripts/page-metadata.js';
import { clipsCache } from '@/cache.js'; import { clipsCache } from '@/cache.js';
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
const pagination = { const pagination = {
endpoint: 'clips/list' as const, endpoint: 'clips/list' as const,
@ -44,6 +47,7 @@ const pagination = {
}; };
const tab = ref('my'); const tab = ref('my');
const favorites = ref<Misskey.entities.Clip[] | null>(null); const favorites = ref<Misskey.entities.Clip[] | null>(null);
const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>(); const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();

View file

@ -7,15 +7,17 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkStickyContainer> <MkStickyContainer>
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="800"> <MkSpacer :contentMax="800">
<div v-if="tab === 'all'"> <MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
<XNotifications class="notifications" :excludeTypes="excludeTypes"/> <div v-if="tab === 'all'" key="all">
</div> <XNotifications :class="$style.notifications" :excludeTypes="excludeTypes"/>
<div v-else-if="tab === 'mentions'"> </div>
<MkNotes :pagination="mentionsPagination"/> <div v-else-if="tab === 'mentions'" key="mention">
</div> <MkNotes :pagination="mentionsPagination"/>
<div v-else-if="tab === 'directNotes'"> </div>
<MkNotes :pagination="directNotesPagination"/> <div v-else-if="tab === 'directNotes'" key="directNotes">
</div> <MkNotes :pagination="directNotesPagination"/>
</div>
</MkHorizontalSwipe>
</MkSpacer> </MkSpacer>
</MkStickyContainer> </MkStickyContainer>
</template> </template>
@ -24,6 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import XNotifications from '@/components/MkNotifications.vue'; import XNotifications from '@/components/MkNotifications.vue';
import MkNotes from '@/components/MkNotes.vue'; import MkNotes from '@/components/MkNotes.vue';
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js'; import { definePageMetadata } from '@/scripts/page-metadata.js';
@ -96,3 +99,10 @@ definePageMetadata(computed(() => ({
icon: 'ti ti-bell', icon: 'ti ti-bell',
}))); })));
</script> </script>
<style module lang="scss">
.notifications {
border-radius: var(--radius);
overflow: clip;
}
</style>

View file

@ -7,30 +7,32 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkStickyContainer> <MkStickyContainer>
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="700"> <MkSpacer :contentMax="700">
<div v-if="tab === 'featured'"> <MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
<MkPagination v-slot="{items}" :pagination="featuredPagesPagination"> <div v-if="tab === 'featured'" key="featured">
<div class="_gaps"> <MkPagination v-slot="{items}" :pagination="featuredPagesPagination">
<MkPagePreview v-for="page in items" :key="page.id" :page="page"/> <div class="_gaps">
</div> <MkPagePreview v-for="page in items" :key="page.id" :page="page"/>
</MkPagination> </div>
</div> </MkPagination>
</div>
<div v-else-if="tab === 'my'" class="_gaps"> <div v-else-if="tab === 'my'" key="my" class="_gaps">
<MkButton class="new" @click="create()"><i class="ti ti-plus"></i></MkButton> <MkButton class="new" @click="create()"><i class="ti ti-plus"></i></MkButton>
<MkPagination v-slot="{items}" :pagination="myPagesPagination"> <MkPagination v-slot="{items}" :pagination="myPagesPagination">
<div class="_gaps"> <div class="_gaps">
<MkPagePreview v-for="page in items" :key="page.id" :page="page"/> <MkPagePreview v-for="page in items" :key="page.id" :page="page"/>
</div> </div>
</MkPagination> </MkPagination>
</div> </div>
<div v-else-if="tab === 'liked'"> <div v-else-if="tab === 'liked'" key="liked">
<MkPagination v-slot="{items}" :pagination="likedPagesPagination"> <MkPagination v-slot="{items}" :pagination="likedPagesPagination">
<div class="_gaps"> <div class="_gaps">
<MkPagePreview v-for="like in items" :key="like.page.id" :page="like.page"/> <MkPagePreview v-for="like in items" :key="like.page.id" :page="like.page"/>
</div> </div>
</MkPagination> </MkPagination>
</div> </div>
</MkHorizontalSwipe>
</MkSpacer> </MkSpacer>
</MkStickyContainer> </MkStickyContainer>
</template> </template>
@ -40,6 +42,7 @@ import { computed, ref } from 'vue';
import MkPagePreview from '@/components/MkPagePreview.vue'; import MkPagePreview from '@/components/MkPagePreview.vue';
import MkPagination from '@/components/MkPagination.vue'; import MkPagination from '@/components/MkPagination.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js'; import { definePageMetadata } from '@/scripts/page-metadata.js';
import { useRouter } from '@/global/router/supplier.js'; import { useRouter } from '@/global/router/supplier.js';

View file

@ -7,18 +7,20 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkStickyContainer> <MkStickyContainer>
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer v-if="tab === 'note'" :contentMax="800"> <MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
<div v-if="notesSearchAvailable"> <MkSpacer v-if="tab === 'note'" key="note" :contentMax="800">
<XNote/> <div v-if="notesSearchAvailable">
</div> <XNote/>
<div v-else> </div>
<MkInfo warn>{{ i18n.ts.notesSearchNotAvailable }}</MkInfo> <div v-else>
</div> <MkInfo warn>{{ i18n.ts.notesSearchNotAvailable }}</MkInfo>
</MkSpacer> </div>
</MkSpacer>
<MkSpacer v-else-if="tab === 'user'" :contentMax="800"> <MkSpacer v-else-if="tab === 'user'" key="user" :contentMax="800">
<XUser/> <XUser/>
</MkSpacer> </MkSpacer>
</MkHorizontalSwipe>
</MkStickyContainer> </MkStickyContainer>
</template> </template>
@ -29,6 +31,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
import { $i } from '@/account.js'; import { $i } from '@/account.js';
import { instance } from '@/instance.js'; import { instance } from '@/instance.js';
import MkInfo from '@/components/MkInfo.vue'; import MkInfo from '@/components/MkInfo.vue';
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
const XNote = defineAsyncComponent(() => import('./search.note.vue')); const XNote = defineAsyncComponent(() => import('./search.note.vue'));
const XUser = defineAsyncComponent(() => import('./search.user.vue')); const XUser = defineAsyncComponent(() => import('./search.user.vue'));

View file

@ -155,6 +155,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="enableInfiniteScroll">{{ i18n.ts.enableInfiniteScroll }}</MkSwitch> <MkSwitch v-model="enableInfiniteScroll">{{ i18n.ts.enableInfiniteScroll }}</MkSwitch>
<MkSwitch v-model="keepScreenOn">{{ i18n.ts.keepScreenOn }}</MkSwitch> <MkSwitch v-model="keepScreenOn">{{ i18n.ts.keepScreenOn }}</MkSwitch>
<MkSwitch v-model="disableStreamingTimeline">{{ i18n.ts.disableStreamingTimeline }}</MkSwitch> <MkSwitch v-model="disableStreamingTimeline">{{ i18n.ts.disableStreamingTimeline }}</MkSwitch>
<MkSwitch v-model="enableHorizontalSwipe">{{ i18n.ts.enableHorizontalSwipe }}</MkSwitch>
</div> </div>
<MkSelect v-model="serverDisconnectedBehavior"> <MkSelect v-model="serverDisconnectedBehavior">
<template #label>{{ i18n.ts.whenServerDisconnected }}</template> <template #label>{{ i18n.ts.whenServerDisconnected }}</template>
@ -296,6 +297,7 @@ const keepScreenOn = computed(defaultStore.makeGetterSetter('keepScreenOn'));
const disableStreamingTimeline = computed(defaultStore.makeGetterSetter('disableStreamingTimeline')); const disableStreamingTimeline = computed(defaultStore.makeGetterSetter('disableStreamingTimeline'));
const useGroupedNotifications = computed(defaultStore.makeGetterSetter('useGroupedNotifications')); const useGroupedNotifications = computed(defaultStore.makeGetterSetter('useGroupedNotifications'));
const enableSeasonalScreenEffect = computed(defaultStore.makeGetterSetter('enableSeasonalScreenEffect')); const enableSeasonalScreenEffect = computed(defaultStore.makeGetterSetter('enableSeasonalScreenEffect'));
const enableHorizontalSwipe = computed(defaultStore.makeGetterSetter('enableHorizontalSwipe'));
watch(lang, () => { watch(lang, () => {
miLocalStorage.setItem('lang', lang.value as string); miLocalStorage.setItem('lang', lang.value as string);

View file

@ -7,27 +7,28 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkStickyContainer> <MkStickyContainer>
<template #header><MkPageHeader v-model:tab="src" :actions="headerActions" :tabs="$i ? headerTabs : headerTabsWhenNotLogin" :displayMyAvatar="true"/></template> <template #header><MkPageHeader v-model:tab="src" :actions="headerActions" :tabs="$i ? headerTabs : headerTabsWhenNotLogin" :displayMyAvatar="true"/></template>
<MkSpacer :contentMax="800"> <MkSpacer :contentMax="800">
<div ref="rootEl" v-hotkey.global="keymap"> <MkHorizontalSwipe v-model:tab="src" :tabs="$i ? headerTabs : headerTabsWhenNotLogin">
<MkInfo v-if="['home', 'local', 'social', 'global'].includes(src) && !defaultStore.reactiveState.timelineTutorials.value[src]" style="margin-bottom: var(--margin);" closable @close="closeTutorial()"> <div :key="src + withRenotes + withReplies + onlyFiles" ref="rootEl" v-hotkey.global="keymap">
{{ i18n.ts._timelineDescription[src] }} <MkInfo v-if="['home', 'local', 'social', 'global'].includes(src) && !defaultStore.reactiveState.timelineTutorials.value[src]" style="margin-bottom: var(--margin);" closable @close="closeTutorial()">
</MkInfo> {{ i18n.ts._timelineDescription[src] }}
<MkPostForm v-if="defaultStore.reactiveState.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--margin);"/> </MkInfo>
<MkPostForm v-if="defaultStore.reactiveState.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--margin);"/>
<div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div> <div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div>
<div :class="$style.tl"> <div :class="$style.tl">
<MkTimeline <MkTimeline
ref="tlComponent" ref="tlComponent"
:key="src + withRenotes + withReplies + onlyFiles" :key="src + withRenotes + withReplies + onlyFiles"
:src="src.split(':')[0]" :src="src.split(':')[0]"
:list="src.split(':')[1]" :list="src.split(':')[1]"
:withRenotes="withRenotes" :withRenotes="withRenotes"
:withReplies="withReplies" :withReplies="withReplies"
:onlyFiles="onlyFiles" :onlyFiles="onlyFiles"
:sound="true" :sound="true"
@queue="queueUpdated" @queue="queueUpdated"
/> />
</div>
</div> </div>
</div> </MkHorizontalSwipe>
</MkSpacer> </MkSpacer>
</MkStickyContainer> </MkStickyContainer>
</template> </template>
@ -38,6 +39,7 @@ import type { Tab } from '@/components/global/MkPageHeader.tabs.vue';
import MkTimeline from '@/components/MkTimeline.vue'; import MkTimeline from '@/components/MkTimeline.vue';
import MkInfo from '@/components/MkInfo.vue'; import MkInfo from '@/components/MkInfo.vue';
import MkPostForm from '@/components/MkPostForm.vue'; import MkPostForm from '@/components/MkPostForm.vue';
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
import { scroll } from '@/scripts/scroll.js'; import { scroll } from '@/scripts/scroll.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
@ -69,7 +71,9 @@ const withRenotes = ref(true);
const withReplies = ref($i ? defaultStore.state.tlWithReplies : false); const withReplies = ref($i ? defaultStore.state.tlWithReplies : false);
const onlyFiles = ref(false); const onlyFiles = ref(false);
watch(src, () => queue.value = 0); watch(src, () => {
queue.value = 0;
});
watch(withReplies, (x) => { watch(withReplies, (x) => {
if ($i) defaultStore.set('tlWithReplies', x); if ($i) defaultStore.set('tlWithReplies', x);

View file

@ -8,19 +8,21 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
<div> <div>
<div v-if="user"> <div v-if="user">
<XHome v-if="tab === 'home'" :user="user"/> <MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
<MkSpacer v-else-if="tab === 'notes'" :contentMax="800" style="padding-top: 0"> <XHome v-if="tab === 'home'" key="home" :user="user"/>
<XTimeline :user="user"/> <MkSpacer v-else-if="tab === 'notes'" key="notes" :contentMax="800" style="padding-top: 0">
</MkSpacer> <XTimeline :user="user"/>
<XActivity v-else-if="tab === 'activity'" :user="user"/> </MkSpacer>
<XAchievements v-else-if="tab === 'achievements'" :user="user"/> <XActivity v-else-if="tab === 'activity'" key="activity" :user="user"/>
<XReactions v-else-if="tab === 'reactions'" :user="user"/> <XAchievements v-else-if="tab === 'achievements'" key="achievements" :user="user"/>
<XClips v-else-if="tab === 'clips'" :user="user"/> <XReactions v-else-if="tab === 'reactions'" key="reactions" :user="user"/>
<XLists v-else-if="tab === 'lists'" :user="user"/> <XClips v-else-if="tab === 'clips'" key="clips" :user="user"/>
<XPages v-else-if="tab === 'pages'" :user="user"/> <XLists v-else-if="tab === 'lists'" key="lists" :user="user"/>
<XFlashs v-else-if="tab === 'flashs'" :user="user"/> <XPages v-else-if="tab === 'pages'" key="pages" :user="user"/>
<XGallery v-else-if="tab === 'gallery'" :user="user"/> <XFlashs v-else-if="tab === 'flashs'" key="flashs" :user="user"/>
<XRaw v-else-if="tab === 'raw'" :user="user"/> <XGallery v-else-if="tab === 'gallery'" key="gallery" :user="user"/>
<XRaw v-else-if="tab === 'raw'" key="raw" :user="user"/>
</MkHorizontalSwipe>
</div> </div>
<MkError v-else-if="error" @retry="fetchUser()"/> <MkError v-else-if="error" @retry="fetchUser()"/>
<MkLoading v-else/> <MkLoading v-else/>
@ -36,6 +38,7 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
import { definePageMetadata } from '@/scripts/page-metadata.js'; import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { $i } from '@/account.js'; import { $i } from '@/account.js';
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
const XHome = defineAsyncComponent(() => import('./home.vue')); const XHome = defineAsyncComponent(() => import('./home.vue'));
const XTimeline = defineAsyncComponent(() => import('./index.timeline.vue')); const XTimeline = defineAsyncComponent(() => import('./index.timeline.vue'));
@ -57,6 +60,7 @@ const props = withDefaults(defineProps<{
}); });
const tab = ref(props.page); const tab = ref(props.page);
const user = ref<null | Misskey.entities.UserDetailed>(null); const user = ref<null | Misskey.entities.UserDetailed>(null);
const error = ref<any>(null); const error = ref<any>(null);

View file

@ -2,33 +2,114 @@
* SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import type { ILocale, ParameterizedString } from '../../../../locales/index.js';
export class I18n<T extends Record<string, any>> { type FlattenKeys<T extends ILocale, TPrediction> = keyof {
public ts: T; [K in keyof T as T[K] extends ILocale
? FlattenKeys<T[K], TPrediction> extends infer C extends string
? `${K & string}.${C}`
: never
: T[K] extends TPrediction
? K
: never]: T[K];
};
constructor(locale: T) { type ParametersOf<T extends ILocale, TKey extends FlattenKeys<T, ParameterizedString<string>>> = T extends ILocale
this.ts = locale; ? TKey extends `${infer K}.${infer C}`
// @ts-expect-error -- C は明らかに FlattenKeys<T[K], ParameterizedString<string>> になるが、型システムはここでは TKey がドット区切りであることのコンテキストを持たないので、型システムに合法にて示すことはできない。
? ParametersOf<T[K], C>
: TKey extends keyof T
? T[TKey] extends ParameterizedString<infer P>
? P
: never
: never
: never;
type Ts<T extends ILocale> = {
readonly [K in keyof T as T[K] extends ParameterizedString<string> ? never : K]: T[K] extends ILocale ? Ts<T[K]> : string;
};
export class I18n<T extends ILocale> {
constructor(private locale: T) {
//#region BIND //#region BIND
this.t = this.t.bind(this); this.t = this.t.bind(this);
//#endregion //#endregion
} }
// string にしているのは、ドット区切りでのパス指定を許可するため public get ts(): Ts<T> {
// なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも if (_DEV_) {
public t(key: string, args?: Record<string, string | number>): string { class Handler<TTarget extends object> implements ProxyHandler<TTarget> {
try { get(target: TTarget, p: string | symbol): unknown {
let str = key.split('.').reduce((o, i) => o[i], this.ts) as unknown as string; const value = target[p as keyof TTarget];
if (args) { if (typeof value === 'object') {
for (const [k, v] of Object.entries(args)) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- 実際には null がくることはないので。
str = str.replace(`{${k}}`, v.toString()); return new Proxy(value!, new Handler<TTarget[keyof TTarget] & object>());
}
if (typeof value === 'string') {
const parameters = Array.from(value.matchAll(/\{(\w+)\}/g)).map(([, parameter]) => parameter);
if (parameters.length) {
console.error(`Missing locale parameters: ${parameters.join(', ')} at ${String(p)}`);
}
return value;
}
console.error(`Unexpected locale key: ${String(p)}`);
return p;
} }
} }
return str;
} catch (err) { return new Proxy(this.locale, new Handler()) as Ts<T>;
console.warn(`missing localization '${key}'`);
return key;
} }
return this.locale as Ts<T>;
}
/**
* @deprecated 使 locale vue
*/
public t<TKey extends FlattenKeys<T, string>>(key: TKey): string;
public t<TKey extends FlattenKeys<T, ParameterizedString<string>>>(key: TKey, args: { readonly [_ in ParametersOf<T, TKey>]: string | number }): string;
public t(key: string, args?: { readonly [_: string]: string | number }) {
let str: string | ParameterizedString<string> | ILocale = this.locale;
for (const k of key.split('.')) {
str = str[k];
if (_DEV_) {
if (typeof str === 'undefined') {
console.error(`Unexpected locale key: ${key}`);
return key;
}
}
}
if (args) {
if (_DEV_) {
const missing = Array.from((str as string).matchAll(/\{(\w+)\}/g), ([, parameter]) => parameter).filter(parameter => !Object.hasOwn(args, parameter));
if (missing.length) {
console.error(`Missing locale parameters: ${missing.join(', ')} at ${key}`);
}
}
for (const [k, v] of Object.entries(args)) {
const search = `{${k}}`;
if (_DEV_) {
if (!(str as string).includes(search)) {
console.error(`Unexpected locale parameter: ${k} at ${key}`);
}
}
str = (str as string).replace(search, v.toString());
}
}
return str;
} }
} }

View file

@ -427,6 +427,10 @@ export const defaultStore = markRaw(new Storage('base', {
sfxVolume: 1, sfxVolume: 1,
}, },
}, },
enableHorizontalSwipe: {
where: 'device',
default: true,
},
sound_masterVolume: { sound_masterVolume: {
where: 'device', where: 'device',