实现 JB 项目时间戳、排序。窗口无边框。

This commit is contained in:
2026-03-29 23:31:03 +08:00
parent 00b5ed9a79
commit 4730f7c948
15 changed files with 311 additions and 74 deletions

View File

@@ -1,6 +1,17 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="CssUnknownProperty" enabled="true" level="WARNING" enabled_by_default="true">
<option name="myCustomPropertiesEnabled" value="true" />
<option name="myIgnoreVendorSpecificProperties" value="false" />
<option name="myCustomPropertiesList">
<value>
<list size="1">
<item index="0" class="java.lang.String" itemvalue="app-region" />
</list>
</value>
</option>
</inspection_tool>
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredPackages">

View File

@@ -6,11 +6,7 @@ import { loadJsonFile } from 'load-json-file'
import { XMLParser } from 'fast-xml-parser'
import { VSCodeGlobalStorageJson } from '@my-type/vscode-globalstorage-json'
import { JetBrainsIdeOptionsRecentProjects } from '@my-type/jetbrains-ide-options-recentProjects'
import {
JetBrainsIDEDisplayNameEnum,
JetBrainsProductCode,
toProductCode
} from '@my-type/jetbrains-state-tools'
import { JetBrainsIDEDisplayNameEnum, JetBrainsProductCode, toProductCode } from '@my-type/jetbrains-state-tools'
import { checkIDEsResultDto, IDECode } from '@my-type/settings'
import { spawn } from 'node:child_process'
@@ -43,7 +39,7 @@ export async function getVscodeProjects(): Promise<IdeProjectsDto> {
// 还有一种协议名是 vscode-remote:// 将会保留,以作为 VSCode Remote 的标识
const path = decodeURIComponent(workspace[0]).replace('file:///', '')
const name = <string>path.split('/').at(-1)
result.push({ name, path, ide: ['VSC'] })
result.push({ name, path, timestamp: 0, ide: ['VSC'] })
}
return result
}
@@ -89,19 +85,44 @@ export async function getJetBrainsProjects(): Promise<IdeProjectsDto> {
await fs.readFile(rpp, 'utf-8')
)
for (const datum of data.application.component.option) {
// 检索正确的 xml 路径
if (datum['@_name'] !== 'additionalInfo') continue
for (const entry of datum.map.entry) {
const name = <string>entry.value.RecentProjectMetaInfo['@_frameTitle'].split(' ').at(0)
const path = entry['@_key'].replace('$USER_HOME$', os.homedir())
// 项目的上次打开时间 projectOpenTimestamp 是一个时间戳,这里提供默认值 0 表示来自 1970 年的上古项目
let timestamp = 0
for (const option of entry.value.RecentProjectMetaInfo.option) {
if (option['@_name'] === 'projectOpenTimestamp') {
timestamp = Number(option['@_value'])
}
}
// 认为包含此(安装/设置?)目录的是未保存的编辑,例如 light-edit 模式
// 正常来说不会在这样的目录下保存项目的……吧?而且俺寻思 IDE 用这种变量的话也不像是人为刻意保存到此目录。
if (path.includes('$APPLICATION_CONFIG_DIR$/')) continue
// 去重。
let pass = false
for (const resultElement of result) {
// 如果路径已存在,即此项目被用其他 JetBrains IDE 打开过
if (resultElement.path === path) {
// 如果 IDE 与该项目已有的工作 IDE 不同,就把这个 IDE 加进列表里去
if (!resultElement.ide.includes(ide)) {
resultElement.ide.push(ide)
}
// 如果 IDE 也重复了就忽略,然后直接
pass = true
}
}
if (!pass) {
result.push({
name,
path,
timestamp,
ide: [ide]
})
}
}
}
} catch {
// 忽略
}

View File

@@ -12,7 +12,8 @@ import os from 'os'
import {
JetBrainsIDEDisplayNameEnum as JIN,
JetBrainsProductCode,
JetBrainsStateDto
JetBrainsStateDto,
toProductDisplayName
} from '@my-type/jetbrains-state-tools'
import { settingsManager } from '../settings'
import { isNodeError } from '@my-type/node-error'
@@ -137,20 +138,20 @@ async function checkVSCodeVersion(): Promise<checkIDEVersionDto> {
*/
export async function checkJetBrainsIDEsVersion(): Promise<checkIDEsVersionDto> {
// 构建数据结构的辅助函数
const _ = (display: string, code: JetBrainsProductCode): checkIDEVersionDto => {
const _ = (code: JetBrainsProductCode): checkIDEVersionDto => {
return {
code,
display,
display: toProductDisplayName(code) as string,
install: 'unknown',
latest: 'unknown'
}
}
const result: checkIDEsVersionDto = {
PY: _('pycharm', 'PY'),
CL: _('clion', 'CL'),
WS: _('webstorm', 'WS'),
PS: _('phpstorm', 'PS'),
IU: _('idea', 'IU')
PY: _('PY'),
CL: _('CL'),
WS: _('WS'),
PS: _('PS'),
IU: _('IU')
}
// 尝试从 JBTState.json 获取已安装的 JetBrains IDEs 的版本

View File

@@ -12,6 +12,7 @@ import path from 'path'
import { getJetBrainsProjects, getVscodeProjects, openProject } from './code-launchpad/ide-projects'
import { createCodeLaunchpadTray, createCodeLaunchpadWindow } from './code-launchpad/code-launchpad'
import type { IDECode } from '@my-type/settings'
import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer'
let mainWindow: BrowserWindow | null = null
// @ts-ignore 保存引用,禁用报错
@@ -31,6 +32,7 @@ function createWindow(): BrowserWindow {
show: false,
autoHideMenuBar: true,
backgroundColor: '#1f1f1f',
frame: false,
icon: fanToolsIcon,
webPreferences: {
preload: path.join(__dirname, '../preload/index.mjs'),
@@ -97,6 +99,14 @@ app.whenReady().then(async () => {
// Set app user model id for windows
electronApp.setAppUserModelId('com.electron')
// 安装 DevTools 插件
if (is.dev) {
// 安装Vue DevTools
installExtension(VUEJS_DEVTOOLS)
.then((name) => console.log(`已安装扩展: ${name}`))
.catch((err) => console.log('安装失败:', err))
}
// Default open or close DevTools by F12 in development
// and ignore CommandOrControl + R in production.
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
@@ -125,6 +135,19 @@ app.whenReady().then(async () => {
}
return false
})
ipcMain.handle('window:minimize', () => {
mainWindow?.minimize()
})
ipcMain.handle('window:maximize', () => {
if (mainWindow?.isMaximized()) {
mainWindow?.unmaximize()
} else {
mainWindow?.maximize()
}
})
ipcMain.handle('window:closeWindow', () => {
mainWindow?.hide()
})
ipcMain.handle('codeLaunchpad:getIDEs', getIDEs)
ipcMain.handle('codeLaunchpad:checkIDEs', checkIDEs)
ipcMain.handle('codeLaunchpad:getIDEsVersion', getIDEsVersion)

View File

@@ -0,0 +1,11 @@
export const formatTimestamp = (timestamp: number, locale = 'zh-CN'): string => {
return new Date(timestamp).toLocaleString(locale, {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
})
}

View File

@@ -5,6 +5,7 @@ type Ide = JetBrainsProductCode | 'VSC'
export interface IdeProjectDto {
name: string
path: string
timestamp: number
ide: Ide[]
}

View File

@@ -1,23 +1,29 @@
// 这些数据结构是由 XMLParser 解析成的 Javascript 对象,解析设置允许属性。
// 不完整,因为我也看不懂很多东西是干嘛用的,写完整了也用不到,所以把用到的定义一下就酱了
export interface RecentProjectMetaInfo {
option: []
frame: object
'@_frameTitle': string
}
export interface JetBrainsIdeOptionsRecentProjects {
application: {
component: {
option: [
{
'@_name': 'activationTimestamp'
'@_value': string
},
{
map: {
entry: {
value: { RecentProjectMetaInfo: RecentProjectMetaInfo }
value: {
RecentProjectMetaInfo: {
// option 中还有很多东西,这里只有我们需要的
option: [{ '@_name': 'projectOpenTimestamp'; '@_value': string }]
frame: object
'@_frameTitle': string
}
}
'@_key': string
}[]
}
'@_name': 'additionalInfo'
}
]
'@_name': 'RecentProjectsManager'

View File

@@ -13,6 +13,9 @@ declare global {
_updateSettings: () => Promise<settingsDto>
_openCodeLaunchpad: () => Promise<boolean>
_closeCodeLaunchpad: () => Promise<boolean>
_minimize: () => Promise<void>
_maximize: () => Promise<void>
_closeWindow: () => Promise<void>
}
codeLaunchpad: {
_getIDEs: () => Promise<checkIDEsResultDto>

View File

@@ -1,3 +1,5 @@
// noinspection JSUnusedGlobalSymbols
import { contextBridge, ipcRenderer } from 'electron'
import { electronAPI } from '@electron-toolkit/preload'
import { settingsDto } from '@my-type/settings'
@@ -8,7 +10,10 @@ const api = {
_saveSettings: (settings: settingsDto) => ipcRenderer.invoke('settings:save', settings),
_updateSettings: () => ipcRenderer.invoke('settings:update'),
_openCodeLaunchpad: () => ipcRenderer.invoke('tools:openCodeLaunchpad'),
_closeCodeLaunchpad: () => ipcRenderer.invoke('tools:closeCodeLaunchpad')
_closeCodeLaunchpad: () => ipcRenderer.invoke('tools:closeCodeLaunchpad'),
_minimize: () => ipcRenderer.invoke('window:minimize'),
_maximize: () => ipcRenderer.invoke('window:maximize'),
_closeWindow: () => ipcRenderer.invoke('window:closeWindow')
}
const codeLaunchpadApi = {

View File

@@ -17,6 +17,7 @@ declare module 'vue' {
DetectedIDECard: typeof import('./src/components/DetectedIDECard.vue')['default']
DetectedIDECardList: typeof import('./src/components/DetectedIDECardList.vue')['default']
NAlert: typeof import('naive-ui')['NAlert']
NavigatorBar: typeof import('./src/components/NavigatorBar.vue')['default']
NButton: typeof import('naive-ui')['NButton']
NButtonGroup: typeof import('naive-ui')['NButtonGroup']
NCard: typeof import('naive-ui')['NCard']
@@ -37,6 +38,7 @@ declare module 'vue' {
NInputNumber: typeof import('naive-ui')['NInputNumber']
NMenu: typeof import('naive-ui')['NMenu']
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
NModal: typeof import('naive-ui')['NModal']
NP: typeof import('naive-ui')['NP']
NSelect: typeof import('naive-ui')['NSelect']
NSpace: typeof import('naive-ui')['NSpace']

View File

@@ -1,9 +1,14 @@
<script lang="ts" setup>
import SidebarRouter from '@renderer/components/SidebarRouter.vue'
import SaveSettingsButton from '@renderer/components/SaveSettingsButton.vue'
import NavigatorBar from '@renderer/components/NavigatorBar.vue'
</script>
<template>
<div class="main-app-container">
<div class="navigator-bar">
<NavigatorBar />
</div>
<div class="everything-container">
<div class="sidebar-router-container scrollarea">
<SidebarRouter />
@@ -15,15 +20,27 @@ import SaveSettingsButton from '@renderer/components/SaveSettingsButton.vue'
</n-message-provider>
</div>
</div>
</div>
</template>
<style lang="scss">
div.main-app-container {
display: flex;
flex-direction: column;
width: 100vw;
height: 100vh;
overflow: hidden;
div.navigator-bar {
padding: 8px;
-webkit-app-region: drag;
}
div.everything-container {
flex: 1;
display: flex;
flex-direction: row;
overflow: hidden;
width: 100vw;
height: 100vh;
div.sidebar-router-container {
flex: 0;
@@ -36,4 +53,5 @@ div.everything-container {
overflow-x: hidden;
}
}
}
</style>

View File

@@ -10,8 +10,24 @@ body {
overflow: hidden;
}
.no-padding {
padding: 0 !important;
}
.small-padding {
padding: 10px !important;
}
.no-padding-top {
padding-top: 0 !important;
}
.no-margin-top {
margin-top: 0 !important;
}
.no-margin-bottom {
margin-bottom: 0;
margin-bottom: 0 !important;
}
// 滚动区域

View File

@@ -4,6 +4,7 @@ import { EllipsisHorizontalSharp as EllipsisIcon } from '@vicons/ionicons5'
import type { IdeProjectDto } from '@my-type/ide-projects'
import { toProductDisplayName } from '@my-type/jetbrains-state-tools'
import { useMessage } from 'naive-ui'
import { formatTimestamp } from '@my-type/dataFormatter'
const message = useMessage()
@@ -67,11 +68,11 @@ async function openWithIDE(ide: string): Promise<void> {
</script>
<template>
<n-card>
<n-card content-class="no-padding" footer-class="no-padding-top small-padding">
<template #default>
<div @click="() => (showDetail = true)">
<div class="click-area" @click="() => (showDetail = true)">
<n-flex justify="left">
<n-h4>{{ project.name }}</n-h4>
<n-h4 class="no-margin-bottom">{{ project.name }}</n-h4>
<n-tag v-for="tag in ideTags" :key="tag" round :type="tag === 'WSL' ? 'primary' : 'info'">
{{ tag }}
</n-tag>
@@ -80,6 +81,10 @@ async function openWithIDE(ide: string): Promise<void> {
</div>
</template>
<template v-if="showDetail" #footer>
<n-flex vertical>
<n-p v-if="project.timestamp !== 0" class="no-margin-bottom" type="default">
上次打开于 {{ formatTimestamp(project.timestamp) }}
</n-p>
<n-flex>
<n-button-group>
<n-button
@@ -87,22 +92,37 @@ async function openWithIDE(ide: string): Promise<void> {
:key="ide"
round
type="info"
size="small"
@click="() => openWithIDE(ide)"
>
{{ ide === 'VSC' ? 'VS Code' : toProductDisplayName(ide) }} 打开
</n-button>
<n-dropdown trigger="click" :options @select="(key) => openWithIDE(key as string)">
<n-button type="info" circle>
<n-button type="info" circle size="small">
<n-icon>
<EllipsisIcon />
</n-icon>
</n-button>
</n-dropdown>
</n-button-group>
<n-button round secondary type="primary" @click="() => (showDetail = false)">收起</n-button>
<n-button round secondary type="primary" size="small" @click="() => (showDetail = false)">
收起
</n-button>
</n-flex>
</n-flex>
</template>
</n-card>
</template>
<style scoped></style>
<style scoped>
div.click-area {
padding: 10px;
border: 1px solid transparent;
border-radius: 4px;
transition: border-color 0.3s;
}
div.click-area:hover {
border: 1px solid #74c072;
}
</style>

View File

@@ -0,0 +1,70 @@
<script setup lang="ts">
import { CloseOutline as CloseIcon } from '@vicons/ionicons5'
import { CropSquareOutlined as FullScreenIcon } from '@vicons/material'
import { ArrowMinimize20Filled as HideIcon } from '@vicons/fluent'
function minimize(): void {
window.api._minimize()
}
function maximize(): void {
window.api._maximize()
}
function closeWindow(): void {
window.api._closeWindow()
}
</script>
<template>
<div class="title-bar">
<div class="title">
<n-text strong type="info">芒果工具箱 FanTools</n-text>
</div>
<div class="button-group">
<n-flex justify="right" align="center" class="no-drag">
<n-button circle type="default" size="small" @click="minimize()">
<n-icon size="large">
<HideIcon />
</n-icon>
</n-button>
<n-button circle type="warning" size="small" @click="maximize()">
<n-icon size="large">
<FullScreenIcon />
</n-icon>
</n-button>
<n-button circle type="error" size="small" @click="closeWindow()">
<n-icon size="large">
<CloseIcon />
</n-icon>
</n-button>
</n-flex>
</div>
</div>
</template>
<style scoped lang="scss">
.no-drag {
-webkit-app-region: no-drag;
}
div.title-bar {
display: flex;
flex-direction: row;
div.title {
flex: 1;
text-align: center;
align-content: center;
font-size: 16px;
background-color: rgb(57 57 57 / 0.4);
border: 1px solid #8cb0bc;
border-radius: 4px;
}
div.button-group {
flex: 0;
min-width: 120px;
}
}
</style>

View File

@@ -2,11 +2,12 @@
import ProjectCard from '@renderer/components-code-launchpad/ProjectCard.vue'
import { useProjects } from '@renderer/stores'
import { computed, onMounted, ref } from 'vue'
import { RefreshOutline as RefreshIcon } from '@vicons/ionicons5'
import { AlertOutline as AlertIcon, RefreshOutline as RefreshIcon } from '@vicons/ionicons5'
const projects = useProjects()
const ide = ref<'VSCode' | 'JetBrains'>('VSCode')
const showModal = ref(false)
// 此 switch-case 结构已经触及所有情况
// eslint-disable-next-line vue/return-in-computed-property
@@ -15,7 +16,7 @@ const reverseProjects = computed(() => {
case 'VSCode':
return projects.vscodeProjects.toReversed()
case 'JetBrains':
return projects.jetBrainsProjects.toReversed()
return projects.jetBrainsProjects.toSorted((a, b) => (a.timestamp > b.timestamp ? -1 : 1))
}
})
@@ -32,10 +33,12 @@ onMounted(() => {
<template>
<n-flex>
<n-button-group class="ide-button-group">
<n-button type="primary" secondary round @click="() => (ide = 'VSCode')">VS Code</n-button>
<n-button type="primary" secondary round @click="() => (ide = 'JetBrains')"
>JetBrains</n-button
>
<n-button type="primary" secondary round @click="() => (ide = 'VSCode')">
<n-text :strong="ide === 'VSCode'" type="success">VS Code</n-text>
</n-button>
<n-button type="primary" secondary round @click="() => (ide = 'JetBrains')">
<n-text :strong="ide === 'JetBrains'" type="success">JetBrains</n-text>
</n-button>
</n-button-group>
<n-button
type="primary"
@@ -52,12 +55,38 @@ onMounted(() => {
<RefreshIcon />
</n-icon>
</n-button>
<n-button type="default" secondary circle @click="() => (showModal = true)">
<n-icon>
<AlertIcon />
</n-icon>
</n-button>
</n-flex>
<n-flex size="small" vertical>
<div v-for="project of reverseProjects" :key="project.path" class="project-card">
<ProjectCard :project />
</div>
</n-flex>
<n-modal v-model:show="showModal" preset="card" title="管理项目?">
<n-p>
代码启动台仅在此罗列找到的项目
<n-text type="warning" strong>您无法在这里管理删除某个项目</n-text>
</n-p>
<n-p>
你可以在这里选择用其他 IDE 打开一个项目但是并非所有 IDE 都支持某些特殊 URI 的项目 例如
<n-code inline>vscode-remote://</n-code>
协议是
<n-text strong type="info">VS Code 远程项目</n-text>
的协议你无法使用 JetBrains IDEs 打开此协议的项目哪怕它们可能运行在
<n-text type="success" strong>WSL</n-text>
</n-p>
<template #footer>
<a href="https://gitea.mangofanfan.cn/MangoFanFanw/FanTools/wiki" target="_blank">
<n-button secondary type="primary"> FanTools.wiki 中查看</n-button>
</a>
</template>
</n-modal>
</template>
<style scoped>