1
0
mirror of https://github.com/MisskeyIO/misskey synced 2024-12-25 03:58:18 +09:00

Merge branch 'develop'

This commit is contained in:
syuilo 2022-07-16 18:21:44 +09:00
commit ff24811676
21 changed files with 191 additions and 120 deletions

View File

@ -9,6 +9,12 @@
You should also include the user name that made the change. You should also include the user name that made the change.
--> -->
## 12.115.0 (2022/07/16)
### Improvements
- Client: Deckのプロファイル切り替えを簡単に @syuilo
- Client: UIのブラッシュアップ @syuilo
## 12.114.0 (2022/07/15) ## 12.114.0 (2022/07/15)
### Improvements ### Improvements

View File

@ -889,7 +889,7 @@ enableAutoSensitiveDescription: "Setzt soweit möglich durch Verwendung von Mach
activeEmailValidationDescription: "Aktivert strengere Überprüfung von E-Mail-Adressen, d.h. Testen auf Wegwerfadressen und darauf, ob mit der Adresse tatsächlich kommuniziert werden kann. Ist dies deaktiviert, so wird nur das Format der E-Mail überprüft." activeEmailValidationDescription: "Aktivert strengere Überprüfung von E-Mail-Adressen, d.h. Testen auf Wegwerfadressen und darauf, ob mit der Adresse tatsächlich kommuniziert werden kann. Ist dies deaktiviert, so wird nur das Format der E-Mail überprüft."
navbar: "Navigationsleiste" navbar: "Navigationsleiste"
shuffle: "Mischen" shuffle: "Mischen"
account: "Benutzerkonten" account: "Benutzerkonto"
_sensitiveMediaDetection: _sensitiveMediaDetection:
description: "Ermöglicht eine Erleichterung der Servermoderation durch die automatische Erkennungen von NSFW-Medien unter Verwendung von Machine Learning. Hierdurch wird die Serverlast etwas erhöht." description: "Ermöglicht eine Erleichterung der Servermoderation durch die automatische Erkennungen von NSFW-Medien unter Verwendung von Machine Learning. Hierdurch wird die Serverlast etwas erhöht."
sensitivity: "Erkennungssensitivität" sensitivity: "Erkennungssensitivität"

View File

@ -889,7 +889,7 @@ enableAutoSensitiveDescription: "Allows automatic detection and marking of NSFW
activeEmailValidationDescription: "Enables stricter validation of email addresses, which includes checking for disposable addresses and by whether it can actually be communicated with. When unchecked, only the format of the email is validated." activeEmailValidationDescription: "Enables stricter validation of email addresses, which includes checking for disposable addresses and by whether it can actually be communicated with. When unchecked, only the format of the email is validated."
navbar: "Navigation bar" navbar: "Navigation bar"
shuffle: "Shuffle" shuffle: "Shuffle"
account: "Accounts" account: "Account"
_sensitiveMediaDetection: _sensitiveMediaDetection:
description: "Reduces the effort of server moderation through automatically recognizing NSFW media via Machine Learning. This will slightly increase the load on the server." description: "Reduces the effort of server moderation through automatically recognizing NSFW media via Machine Learning. This will slightly increase the load on the server."
sensitivity: "Detection sensitivity" sensitivity: "Detection sensitivity"

View File

@ -1762,6 +1762,8 @@ _deck:
stackLeft: "左に重ねる" stackLeft: "左に重ねる"
popRight: "右に出す" popRight: "右に出す"
profile: "プロファイル" profile: "プロファイル"
newProfile: "新規プロファイル"
deleteProfile: "プロファイルを削除"
introduction: "カラムを組み合わせて自分だけのインターフェイスを作りましょう!" introduction: "カラムを組み合わせて自分だけのインターフェイスを作りましょう!"
introduction2: "画面の右にある + を押して、いつでもカラムを追加できます。" introduction2: "画面の右にある + を押して、いつでもカラムを追加できます。"
widgetsIntroduction: "カラムのメニューから、「ウィジェットの編集」を選択してウィジェットを追加してください" widgetsIntroduction: "カラムのメニューから、「ウィジェットの編集」を選択してウィジェットを追加してください"

View File

@ -820,6 +820,20 @@ ffVisibilityDescription: "ช่วยให้คุณสามารถกำ
continueThread: "ดูความต่อเนื่องเธรด" continueThread: "ดูความต่อเนื่องเธรด"
deleteAccountConfirm: "การดำเนินการนี้จะลบบัญชีของคุณอย่างถาวรเลยนะ แน่ใจหรอดำเนินการ?" deleteAccountConfirm: "การดำเนินการนี้จะลบบัญชีของคุณอย่างถาวรเลยนะ แน่ใจหรอดำเนินการ?"
incorrectPassword: "รหัสผ่านไม่ถูกต้อง" incorrectPassword: "รหัสผ่านไม่ถูกต้อง"
voteConfirm: "ยืนยันการโหวต \"{choice}\" มั้ย?"
hide: "ซ่อน"
leaveGroup: "ออกจากกลุ่ม"
leaveGroupConfirm: "คุณแน่ใจหรอว่าต้องการออกจาก \"{name}\""
useDrawerReactionPickerForMobile: "แสดงผล ตัวเลือกปฏิกิริยาเป็นลิ้นชักบนมือถือ"
welcomeBackWithName: "ยินดีต้อนรับการกลับมานะค่ะ, {name}"
clickToFinishEmailVerification: "กรุณาคลิก [{ok}] เพื่อดำเนินการยืนยันอีเมลให้เสร็จสมบูรณ์นะ"
overridedDeviceKind: "ประเภทอุปกรณ์"
smartphone: "สมาร์ทโฟน"
tablet: "แท็บเล็ต"
auto: "อัตโนมัติ"
themeColor: "อินสแตนซ์ Ticker Color"
size: "ขนาด"
numberOfColumn: "จำนวนคอลัมน์"
searchByGoogle: "ค้นหา" searchByGoogle: "ค้นหา"
file: "ไฟล์" file: "ไฟล์"
account: "บัญชีผู้ใช้" account: "บัญชีผู้ใช้"

View File

@ -203,6 +203,7 @@ done: "完成"
processing: "正在处理" processing: "正在处理"
preview: "预览" preview: "预览"
default: "默认" default: "默认"
defaultValueIs: "默认值: {value}"
noCustomEmojis: "没有自定义表情符号" noCustomEmojis: "没有自定义表情符号"
noJobs: "没有任务" noJobs: "没有任务"
federating: "联合中" federating: "联合中"
@ -336,7 +337,7 @@ pinnedUsers: "置顶用户"
pinnedUsersDescription: "在「发现」页面中使用换行标记想要置顶的用户。" pinnedUsersDescription: "在「发现」页面中使用换行标记想要置顶的用户。"
pinnedPages: "固定页面" pinnedPages: "固定页面"
pinnedPagesDescription: "输入您要固定到实例首页的页面路径,以换行符分隔。" pinnedPagesDescription: "输入您要固定到实例首页的页面路径,以换行符分隔。"
pinnedClipId: "置顶的签ID" pinnedClipId: "置顶的便签ID"
pinnedNotes: "已置顶的帖子" pinnedNotes: "已置顶的帖子"
hcaptcha: "hCaptcha" hcaptcha: "hCaptcha"
enableHcaptcha: "启用 hCaptcha" enableHcaptcha: "启用 hCaptcha"
@ -640,10 +641,12 @@ random: "随机"
system: "系统" system: "系统"
switchUi: "切换界面" switchUi: "切换界面"
desktop: "桌面" desktop: "桌面"
clip: "签" clip: "便签"
createNew: "新建" createNew: "新建"
optional: "可选" optional: "可选"
createNewClip: "新建书签" createNewClip: "新建便签"
unclip: "移除便签"
confirmToUnclipAlreadyClippedNote: "本帖已包含在便签\"{name}\"里。您想要将本帖从该便签中移除吗?"
public: "公开" public: "公开"
i18nInfo: "Misskey已经被志愿者们翻译成了各种语言。如果你也有兴趣可以通过{link}帮助翻译。" i18nInfo: "Misskey已经被志愿者们翻译成了各种语言。如果你也有兴趣可以通过{link}帮助翻译。"
manageAccessTokens: "管理 Access Tokens" manageAccessTokens: "管理 Access Tokens"
@ -677,7 +680,7 @@ pageLikesCount: "页面点赞次数"
pageLikedCount: "页面被点赞次数" pageLikedCount: "页面被点赞次数"
contact: "联系人" contact: "联系人"
useSystemFont: "使用系统默认字体" useSystemFont: "使用系统默认字体"
clips: "签" clips: "便签"
experimentalFeatures: "实验性功能" experimentalFeatures: "实验性功能"
developer: "开发者" developer: "开发者"
makeExplorable: "使账号可见。" makeExplorable: "使账号可见。"
@ -857,6 +860,7 @@ requireAdminForView: "需要使用管理员账户登录才能查看。"
isSystemAccount: "该账号由系统自动创建和管理。" isSystemAccount: "该账号由系统自动创建和管理。"
typeToConfirm: "输入 {x} 以确认操作。" typeToConfirm: "输入 {x} 以确认操作。"
deleteAccount: "删除账户" deleteAccount: "删除账户"
document: "文档"
numberOfPageCache: "缓存页数" numberOfPageCache: "缓存页数"
numberOfPageCacheDescription: "设置较高的值会更方便用户,但服务器负载和内存使用量会增加。" numberOfPageCacheDescription: "设置较高的值会更方便用户,但服务器负载和内存使用量会增加。"
logoutConfirm: "是否确认登出?" logoutConfirm: "是否确认登出?"
@ -867,6 +871,7 @@ reverse: "翻转"
colored: "彩色" colored: "彩色"
refreshInterval: "刷新间隔" refreshInterval: "刷新间隔"
label: "标签" label: "标签"
type: "类型"
speed: "速度" speed: "速度"
slow: "慢" slow: "慢"
fast: "快" fast: "快"
@ -878,6 +883,8 @@ cannotUploadBecauseInappropriate: "因为可能含有不适宜的内容,无法
cannotUploadBecauseNoFreeSpace: "因为已无可用空间,无法上传。" cannotUploadBecauseNoFreeSpace: "因为已无可用空间,无法上传。"
beta: "测试" beta: "测试"
enableAutoSensitive: "自动 NSFW 识别" enableAutoSensitive: "自动 NSFW 识别"
navbar: "导航栏"
shuffle: "随机"
account: "账户" account: "账户"
_sensitiveMediaDetection: _sensitiveMediaDetection:
description: "可以使用机器学习技术自动检测敏感媒体,以便进行审核。服务器负载将略微增加。" description: "可以使用机器学习技术自动检测敏感媒体,以便进行审核。服务器负载将略微增加。"

View File

@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "12.114.0", "version": "12.115.0",
"codename": "indigo", "codename": "indigo",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -3,7 +3,7 @@
<div v-if="!showMenu" class="main" :class="ad.place"> <div v-if="!showMenu" class="main" :class="ad.place">
<a :href="ad.url" target="_blank"> <a :href="ad.url" target="_blank">
<img :src="ad.imageUrl"> <img :src="ad.imageUrl">
<button class="_button menu" @click.prevent.stop="toggleMenu"><span class="fas fa-info-circle"></span></button> <button class="_button menu" @click.prevent.stop="toggleMenu"><span class="fas fa-info-circle info-circle"></span></button>
</a> </a>
</div> </div>
<div v-else class="menu"> <div v-else class="menu">
@ -135,13 +135,19 @@ export default defineComponent({
display: block; display: block;
object-fit: contain; object-fit: contain;
margin: auto; margin: auto;
border-radius: 5px;
} }
> .menu { > .menu {
position: absolute; position: absolute;
top: 0; top: 1px;
right: 0; right: 1px;
background: var(--panel);
> .info-circle {
border: 3px solid var(--panel);
border-radius: 50%;
background: var(--panel);
}
} }
} }

View File

@ -1,68 +1,41 @@
char2filePath<template> <template>
<img v-if="customEmoji" class="mk-emoji custom" :class="{ normal, noStyle }" :src="url" :alt="alt" :title="alt" decoding="async"/> <img v-if="customEmoji" class="mk-emoji custom" :class="{ normal, noStyle }" :src="url" :alt="alt" :title="alt" decoding="async"/>
<img v-else-if="char && !useOsNativeEmojis" class="mk-emoji" :src="url" :alt="alt" :title="alt" decoding="async"/> <img v-else-if="char && !useOsNativeEmojis" class="mk-emoji" :src="url" :alt="alt" :title="alt" decoding="async"/>
<span v-else-if="char && useOsNativeEmojis">{{ char }}</span> <span v-else-if="char && useOsNativeEmojis">{{ char }}</span>
<span v-else>{{ emoji }}</span> <span v-else>{{ emoji }}</span>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { computed, defineComponent, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import { CustomEmoji } from 'misskey-js/built/entities';
import { getStaticImageUrl } from '@/scripts/get-static-image-url'; import { getStaticImageUrl } from '@/scripts/get-static-image-url';
import { char2filePath } from '@/scripts/twemoji-base'; import { char2filePath } from '@/scripts/twemoji-base';
import { defaultStore } from '@/store'; import { defaultStore } from '@/store';
import { instance } from '@/instance'; import { instance } from '@/instance';
export default defineComponent({ const props = defineProps<{
props: { emoji: string;
emoji: { normal?: boolean;
type: String, noStyle?: boolean;
required: true customEmojis?: CustomEmoji[];
}, isReaction?: boolean;
normal: { }>();
type: Boolean,
required: false,
default: false
},
noStyle: {
type: Boolean,
required: false,
default: false
},
customEmojis: {
required: false
},
isReaction: {
type: Boolean,
default: false
},
},
setup(props) { const isCustom = computed(() => props.emoji.startsWith(':'));
const isCustom = computed(() => props.emoji.startsWith(':')); const char = computed(() => isCustom.value ? null : props.emoji);
const char = computed(() => isCustom.value ? null : props.emoji); const useOsNativeEmojis = computed(() => defaultStore.state.useOsNativeEmojis && !props.isReaction);
const useOsNativeEmojis = computed(() => defaultStore.state.useOsNativeEmojis && !props.isReaction); const ce = computed(() => props.customEmojis ?? instance.emojis ?? []);
const ce = computed(() => props.customEmojis || instance.emojis || []); const customEmoji = computed(() => isCustom.value ? ce.value.find(x => x.name === props.emoji.substr(1, props.emoji.length - 2)) : null);
const customEmoji = computed(() => isCustom.value ? ce.value.find(x => x.name === props.emoji.substr(1, props.emoji.length - 2)) : null); const url = computed(() => {
const url = computed(() => { if (char.value) {
if (char.value) { return char2filePath(char.value);
return char2filePath(char.value); } else {
} else { return defaultStore.state.disableShowingAnimatedImages
return defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(customEmoji.value.url)
? getStaticImageUrl(customEmoji.value.url) : customEmoji.value.url;
: customEmoji.value.url; }
}
});
const alt = computed(() => customEmoji.value ? `:${customEmoji.value.name}:` : char.value);
return {
url,
char,
alt,
customEmoji,
useOsNativeEmojis,
};
},
}); });
const alt = computed(() => customEmoji.value ? `:${customEmoji.value.name}:` : char.value);
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -18,7 +18,7 @@
</div> </div>
</div> </div>
<div v-if="!narrow || hideTitle" class="tabs"> <div v-if="!narrow || hideTitle" class="tabs">
<button v-for="tab in tabs" :ref="(el) => tabRefs[tab.key] = el" v-tooltip="tab.title" class="tab _button" :class="{ active: tab.key != null && tab.key === props.tab }" @mousedown="(ev) => onTabMousedown(tab, ev)" @click="(ev) => onTabClick(tab, ev)"> <button v-for="tab in tabs" :ref="(el) => tabRefs[tab.key] = el" v-tooltip.noDelay="tab.title" class="tab _button" :class="{ active: tab.key != null && tab.key === props.tab }" @mousedown="(ev) => onTabMousedown(tab, ev)" @click="(ev) => onTabClick(tab, ev)">
<i v-if="tab.icon" class="icon" :class="tab.icon"></i> <i v-if="tab.icon" class="icon" :class="tab.icon"></i>
<span v-if="!tab.iconOnly" class="title">{{ tab.title }}</span> <span v-if="!tab.iconOnly" class="title">{{ tab.title }}</span>
</button> </button>
@ -27,7 +27,7 @@
</template> </template>
<div class="buttons right"> <div class="buttons right">
<template v-for="action in actions"> <template v-for="action in actions">
<button v-tooltip="action.text" class="_button button" :class="{ highlighted: action.highlighted }" @click.stop="action.handler" @touchstart="preventDrag"><i :class="action.icon"></i></button> <button v-tooltip.noDelay="action.text" class="_button button" :class="{ highlighted: action.highlighted }" @click.stop="action.handler" @touchstart="preventDrag"><i :class="action.icon"></i></button>
</template> </template>
</div> </div>
</div> </div>

View File

@ -554,6 +554,13 @@ function readPromo() {
&.max-width_500px { &.max-width_500px {
font-size: 0.9em; font-size: 0.9em;
> .article {
> .avatar {
width: 50px;
height: 50px;
}
}
} }
&.max-width_450px { &.max-width_450px {
@ -570,8 +577,8 @@ function readPromo() {
> .avatar { > .avatar {
margin: 0 10px 8px 0; margin: 0 10px 8px 0;
width: 50px; width: 46px;
height: 50px; height: 46px;
top: calc(14px + var(--stickyTop, 0px)); top: calc(14px + var(--stickyTop, 0px));
} }
} }

View File

@ -7,10 +7,11 @@ import { popup, alert } from '@/os';
const start = isTouchUsing ? 'touchstart' : 'mouseover'; const start = isTouchUsing ? 'touchstart' : 'mouseover';
const end = isTouchUsing ? 'touchend' : 'mouseleave'; const end = isTouchUsing ? 'touchend' : 'mouseleave';
const delay = 100;
export default { export default {
mounted(el: HTMLElement, binding, vn) { mounted(el: HTMLElement, binding, vn) {
const delay = binding.modifiers.noDelay ? 0 : 100;
const self = (el as any)._tooltipDirective_ = {} as any; const self = (el as any)._tooltipDirective_ = {} as any;
self.text = binding.value as string; self.text = binding.value as string;

View File

@ -9,7 +9,7 @@
</div> </div>
</div> </div>
<div class="tabs"> <div class="tabs">
<button v-for="tab in tabs" :ref="(el) => tabRefs[tab.key] = el" v-tooltip="tab.title" class="tab _button" :class="{ active: tab.key != null && tab.key === props.tab }" @mousedown="(ev) => onTabMousedown(tab, ev)" @click="(ev) => onTabClick(tab, ev)"> <button v-for="tab in tabs" :ref="(el) => tabRefs[tab.key] = el" v-tooltip.noDelay="tab.title" class="tab _button" :class="{ active: tab.key != null && tab.key === props.tab }" @mousedown="(ev) => onTabMousedown(tab, ev)" @click="(ev) => onTabClick(tab, ev)">
<i v-if="tab.icon" class="icon" :class="tab.icon"></i> <i v-if="tab.icon" class="icon" :class="tab.icon"></i>
<span v-if="!tab.iconOnly" class="title">{{ tab.title }}</span> <span v-if="!tab.iconOnly" class="title">{{ tab.title }}</span>
</button> </button>
@ -20,7 +20,7 @@
<template v-if="actions"> <template v-if="actions">
<template v-for="action in actions"> <template v-for="action in actions">
<MkButton v-if="action.asFullButton" class="fullButton" primary @click.stop="action.handler"><i :class="action.icon" style="margin-right: 6px;"></i>{{ action.text }}</MkButton> <MkButton v-if="action.asFullButton" class="fullButton" primary @click.stop="action.handler"><i :class="action.icon" style="margin-right: 6px;"></i>{{ action.text }}</MkButton>
<button v-else v-tooltip="action.text" class="_button button" :class="{ highlighted: action.highlighted }" @click.stop="action.handler" @touchstart="preventDrag"><i :class="action.icon"></i></button> <button v-else v-tooltip.noDelay="action.text" class="_button button" :class="{ highlighted: action.highlighted }" @click.stop="action.handler" @touchstart="preventDrag"><i :class="action.icon"></i></button>
</template> </template>
</template> </template>
</div> </div>

View File

@ -9,8 +9,6 @@
<option value="left">{{ i18n.ts.left }}</option> <option value="left">{{ i18n.ts.left }}</option>
<option value="center">{{ i18n.ts.center }}</option> <option value="center">{{ i18n.ts.center }}</option>
</FormRadios> </FormRadios>
<FormLink class="_formBlock" @click="setProfile">{{ i18n.ts._deck.profile }}<template #suffix>{{ profile }}</template></FormLink>
</div> </div>
</template> </template>
@ -29,18 +27,6 @@ import { definePageMetadata } from '@/scripts/page-metadata';
const navWindow = computed(deckStore.makeGetterSetter('navWindow')); const navWindow = computed(deckStore.makeGetterSetter('navWindow'));
const alwaysShowMainColumn = computed(deckStore.makeGetterSetter('alwaysShowMainColumn')); const alwaysShowMainColumn = computed(deckStore.makeGetterSetter('alwaysShowMainColumn'));
const columnAlign = computed(deckStore.makeGetterSetter('columnAlign')); const columnAlign = computed(deckStore.makeGetterSetter('columnAlign'));
const profile = computed(deckStore.makeGetterSetter('profile'));
async function setProfile() {
const { canceled, result: name } = await os.inputText({
title: i18n.ts._deck.profile,
allowEmpty: false,
});
if (canceled) return;
profile.value = name;
unisonReload();
}
const headerActions = $computed(() => []); const headerActions = $computed(() => []);

View File

@ -155,7 +155,7 @@ const age = $computed(() => {
}); });
function menu(ev) { function menu(ev) {
os.popupMenu(getUserMenu(props.user), ev.currentTarget ?? ev.target); os.popupMenu(getUserMenu(props.user, router), ev.currentTarget ?? ev.target);
} }
function parallaxLoop() { function parallaxLoop() {

View File

@ -23,7 +23,6 @@ import calcAge from 's-age';
import * as Acct from 'misskey-js/built/acct'; import * as Acct from 'misskey-js/built/acct';
import * as misskey from 'misskey-js'; import * as misskey from 'misskey-js';
import { getScrollPosition } from '@/scripts/scroll'; import { getScrollPosition } from '@/scripts/scroll';
import { getUserMenu } from '@/scripts/get-user-menu';
import number from '@/filters/number'; import number from '@/filters/number';
import { userPage, acct as getAcct } from '@/filters/user'; import { userPage, acct as getAcct } from '@/filters/user';
import * as os from '@/os'; import * as os from '@/os';
@ -65,10 +64,6 @@ watch(() => props.acct, fetchUser, {
immediate: true, immediate: true,
}); });
function menu(ev) {
os.popupMenu(getUserMenu(user), ev.currentTarget ?? ev.target);
}
const headerActions = $computed(() => []); const headerActions = $computed(() => []);
const headerTabs = $computed(() => user ? [{ const headerTabs = $computed(() => user ? [{

View File

@ -7,8 +7,9 @@ import * as os from '@/os';
import { userActions } from '@/store'; import { userActions } from '@/store';
import { $i, iAmModerator } from '@/account'; import { $i, iAmModerator } from '@/account';
import { mainRouter } from '@/router'; import { mainRouter } from '@/router';
import { Router } from '@/nirax';
export function getUserMenu(user) { export function getUserMenu(user, router: Router = mainRouter) {
const meId = $i ? $i.id : null; const meId = $i ? $i.id : null;
async function pushList() { async function pushList() {
@ -161,7 +162,7 @@ export function getUserMenu(user) {
icon: 'fas fa-info-circle', icon: 'fas fa-info-circle',
text: i18n.ts.info, text: i18n.ts.info,
action: () => { action: () => {
os.pageWindow(`/user-info/${user.id}`); router.push(`/user-info/${user.id}`);
}, },
}, { }, {
icon: 'fas fa-envelope', icon: 'fas fa-envelope',
@ -227,7 +228,7 @@ export function getUserMenu(user) {
icon: 'fas fa-pencil-alt', icon: 'fas fa-pencil-alt',
text: i18n.ts.editProfile, text: i18n.ts.editProfile,
action: () => { action: () => {
mainRouter.push('/settings/profile'); router.push('/settings/profile');
}, },
}]); }]);
} }

View File

@ -3,7 +3,7 @@
<div class="body"> <div class="body">
<div class="top"> <div class="top">
<div class="banner" :style="{ backgroundImage: `url(${ $instance.bannerUrl })` }"></div> <div class="banner" :style="{ backgroundImage: `url(${ $instance.bannerUrl })` }"></div>
<button v-click-anime v-tooltip.right="$instance.name ?? i18n.ts.instance" class="item _button instance" @click="openInstanceMenu"> <button v-click-anime class="item _button instance" @click="openInstanceMenu">
<img :src="$instance.iconUrl || $instance.faviconUrl || '/favicon.ico'" alt="" class="icon"/> <img :src="$instance.iconUrl || $instance.faviconUrl || '/favicon.ico'" alt="" class="icon"/>
</button> </button>
</div> </div>

View File

@ -3,12 +3,12 @@
<div class="body"> <div class="body">
<div class="top"> <div class="top">
<div class="banner" :style="{ backgroundImage: `url(${ $instance.bannerUrl })` }"></div> <div class="banner" :style="{ backgroundImage: `url(${ $instance.bannerUrl })` }"></div>
<button v-click-anime v-tooltip.right="$instance.name ?? i18n.ts.instance" class="item _button instance" @click="openInstanceMenu"> <button v-click-anime v-tooltip.noDelay.right="$instance.name ?? i18n.ts.instance" class="item _button instance" @click="openInstanceMenu">
<img :src="$instance.iconUrl || $instance.faviconUrl || '/favicon.ico'" alt="" class="icon"/> <img :src="$instance.iconUrl || $instance.faviconUrl || '/favicon.ico'" alt="" class="icon"/>
</button> </button>
</div> </div>
<div class="middle"> <div class="middle">
<MkA v-click-anime v-tooltip.right="i18n.ts.timeline" class="item index" active-class="active" to="/" exact> <MkA v-click-anime v-tooltip.noDelay.right="i18n.ts.timeline" class="item index" active-class="active" to="/" exact>
<i class="icon fas fa-home fa-fw"></i><span class="text">{{ i18n.ts.timeline }}</span> <i class="icon fas fa-home fa-fw"></i><span class="text">{{ i18n.ts.timeline }}</span>
</MkA> </MkA>
<template v-for="item in menu"> <template v-for="item in menu">
@ -17,7 +17,7 @@
:is="navbarItemDef[item].to ? 'MkA' : 'button'" :is="navbarItemDef[item].to ? 'MkA' : 'button'"
v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)"
v-click-anime v-click-anime
v-tooltip.right="i18n.ts[navbarItemDef[item].title]" v-tooltip.noDelay.right="i18n.ts[navbarItemDef[item].title]"
class="item _button" class="item _button"
:class="[item, { active: navbarItemDef[item].active }]" :class="[item, { active: navbarItemDef[item].active }]"
active-class="active" active-class="active"
@ -29,22 +29,22 @@
</component> </component>
</template> </template>
<div class="divider"></div> <div class="divider"></div>
<MkA v-if="$i.isAdmin || $i.isModerator" v-click-anime v-tooltip.right="i18n.ts.controlPanel" class="item" active-class="active" to="/admin"> <MkA v-if="$i.isAdmin || $i.isModerator" v-click-anime v-tooltip.noDelay.right="i18n.ts.controlPanel" class="item" active-class="active" to="/admin">
<i class="icon fas fa-door-open fa-fw"></i><span class="text">{{ i18n.ts.controlPanel }}</span> <i class="icon fas fa-door-open fa-fw"></i><span class="text">{{ i18n.ts.controlPanel }}</span>
</MkA> </MkA>
<button v-click-anime class="item _button" @click="more"> <button v-click-anime class="item _button" @click="more">
<i class="icon fa fa-ellipsis-h fa-fw"></i><span class="text">{{ i18n.ts.more }}</span> <i class="icon fa fa-ellipsis-h fa-fw"></i><span class="text">{{ i18n.ts.more }}</span>
<span v-if="otherMenuItemIndicated" class="indicator"><i class="icon fas fa-circle"></i></span> <span v-if="otherMenuItemIndicated" class="indicator"><i class="icon fas fa-circle"></i></span>
</button> </button>
<MkA v-click-anime v-tooltip.right="i18n.ts.settings" class="item" active-class="active" to="/settings"> <MkA v-click-anime v-tooltip.noDelay.right="i18n.ts.settings" class="item" active-class="active" to="/settings">
<i class="icon fas fa-cog fa-fw"></i><span class="text">{{ i18n.ts.settings }}</span> <i class="icon fas fa-cog fa-fw"></i><span class="text">{{ i18n.ts.settings }}</span>
</MkA> </MkA>
</div> </div>
<div class="bottom"> <div class="bottom">
<button v-tooltip.right="i18n.ts.note" class="item _button post" data-cy-open-post-form @click="os.post"> <button v-tooltip.noDelay.right="i18n.ts.note" class="item _button post" data-cy-open-post-form @click="os.post">
<i class="icon fas fa-pencil-alt fa-fw"></i><span class="text">{{ i18n.ts.note }}</span> <i class="icon fas fa-pencil-alt fa-fw"></i><span class="text">{{ i18n.ts.note }}</span>
</button> </button>
<button v-click-anime v-tooltip.right="i18n.ts.account" class="item _button account" @click="openAccountMenu"> <button v-click-anime v-tooltip.noDelay.right="i18n.ts.account" class="item _button account" @click="openAccountMenu">
<MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/> <MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/>
</button> </button>
</div> </div>
@ -356,7 +356,7 @@ function more(ev: MouseEvent) {
> .icon { > .icon {
display: inline-block; display: inline-block;
width: 38px; width: 30px;
aspect-ratio: 1; aspect-ratio: 1;
} }
} }

View File

@ -33,8 +33,16 @@
<div>{{ i18n.ts._deck.introduction2 }}</div> <div>{{ i18n.ts._deck.introduction2 }}</div>
</div> </div>
<div class="sideMenu"> <div class="sideMenu">
<button v-tooltip.left="i18n.ts._deck.addColumn" class="_button button" @click="addColumn"><i class="fas fa-plus"></i></button> <div class="top">
<button v-tooltip.left="i18n.ts.settings" class="_button button settings" @click="showSettings"><i class="fas fa-cog"></i></button> <button v-tooltip.noDelay.left="`${i18n.ts._deck.profile}: ${deckStore.state.profile}`" class="_button button" @click="changeProfile"><i class="fas fa-caret-down"></i></button>
<button v-tooltip.noDelay.left="i18n.ts._deck.deleteProfile" class="_button button" @click="deleteProfile"><i class="fas fa-trash-can"></i></button>
</div>
<div class="middle">
<button v-tooltip.noDelay.left="i18n.ts._deck.addColumn" class="_button button" @click="addColumn"><i class="fas fa-plus"></i></button>
</div>
<div class="bottom">
<button v-tooltip.noDelay.left="i18n.ts.settings" class="_button button settings" @click="showSettings"><i class="fas fa-cog"></i></button>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -67,7 +75,7 @@
import { computed, defineAsyncComponent, onMounted, provide, ref, watch } from 'vue'; import { computed, defineAsyncComponent, onMounted, provide, ref, watch } from 'vue';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import XCommon from './_common_/common.vue'; import XCommon from './_common_/common.vue';
import { deckStore, addColumn as addColumnToStore, loadDeck } from './deck/deck-store'; import { deckStore, addColumn as addColumnToStore, loadDeck, getProfiles, deleteProfile as deleteProfile_ } from './deck/deck-store';
import DeckColumnCore from '@/ui/deck/column-core.vue'; import DeckColumnCore from '@/ui/deck/column-core.vue';
import XSidebar from '@/ui/_common_/navbar.vue'; import XSidebar from '@/ui/_common_/navbar.vue';
import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue'; import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue';
@ -78,6 +86,7 @@ import { navbarItemDef } from '@/navbar';
import { $i } from '@/account'; import { $i } from '@/account';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { mainRouter } from '@/router'; import { mainRouter } from '@/router';
import { unisonReload } from '@/scripts/unison-reload';
const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue')); const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue'));
mainRouter.navHook = (path): boolean => { mainRouter.navHook = (path): boolean => {
@ -168,6 +177,51 @@ loadDeck();
function moveFocus(id: string, direction: 'up' | 'down' | 'left' | 'right') { function moveFocus(id: string, direction: 'up' | 'down' | 'left' | 'right') {
// TODO?? // TODO??
} }
function changeProfile(ev: MouseEvent) {
const items = ref([{
text: deckStore.state.profile,
active: true.valueOf,
}]);
getProfiles().then(profiles => {
items.value = [{
text: deckStore.state.profile,
active: true.valueOf,
}, ...(profiles.filter(k => k !== deckStore.state.profile).map(k => ({
text: k,
action: () => {
deckStore.set('profile', k);
unisonReload();
},
}))), null, {
text: i18n.ts._deck.newProfile,
icon: 'fas fa-plus',
action: async () => {
const { canceled, result: name } = await os.inputText({
title: i18n.ts._deck.profile,
allowEmpty: false,
});
if (canceled) return;
deckStore.set('profile', name);
unisonReload();
},
}];
});
os.popupMenu(items, ev.currentTarget ?? ev.target);
}
async function deleteProfile() {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.t('deleteAreYouSure', { x: deckStore.state.profile }),
});
if (canceled) return;
deleteProfile_(deckStore.state.profile);
deckStore.set('profile', 'default');
unisonReload();
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -271,9 +325,25 @@ function moveFocus(id: string, direction: 'up' | 'down' | 'left' | 'right') {
justify-content: center; justify-content: center;
width: 32px; width: 32px;
> .button { > .top, > .middle, > .bottom {
width: 100%; > .button {
aspect-ratio: 1; display: block;
width: 100%;
aspect-ratio: 1;
}
}
> .top {
margin-bottom: auto;
}
> .middle {
margin-top: auto;
margin-bottom: auto;
}
> .bottom {
margin-top: auto;
} }
} }
} }

View File

@ -72,18 +72,8 @@ export const loadDeck = async () => {
return; return;
} }
deckStore.set('columns', [{ deckStore.set('columns', []);
id: 'a', deckStore.set('layout', []);
type: 'main',
name: i18n.ts._deck._columns.main,
width: 350,
}, {
id: 'b',
type: 'notifications',
name: i18n.ts._deck._columns.notifications,
width: 330,
}]);
deckStore.set('layout', [['a'], ['b']]);
return; return;
} }
throw err; throw err;
@ -105,6 +95,19 @@ export const saveDeck = throttle(1000, () => {
}); });
}); });
export async function getProfiles(): Promise<string[]> {
return await api('i/registry/keys', {
scope: ['client', 'deck', 'profiles'],
});
}
export async function deleteProfile(key: string): Promise<void> {
return await api('i/registry/remove', {
scope: ['client', 'deck', 'profiles'],
key: key,
});
}
export function addColumn(column: Column) { export function addColumn(column: Column) {
if (column.name === undefined) column.name = null; if (column.name === undefined) column.name = null;
deckStore.push('columns', column); deckStore.push('columns', column);