mirror of
https://github.com/SrIzan10/osu-radio.git
synced 2026-05-01 10:55:12 +00:00
Switched to Solid js and Vite
This commit is contained in:
9
.editorconfig
Normal file
9
.editorconfig
Normal file
@@ -0,0 +1,9 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
20
.eslintrc
Normal file
20
.eslintrc
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"extends": [
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*"],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"@typescript-eslint/ban-types": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
dist
|
||||
out
|
||||
*.log*
|
||||
5
.idea/.gitignore
generated
vendored
Normal file
5
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
64
.idea/codeStyles/Project.xml
generated
Normal file
64
.idea/codeStyles/Project.xml
generated
Normal file
@@ -0,0 +1,64 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<HTMLCodeStyleSettings>
|
||||
<option name="HTML_UNIFORM_INDENT" value="true" />
|
||||
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
|
||||
<option name="HTML_DO_NOT_INDENT_CHILDREN_OF" value="html" />
|
||||
<option name="HTML_QUOTE_STYLE" value="Single" />
|
||||
<option name="HTML_ENFORCE_QUOTES" value="true" />
|
||||
</HTMLCodeStyleSettings>
|
||||
<JSCodeStyleSettings version="0">
|
||||
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
||||
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
|
||||
<option name="USE_DOUBLE_QUOTES" value="false" />
|
||||
<option name="FORCE_QUOTE_STYlE" value="true" />
|
||||
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
|
||||
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
||||
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
||||
</JSCodeStyleSettings>
|
||||
<TypeScriptCodeStyleSettings version="0">
|
||||
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
||||
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
|
||||
<option name="USE_DOUBLE_QUOTES" value="false" />
|
||||
<option name="FORCE_QUOTE_STYlE" value="true" />
|
||||
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
|
||||
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
||||
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
||||
</TypeScriptCodeStyleSettings>
|
||||
<VueCodeStyleSettings>
|
||||
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
|
||||
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
|
||||
</VueCodeStyleSettings>
|
||||
<codeStyleSettings language="HTML">
|
||||
<option name="SOFT_MARGINS" value="100" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="JSON">
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="4" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="JavaScript">
|
||||
<option name="KEEP_BLANK_LINES_IN_CODE" value="3" />
|
||||
<option name="BLANK_LINES_AFTER_IMPORTS" value="3" />
|
||||
<option name="BLANK_LINES_AROUND_CLASS" value="3" />
|
||||
<option name="BLANK_LINES_AROUND_METHOD" value="3" />
|
||||
<option name="SOFT_MARGINS" value="100" />
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="TypeScript">
|
||||
<option name="BLANK_LINES_AFTER_IMPORTS" value="3" />
|
||||
<option name="BLANK_LINES_AROUND_CLASS" value="3" />
|
||||
<option name="SOFT_MARGINS" value="100" />
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="Vue">
|
||||
<option name="SOFT_MARGINS" value="100" />
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
@@ -0,0 +1,5 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
||||
6
.idea/compiler.xml
generated
Normal file
6
.idea/compiler.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="TypeScriptCompiler">
|
||||
<option name="recompileOnChanges" value="true" />
|
||||
</component>
|
||||
</project>
|
||||
7
.idea/discord.xml
generated
Normal file
7
.idea/discord.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DiscordProjectSettings">
|
||||
<option name="show" value="PROJECT_FILES" />
|
||||
<option name="description" value="" />
|
||||
</component>
|
||||
</project>
|
||||
7
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
7
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="JSXNamespaceValidation" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||
</profile>
|
||||
</component>
|
||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/osu-radio-solidjs.iml" filepath="$PROJECT_DIR$/.idea/osu-radio-solidjs.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
12
.idea/osu-radio-solidjs.iml
generated
Normal file
12
.idea/osu-radio-solidjs.iml
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
6
.prettierignore
Normal file
6
.prettierignore
Normal file
@@ -0,0 +1,6 @@
|
||||
out
|
||||
dist
|
||||
pnpm-lock.yaml
|
||||
LICENSE.md
|
||||
tsconfig.json
|
||||
tsconfig.*.json
|
||||
4
.prettierrc.yaml
Normal file
4
.prettierrc.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
singleQuote: false
|
||||
semi: true
|
||||
printWidth: 100
|
||||
trailingComma: none
|
||||
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["dbaeumer.vscode-eslint"]
|
||||
}
|
||||
39
.vscode/launch.json
vendored
Normal file
39
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Debug Main Process",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"cwd": "${workspaceRoot}",
|
||||
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite",
|
||||
"windows": {
|
||||
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd"
|
||||
},
|
||||
"runtimeArgs": ["--sourcemap"],
|
||||
"env": {
|
||||
"REMOTE_DEBUGGING_PORT": "9222"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Debug Renderer Process",
|
||||
"port": 9222,
|
||||
"request": "attach",
|
||||
"type": "chrome",
|
||||
"webRoot": "${workspaceFolder}/src/renderer",
|
||||
"timeout": 60000,
|
||||
"presentation": {
|
||||
"hidden": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"compounds": [
|
||||
{
|
||||
"name": "Debug All",
|
||||
"configurations": ["Debug Main Process", "Debug Renderer Process"],
|
||||
"presentation": {
|
||||
"order": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
11
.vscode/settings.json
vendored
Normal file
11
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
}
|
||||
34
README.md
Normal file
34
README.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# osu-radio-solidjs
|
||||
|
||||
An Electron application with Solid and TypeScript
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
- [VSCode](https://code.visualstudio.com/) + [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) + [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
|
||||
|
||||
## Project Setup
|
||||
|
||||
### Install
|
||||
|
||||
```bash
|
||||
$ npm install
|
||||
```
|
||||
|
||||
### Development
|
||||
|
||||
```bash
|
||||
$ npm run dev
|
||||
```
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
# For windows
|
||||
$ npm run build:win
|
||||
|
||||
# For macOS
|
||||
$ npm run build:mac
|
||||
|
||||
# For Linux
|
||||
$ npm run build:linux
|
||||
```
|
||||
12
build/entitlements.mac.plist
Normal file
12
build/entitlements.mac.plist
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
BIN
build/icon.icns
Normal file
BIN
build/icon.icns
Normal file
Binary file not shown.
BIN
build/icon.ico
Normal file
BIN
build/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 121 KiB |
BIN
build/icon.png
Normal file
BIN
build/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
36
build/notarize.js
Normal file
36
build/notarize.js
Normal file
@@ -0,0 +1,36 @@
|
||||
const { notarize } = require('@electron/notarize')
|
||||
|
||||
module.exports = async (context) => {
|
||||
if (process.platform !== 'darwin') return
|
||||
|
||||
console.log('aftersign hook triggered, start to notarize app.')
|
||||
|
||||
if (!process.env.CI) {
|
||||
console.log(`skipping notarizing, not in CI.`)
|
||||
return
|
||||
}
|
||||
|
||||
if (!('APPLE_ID' in process.env && 'APPLE_ID_PASS' in process.env)) {
|
||||
console.warn('skipping notarizing, APPLE_ID and APPLE_ID_PASS env variables must be set.')
|
||||
return
|
||||
}
|
||||
|
||||
const appId = 'com.electron.app'
|
||||
|
||||
const { appOutDir } = context
|
||||
|
||||
const appName = context.packager.appInfo.productFilename
|
||||
|
||||
try {
|
||||
await notarize({
|
||||
appBundleId: appId,
|
||||
appPath: `${appOutDir}/${appName}.app`,
|
||||
appleId: process.env.APPLE_ID,
|
||||
appleIdPassword: process.env.APPLEIDPASS
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
console.log(`done notarizing ${appId}.`)
|
||||
}
|
||||
3
dev-app-update.yml
Normal file
3
dev-app-update.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
provider: generic
|
||||
url: https://example.com/auto-updates
|
||||
updaterCacheDirName: osu-radio-solidjs-updater
|
||||
43
electron-builder.yml
Normal file
43
electron-builder.yml
Normal file
@@ -0,0 +1,43 @@
|
||||
appId: com.electron.app
|
||||
productName: osu-radio-solidjs
|
||||
directories:
|
||||
buildResources: build
|
||||
files:
|
||||
- '!**/.vscode/*'
|
||||
- '!src/*'
|
||||
- '!electron.vite.config.{js,ts,mjs,cjs}'
|
||||
- '!{.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
|
||||
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
|
||||
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
|
||||
asarUnpack:
|
||||
- resources/**
|
||||
afterSign: build/notarize.js
|
||||
win:
|
||||
executableName: osu-radio-solidjs
|
||||
nsis:
|
||||
artifactName: ${name}-${version}-setup.${ext}
|
||||
shortcutName: ${productName}
|
||||
uninstallDisplayName: ${productName}
|
||||
createDesktopShortcut: always
|
||||
mac:
|
||||
entitlementsInherit: build/entitlements.mac.plist
|
||||
extendInfo:
|
||||
- NSCameraUsageDescription: Application requests access to the device's camera.
|
||||
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
||||
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
|
||||
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
|
||||
dmg:
|
||||
artifactName: ${name}-${version}.${ext}
|
||||
linux:
|
||||
target:
|
||||
- AppImage
|
||||
- snap
|
||||
- deb
|
||||
maintainer: electronjs.org
|
||||
category: Utility
|
||||
appImage:
|
||||
artifactName: ${name}-${version}.${ext}
|
||||
npmRebuild: false
|
||||
publish:
|
||||
provider: generic
|
||||
url: https://example.com/auto-updates
|
||||
20
electron.vite.config.ts
Normal file
20
electron.vite.config.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { resolve } from 'path'
|
||||
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
|
||||
import solid from 'vite-plugin-solid'
|
||||
|
||||
export default defineConfig({
|
||||
main: {
|
||||
plugins: [externalizeDepsPlugin()]
|
||||
},
|
||||
preload: {
|
||||
plugins: [externalizeDepsPlugin()]
|
||||
},
|
||||
renderer: {
|
||||
resolve: {
|
||||
alias: {
|
||||
'@renderer': resolve('src/renderer/src')
|
||||
}
|
||||
},
|
||||
plugins: [solid()]
|
||||
}
|
||||
})
|
||||
4774
package-lock.json
generated
Normal file
4774
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
47
package.json
Normal file
47
package.json
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "osu-radio-solidjs",
|
||||
"version": "1.0.0",
|
||||
"description": "An Electron application with Solid and TypeScript",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "example.com",
|
||||
"homepage": "https://www.electronjs.org",
|
||||
"scripts": {
|
||||
"format": "prettier --write .",
|
||||
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
|
||||
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
|
||||
"typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
|
||||
"typecheck": "npm run typecheck:node && npm run typecheck:web",
|
||||
"start": "electron-vite preview",
|
||||
"dev": "electron-vite dev",
|
||||
"build": "npm run typecheck && electron-vite build",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"build:win": "npm run build && electron-builder --win --config",
|
||||
"build:mac": "electron-vite build && electron-builder --mac --config",
|
||||
"build:linux": "electron-vite build && electron-builder --linux --config"
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron-toolkit/preload": "^2.0.0",
|
||||
"@electron-toolkit/utils": "^1.0.2",
|
||||
"electron-updater": "^5.3.0",
|
||||
"get-audio-duration": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-toolkit/tsconfig": "^1.0.1",
|
||||
"@electron/notarize": "^1.2.3",
|
||||
"@types/node": "^18.16.16",
|
||||
"@typescript-eslint/eslint-plugin": "^5.60.1",
|
||||
"@typescript-eslint/parser": "^5.60.1",
|
||||
"electron": "^24.4.1",
|
||||
"electron-builder": "^23.6.0",
|
||||
"electron-vite": "^1.0.23",
|
||||
"eslint": "^8.42.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-solid": "^0.12.1",
|
||||
"prettier": "^2.8.8",
|
||||
"solid-js": "^1.7.6",
|
||||
"typescript": "^5.1.3",
|
||||
"vite": "^4.3.9",
|
||||
"vite-plugin-solid": "^2.7.0"
|
||||
}
|
||||
}
|
||||
BIN
resources/icon.png
Normal file
BIN
resources/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
96
src/@types.d.ts
vendored
Normal file
96
src/@types.d.ts
vendored
Normal file
@@ -0,0 +1,96 @@
|
||||
export type Optional<T> = {
|
||||
value: T,
|
||||
isNone: false;
|
||||
} | {
|
||||
isNone: true
|
||||
}
|
||||
|
||||
|
||||
|
||||
export type Result<T, E> = {
|
||||
value: T,
|
||||
isError: false,
|
||||
} | {
|
||||
error: E,
|
||||
isError: true
|
||||
}
|
||||
|
||||
|
||||
|
||||
export type WatchFile = {
|
||||
/** Only file name, not absolute path */
|
||||
fileName: string,
|
||||
ctime: string
|
||||
};
|
||||
|
||||
|
||||
|
||||
export type Song = {
|
||||
/** Path to audio source (unique factor) */
|
||||
id: string,
|
||||
|
||||
audio ?: WatchFile & { volume?: number },
|
||||
bg ?: WatchFile,
|
||||
config ?: WatchFile,
|
||||
dir: string,
|
||||
|
||||
title: string,
|
||||
artist: string,
|
||||
creator: string,
|
||||
bpm: number[][],
|
||||
duration: number,
|
||||
beatmapSetID ?: number,
|
||||
|
||||
mode?: number,
|
||||
titleUnicode?: string,
|
||||
artistUnicode?: string,
|
||||
tags?: string[],
|
||||
};
|
||||
|
||||
|
||||
|
||||
export type SongIndex = {
|
||||
id: string,
|
||||
t: string,
|
||||
a: string,
|
||||
c: string,
|
||||
tags: string[],
|
||||
bpm: number
|
||||
};
|
||||
|
||||
|
||||
|
||||
export type Settings = {
|
||||
volume: number,
|
||||
osuDir: string,
|
||||
};
|
||||
|
||||
|
||||
|
||||
type OmitPropsWithReturnType<O extends { [K: string]: (...args: unknown[]) => unknown }, V> = {
|
||||
[K in keyof O as ReturnType<O[K]> extends V ? never : K]: O[K]
|
||||
}
|
||||
|
||||
|
||||
|
||||
export type APIFunction<F extends (...args: unknown) => unknown> = (evt: Electron.IpcMainInvokeEvent, ...args: Parameters<F>) => ReturnType<F> | Promise<ReturnType<F>>;
|
||||
|
||||
export type API = {
|
||||
queueCurrent: () => Song,
|
||||
queueNext: () => void,
|
||||
queuePrevious: () => void
|
||||
}
|
||||
|
||||
|
||||
|
||||
export type PacketType = "DATA" | "ERROR"
|
||||
|
||||
export type Packet<T> = {
|
||||
type: PacketType,
|
||||
data: T,
|
||||
token: string,
|
||||
channel: string,
|
||||
reason?: string,
|
||||
}
|
||||
|
||||
export type APIListener<A, R> = (...args: A) => R
|
||||
65
src/main/index.ts
Normal file
65
src/main/index.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { app, BrowserWindow } from 'electron';
|
||||
import { join } from 'path';
|
||||
import { electronApp, is, optimizer } from '@electron-toolkit/utils';
|
||||
import icon from '../../resources/icon.png?asset';
|
||||
import { Router } from './lib/route-pass/Router';
|
||||
|
||||
async function createWindow() {
|
||||
const mainWindow = new BrowserWindow({
|
||||
width: 900,
|
||||
height: 670,
|
||||
show: false,
|
||||
autoHideMenuBar: true,
|
||||
...(process.platform === 'linux' ? { icon } : {}),
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/index.js'),
|
||||
sandbox: false
|
||||
}
|
||||
});
|
||||
|
||||
mainWindow.on('ready-to-show', () => {
|
||||
mainWindow.show();
|
||||
});
|
||||
|
||||
// HMR for renderer base on electron-vite cli.
|
||||
// Load the remote URL for development or the local html file for production.
|
||||
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
||||
await mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL']);
|
||||
} else {
|
||||
await mainWindow.loadFile(join(__dirname, '../renderer/index.html'));
|
||||
}
|
||||
|
||||
Router.dispatch(mainWindow, "log", {
|
||||
packed: "bytes",
|
||||
payload: 69420n
|
||||
}).then(console.log);
|
||||
Router.dispatch(mainWindow, "non-existent", "bruh").catch(console.log);
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
// Set app user model id for windows
|
||||
electronApp.setAppUserModelId('com.electron');
|
||||
|
||||
// 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
|
||||
app.on('browser-window-created', (_, window) => {
|
||||
optimizer.watchWindowShortcuts(window);
|
||||
});
|
||||
|
||||
createWindow();
|
||||
|
||||
app.on('activate', function() {
|
||||
// On macOS, it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
||||
});
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
// routers
|
||||
23
src/main/lib/Signal.ts
Normal file
23
src/main/lib/Signal.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
type SignalListener<T> = (value: T) => any;
|
||||
|
||||
export class Signal<T> {
|
||||
#value: T;
|
||||
#listeners: SignalListener<T>[] = [];
|
||||
|
||||
|
||||
|
||||
constructor(startValue?: T) {
|
||||
this.#value = startValue;
|
||||
}
|
||||
|
||||
listen(listener: SignalListener<T>): void {
|
||||
this.#listeners.push(listener);
|
||||
}
|
||||
|
||||
set value(value: T) {
|
||||
this.#value = value;
|
||||
for (let i = 0; i < this.#listeners.length; i++) {
|
||||
this.#listeners[i](value);
|
||||
}
|
||||
}
|
||||
}
|
||||
266
src/main/lib/osu-file-parser/OsuFileParser.ts
Normal file
266
src/main/lib/osu-file-parser/OsuFileParser.ts
Normal file
@@ -0,0 +1,266 @@
|
||||
import { Optional, Result, Song } from '../../../@types';
|
||||
import fs from 'fs';
|
||||
import readline from 'readline';
|
||||
import path from 'path';
|
||||
import getAudioDurationInSeconds from 'get-audio-duration';
|
||||
import { WatchFile } from './WatchFile';
|
||||
import { none, some } from '../rust-like-utils-backend/Optional';
|
||||
import { fail, ok } from '../rust-like-utils-backend/Result';
|
||||
import { Signal } from '../Signal';
|
||||
|
||||
|
||||
|
||||
const bgFileNameRegex = /.*"(?<!Video.*)(.*)".*/;
|
||||
const beatmapSetIDRegex = /([0-9]+) .*/;
|
||||
|
||||
const propertiesMap: Map<string, string> = new Map([
|
||||
['AudioFilename', 'audioSrc'],
|
||||
['BeatmapSetID', 'beatmapSetID'],
|
||||
['Title', 'title'],
|
||||
['TitleUnicode', 'titleUnicode'],
|
||||
['Artist', 'artist'],
|
||||
['ArtistUnicode', 'artistUnicode'],
|
||||
['Creator', 'creator'],
|
||||
['Tags', 'tags'],
|
||||
['Mode', 'mode']
|
||||
]);
|
||||
|
||||
|
||||
|
||||
type FileState =
|
||||
'Initial'
|
||||
| 'General'
|
||||
| 'Editor'
|
||||
| 'Metadata'
|
||||
| 'Difficulty'
|
||||
| 'Events'
|
||||
| 'TimingPoints'
|
||||
| 'Colours'
|
||||
| 'HitObjects';
|
||||
|
||||
type BPM = number[];
|
||||
const OFFSET = 0;
|
||||
const BPM = 1;
|
||||
|
||||
const audioSourceNotFound = 'Audio does not exists.' as const;
|
||||
|
||||
|
||||
|
||||
export class OsuFileParser {
|
||||
private readonly file: string;
|
||||
private audioSourceToken = 'AudioFilename: ';
|
||||
|
||||
private constructor(file: string) {
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
static new(file: string): Optional<OsuFileParser> {
|
||||
if (!fs.existsSync(file)) {
|
||||
return none();
|
||||
}
|
||||
|
||||
return some(new OsuFileParser(file));
|
||||
}
|
||||
|
||||
static async parseSong(osuFile: string, obj: any): Promise<Result<Song, string>> {
|
||||
const config = WatchFile.new(osuFile);
|
||||
if (config.isError) {
|
||||
return fail(config.error);
|
||||
}
|
||||
|
||||
obj.config = config.value;
|
||||
|
||||
if (obj.audioSrc === undefined) {
|
||||
return fail(audioSourceNotFound);
|
||||
}
|
||||
|
||||
obj.dir = path.dirname(osuFile);
|
||||
obj.id = path.join(obj.dir, obj.audioSrc);
|
||||
const audio = WatchFile.new(obj.id);
|
||||
|
||||
if (audio.isError) {
|
||||
return fail(audio.error);
|
||||
}
|
||||
|
||||
obj.audio = audio.value;
|
||||
delete obj.audioSrc;
|
||||
|
||||
if (obj.bgSrc !== undefined) {
|
||||
const bg = WatchFile.new(path.join(obj.dir, obj.bgSrc));
|
||||
if (!bg.isError) {
|
||||
obj.bg = bg.value;
|
||||
delete obj.bgSrc;
|
||||
}
|
||||
}
|
||||
|
||||
if (obj.mode !== undefined) {
|
||||
obj.mode = Number(obj.mode);
|
||||
}
|
||||
|
||||
if (obj.beatmapSetID !== undefined) {
|
||||
obj.beatmapSetID = Number(obj.beatmapSetID);
|
||||
}
|
||||
|
||||
if (typeof obj.tags === 'string') {
|
||||
obj.tags = obj.tags.split(' ');
|
||||
}
|
||||
|
||||
if (obj.beatmapSetID === undefined) {
|
||||
const beatmapSetID = beatmapSetIDRegex.exec(path.basename(obj.dir));
|
||||
if (beatmapSetID !== null) {
|
||||
obj.beatmapSetID = Number(beatmapSetID[1]);
|
||||
}
|
||||
}
|
||||
|
||||
obj.duration = await getAudioDurationInSeconds(obj.id);
|
||||
|
||||
return ok(obj as Song);
|
||||
}
|
||||
|
||||
static async parseDir(dir: string, update?: Signal<{
|
||||
i: number,
|
||||
total: number
|
||||
}>): Promise<Result<Map<string, Song>, string>> {
|
||||
if (!fs.existsSync(dir)) {
|
||||
return fail('Directory does not exists.');
|
||||
}
|
||||
|
||||
const dirs = fs.readdirSync(dir);
|
||||
const audioSources: Set<string> = new Set();
|
||||
const songs = new Map<string, Song>();
|
||||
|
||||
for (let i = 0; i < dirs.length; i++) {
|
||||
if (update !== undefined) {
|
||||
update.value = {
|
||||
i: i + 1,
|
||||
total: dirs.length
|
||||
};
|
||||
}
|
||||
|
||||
const subDirPath = path.join(dir, dirs[i]);
|
||||
if (!fs.lstatSync(subDirPath).isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(subDirPath);
|
||||
for (let j = 0; j < files.length; j++) {
|
||||
if (!files[j].endsWith('.osu')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const parser = OsuFileParser.new(path.join(subDirPath, files[j]));
|
||||
if (parser.isNone) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const audioSource = parser.value.getAudioSource();
|
||||
if (audioSource.isNone || audioSources.has(audioSource.value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const song = await parser.value.parseFile();
|
||||
if (song.isError) {
|
||||
continue;
|
||||
}
|
||||
|
||||
songs.set(song.value.id, song.value);
|
||||
audioSources.add(audioSource.value);
|
||||
}
|
||||
}
|
||||
|
||||
return ok(songs);
|
||||
}
|
||||
|
||||
getAudioSource(): Optional<string> {
|
||||
const content = fs.readFileSync(this.file, { encoding: 'utf8' });
|
||||
const start = content.indexOf(this.audioSourceToken) + this.audioSourceToken.length;
|
||||
|
||||
for (let i = start; i < content.length; i++) {
|
||||
if (content[i] === '\n'
|
||||
|| content[i] === '\r'
|
||||
|| (content[i] === '\r' && content[i + 1] === '\n')) {
|
||||
return some(content.substring(start, i));
|
||||
}
|
||||
}
|
||||
|
||||
return none();
|
||||
}
|
||||
|
||||
async parseFile(): Promise<Result<Song, string>> {
|
||||
const fileLines = readline.createInterface({
|
||||
input: fs.createReadStream(this.file),
|
||||
crlfDelay: Infinity
|
||||
});
|
||||
|
||||
let state: FileState = 'Initial';
|
||||
const song: any & { bpm: number[][] } = {};
|
||||
song.bpm = [];
|
||||
|
||||
for await (const line of fileLines) {
|
||||
const trimmed = line.trim();
|
||||
|
||||
if (trimmed === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (trimmed[0] === '[' && trimmed[trimmed.length - 1] === ']') {
|
||||
state = trimmed.substring(1, trimmed.length - 1) as FileState;
|
||||
|
||||
if (state === 'HitObjects') {
|
||||
break;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (state === 'Initial' || state === 'Editor' || state === 'Difficulty' || state === 'Colours') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (state === 'Events') {
|
||||
const bg = bgFileNameRegex.exec(trimmed);
|
||||
if (bg !== null) {
|
||||
song['bgSrc'] = bg[1];
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (state === 'TimingPoints') {
|
||||
const timingPoint = trimmed.split(',').map(x => Number(x));
|
||||
|
||||
if (timingPoint.length === 2) {
|
||||
song.bpm.push(timingPoint);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (timingPoint[timingPoint.length - 2] === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (song.bpm.length !== 0 && song.bpm[song.bpm.length - 1][BPM] === timingPoint[BPM]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
song.bpm.push([timingPoint[OFFSET], timingPoint[BPM]]);
|
||||
}
|
||||
|
||||
const useSpaceAfterColon = state === 'General';
|
||||
|
||||
const split = trimmed.split(useSpaceAfterColon ? ': ' : ':');
|
||||
if (split.length !== 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const property = propertiesMap.get(split[0]);
|
||||
|
||||
if (property === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
song[property] = split[1];
|
||||
}
|
||||
|
||||
return await OsuFileParser.parseSong(this.file, song);
|
||||
}
|
||||
}
|
||||
25
src/main/lib/osu-file-parser/WatchFile.ts
Normal file
25
src/main/lib/osu-file-parser/WatchFile.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { Result } from "../../../@types";
|
||||
import { fail, ok } from "../rust-like-utils-backend/Result";
|
||||
|
||||
export class WatchFile {
|
||||
fileName: string;
|
||||
ctime: string;
|
||||
|
||||
private constructor(fileName: string, ctime: string) {
|
||||
this.fileName = fileName;
|
||||
this.ctime = ctime;
|
||||
}
|
||||
|
||||
static new(fullyQualifiedPath: string): Result<WatchFile, string> {
|
||||
if (!fs.existsSync(fullyQualifiedPath)) {
|
||||
return fail("File does not exists.");
|
||||
}
|
||||
|
||||
return ok(new WatchFile(
|
||||
path.basename(fullyQualifiedPath),
|
||||
new Date(fs.lstatSync(fullyQualifiedPath).ctimeMs).toISOString()
|
||||
));
|
||||
}
|
||||
}
|
||||
7
src/main/lib/route-pass/Packet.ts
Normal file
7
src/main/lib/route-pass/Packet.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { PacketType, Packet } from "../../../@types";
|
||||
|
||||
export function cratePacket<T>(channel: string, token: string, data: T, type: PacketType = "DATA"): Packet<T> {
|
||||
return {
|
||||
channel, token, data, type
|
||||
}
|
||||
}
|
||||
6
src/main/lib/route-pass/Router.d.ts
vendored
Normal file
6
src/main/lib/route-pass/Router.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
import type { API, APIFunction } from '../../../@types';
|
||||
import { BrowserWindow } from 'electron';
|
||||
export declare class Router {
|
||||
static respond<E extends keyof API>(event: E, fn: APIFunction<API[E]>): void;
|
||||
static dispatch(window: BrowserWindow, channel: string, data: any): Promise<any>;
|
||||
}
|
||||
46
src/main/lib/route-pass/Router.js
Normal file
46
src/main/lib/route-pass/Router.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import { ipcMain } from 'electron';
|
||||
import { TokenNamespace } from '../tungsten/token';
|
||||
import { assertNever } from '../tungsten/assertNever';
|
||||
import { cratePacket } from './Packet';
|
||||
const tokens = new TokenNamespace();
|
||||
const pending = [];
|
||||
ipcMain.on("communication/main", (_evt, packet) => {
|
||||
for (let i = 0; i < pending.length; i++) {
|
||||
if (pending[i].packet.token !== packet.token) {
|
||||
continue;
|
||||
}
|
||||
switch (packet.type) {
|
||||
case "DATA": {
|
||||
pending[i].resolve(packet.data);
|
||||
break;
|
||||
}
|
||||
case "ERROR": {
|
||||
pending[i].reject(packet.reason);
|
||||
break;
|
||||
}
|
||||
default: assertNever(packet.type);
|
||||
}
|
||||
}
|
||||
});
|
||||
export class Router {
|
||||
static respond(event, fn) {
|
||||
ipcMain.handle(event, fn);
|
||||
}
|
||||
static dispatch(window, channel, data) {
|
||||
const packet = cratePacket(channel, tokens.create(), data);
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
pending.push({
|
||||
packet,
|
||||
reject,
|
||||
resolve
|
||||
});
|
||||
});
|
||||
if (window.isVisible()) {
|
||||
window.webContents.send("communication/renderer", packet);
|
||||
}
|
||||
else {
|
||||
window.on("ready-to-show", () => window.webContents.send("communication/renderer", packet));
|
||||
}
|
||||
return promise;
|
||||
}
|
||||
}
|
||||
63
src/main/lib/route-pass/Router.ts
Normal file
63
src/main/lib/route-pass/Router.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import type { API, APIFunction, Packet } from '../../../@types';
|
||||
import { BrowserWindow, ipcMain } from 'electron';
|
||||
import { TokenNamespace } from '../tungsten/token';
|
||||
import { assertNever } from '../tungsten/assertNever';
|
||||
import { cratePacket } from './Packet';
|
||||
|
||||
|
||||
|
||||
type Pending = {
|
||||
resolve: (value: any) => void,
|
||||
reject: (reason?: any) => void,
|
||||
packet: Packet<any>
|
||||
}
|
||||
|
||||
const tokens = new TokenNamespace();
|
||||
const pending: Pending[] = [];
|
||||
ipcMain.on("communication/main", (_evt, packet: Packet<any>) => {
|
||||
for (let i = 0; i < pending.length; i++) {
|
||||
if (pending[i].packet.token !== packet.token) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (packet.type) {
|
||||
case "DATA": {
|
||||
pending[i].resolve(packet.data);
|
||||
break;
|
||||
}
|
||||
case "ERROR": {
|
||||
pending[i].reject(packet.reason);
|
||||
break;
|
||||
}
|
||||
default: assertNever(packet.type);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
export class Router {
|
||||
static respond<E extends keyof API>(event: E, fn: APIFunction<API[E]>): void {
|
||||
ipcMain.handle(event, fn as any);
|
||||
}
|
||||
|
||||
static dispatch(window: BrowserWindow, channel: string, data: any): Promise<any> {
|
||||
const packet = cratePacket(channel, tokens.create(), data);
|
||||
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
pending.push({
|
||||
packet,
|
||||
reject,
|
||||
resolve
|
||||
});
|
||||
});
|
||||
|
||||
if (window.isVisible()) {
|
||||
window.webContents.send("communication/renderer", packet);
|
||||
} else {
|
||||
window.on("ready-to-show", () => window.webContents.send("communication/renderer", packet));
|
||||
}
|
||||
|
||||
return promise;
|
||||
}
|
||||
}
|
||||
16
src/main/lib/rust-like-utils-backend/Optional.ts
Normal file
16
src/main/lib/rust-like-utils-backend/Optional.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Optional } from "../../../@types";
|
||||
|
||||
|
||||
|
||||
export function none(): Optional<any> {
|
||||
return {
|
||||
isNone: true
|
||||
};
|
||||
}
|
||||
|
||||
export function some<V>(value: V): Optional<V> {
|
||||
return {
|
||||
value,
|
||||
isNone: false
|
||||
};
|
||||
}
|
||||
17
src/main/lib/rust-like-utils-backend/Result.ts
Normal file
17
src/main/lib/rust-like-utils-backend/Result.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Result } from "../../../@types";
|
||||
|
||||
|
||||
|
||||
export function ok<V>(value: V): Result<V, any> {
|
||||
return {
|
||||
value,
|
||||
isError: false
|
||||
};
|
||||
}
|
||||
|
||||
export function fail<E>(error: E): Result<any, E> {
|
||||
return {
|
||||
error,
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
39
src/main/lib/storage/Storage.ts
Normal file
39
src/main/lib/storage/Storage.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { app } from "electron";
|
||||
import { Song, Settings } from "../../../@types";
|
||||
import { Table } from "./Table";
|
||||
|
||||
|
||||
|
||||
type TableMap = {
|
||||
"songs": { [key: string]: Song },
|
||||
"playlists": { [key: string]: Song[] },
|
||||
"settings": Settings
|
||||
}
|
||||
|
||||
|
||||
|
||||
export class Storage {
|
||||
private static cache: Map<string, Table<any>> = new Map();
|
||||
|
||||
static getTable<T extends keyof TableMap>(name: T): Table<TableMap[T]> {
|
||||
const hit = this.cache.get(name);
|
||||
|
||||
if (hit !== undefined) {
|
||||
return hit;
|
||||
}
|
||||
|
||||
const tablePath = path.join(app.getPath("userData"), `/storage/${name}.json`);
|
||||
|
||||
if (!fs.existsSync(tablePath)) {
|
||||
fs.mkdirSync(path.join(app.getPath("userData"), "/storage"), { recursive: true });
|
||||
fs.writeFileSync(tablePath, "{}");
|
||||
return new Table(tablePath, {} as unknown);
|
||||
}
|
||||
|
||||
const table = new Table(tablePath, JSON.parse(fs.readFileSync(tablePath, { encoding: "utf8" })));
|
||||
this.cache.set(name, table);
|
||||
return table;
|
||||
}
|
||||
}
|
||||
33
src/main/lib/storage/Table.ts
Normal file
33
src/main/lib/storage/Table.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import fs from "fs";
|
||||
import { none, some } from "../rust-like-utils-backend/Optional";
|
||||
import { Optional } from "../../../@types";
|
||||
|
||||
export class Table<Struct> {
|
||||
private readonly path: string;
|
||||
private readonly struct: Struct;
|
||||
|
||||
constructor(path: string, struct: Struct) {
|
||||
this.path = path;
|
||||
this.struct = struct;
|
||||
}
|
||||
|
||||
get<K extends keyof Struct>(key: K): Optional<Struct[K]> {
|
||||
return this.struct[key] === undefined
|
||||
? none()
|
||||
: some(this.struct[key]);
|
||||
}
|
||||
|
||||
write<K extends keyof Struct>(key: K, content?: Struct[K]): void {
|
||||
this.struct[key] = content;
|
||||
fs.writeFileSync(this.path, JSON.stringify(this.struct), { encoding: "utf8" });
|
||||
}
|
||||
|
||||
delete<K extends keyof Struct>(key: K): void {
|
||||
delete this.struct[key];
|
||||
fs.writeFileSync(this.path, JSON.stringify(this.struct), { encoding: "utf8" });
|
||||
}
|
||||
|
||||
filePath(): string {
|
||||
return this.path;
|
||||
}
|
||||
}
|
||||
1
src/main/lib/tungsten/assertNever.ts
Normal file
1
src/main/lib/tungsten/assertNever.ts
Normal file
@@ -0,0 +1 @@
|
||||
export function assertNever(_value: never) {}
|
||||
24
src/main/lib/tungsten/math.ts
Normal file
24
src/main/lib/tungsten/math.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export function clamp(min: number, max: number, number: number): number {
|
||||
if (number < min) return min;
|
||||
return number > max ? max : number;
|
||||
}
|
||||
|
||||
export function map(value: number, fromStart: number, fromEnd: number, toStart: number, toEnd: number): number {
|
||||
return ((value - fromStart) / (fromEnd - fromStart)) * (toEnd - toStart) + toStart;
|
||||
}
|
||||
|
||||
export function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function rng(from: number, to: number): number {
|
||||
return Math.random() * (Math.max(from, to) - Math.min(from, to)) + from;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function flatRNG(from: number, to: number): number {
|
||||
return Math.floor(rng(from, to));
|
||||
}
|
||||
56
src/main/lib/tungsten/token.ts
Normal file
56
src/main/lib/tungsten/token.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { flatRNG } from "./math";
|
||||
|
||||
const TOKEN_CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
||||
const TOKEN_CHARSET_ALPHA = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||
|
||||
export type Token = string;
|
||||
|
||||
|
||||
|
||||
const globalTokens: Set<Token> = new Set();
|
||||
|
||||
export function generateToken(forceFirstLetter = false, length = 8, set: Set<string> = undefined as any): Token {
|
||||
let id;
|
||||
const MAX_RETRIES = 10_000;
|
||||
let retry = 0;
|
||||
|
||||
do {
|
||||
if (forceFirstLetter === true) {
|
||||
id = TOKEN_CHARSET_ALPHA[flatRNG(0, TOKEN_CHARSET_ALPHA.length)];
|
||||
length--;
|
||||
}
|
||||
|
||||
for (let i = 0; i < length - 1; i++) {
|
||||
id += TOKEN_CHARSET[flatRNG(0, TOKEN_CHARSET.length)];
|
||||
}
|
||||
|
||||
if (++retry === MAX_RETRIES) break;
|
||||
} while ((set ?? globalTokens).has(id));
|
||||
|
||||
if (retry === MAX_RETRIES) {
|
||||
return generateToken(forceFirstLetter, length + 1, set);
|
||||
}
|
||||
|
||||
(set ?? globalTokens).add(id);
|
||||
return id;
|
||||
}
|
||||
|
||||
export function freeToken(token: Token, set: Set<Token> = undefined as any): void {
|
||||
(set ?? globalTokens).delete(token);
|
||||
}
|
||||
|
||||
export class TokenNamespace {
|
||||
private readonly set: Set<Token>;
|
||||
|
||||
constructor() {
|
||||
this.set = new Set();
|
||||
}
|
||||
|
||||
create(forceFirstLetter = false, length = 8): Token {
|
||||
return generateToken(forceFirstLetter, length, this.set);
|
||||
}
|
||||
|
||||
destroy(token: Token): void {
|
||||
freeToken(token, this.set);
|
||||
}
|
||||
}
|
||||
100
src/main/lib/utils.ts
Normal file
100
src/main/lib/utils.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { Song, SongIndex } from '../../@types';
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
|
||||
export function averageBPM(bpm: number[][], durationMS: number): number {
|
||||
if (bpm.length === 0) {
|
||||
return NaN;
|
||||
}
|
||||
|
||||
if (bpm.length === 1) {
|
||||
return bpm[0][1];
|
||||
}
|
||||
|
||||
const lookup = new Map<number, number[]>();
|
||||
let highestEntry = [-Infinity, NaN];
|
||||
|
||||
for (let i = 0; i < bpm.length; i++) {
|
||||
const end = i + 1 === bpm.length
|
||||
? durationMS
|
||||
: bpm[i + 1][0];
|
||||
|
||||
const entry = lookup.get(bpm[i][1]);
|
||||
if (entry === undefined) {
|
||||
lookup.set(bpm[i][1], [end - bpm[i][0], bpm[i][1]]);
|
||||
continue;
|
||||
}
|
||||
|
||||
entry[0] += end - bpm[i][0];
|
||||
|
||||
if (entry[0] > highestEntry[0]) {
|
||||
highestEntry = entry;
|
||||
}
|
||||
}
|
||||
|
||||
return highestEntry[1];
|
||||
}
|
||||
|
||||
export function filterTags(tags: string[], includes: string[], excludes: string[]): boolean {
|
||||
if (tags.length === 0 || includes.length === 0 && excludes.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let init = 0;
|
||||
for (let i = 0; i < tags.length; i++) {
|
||||
if (includes.includes(tags[i])) {
|
||||
init++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (excludes.includes(tags[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return init === includes.length;
|
||||
}
|
||||
|
||||
export function indexSongs(songs: { [id: string]: Song }): [SongIndex[], Map<string, string[]>] {
|
||||
const indexes: SongIndex[] = [];
|
||||
const tags = new Map<string, string[]>();
|
||||
|
||||
for (const id in songs) {
|
||||
const s = songs[id];
|
||||
indexes.push({
|
||||
id,
|
||||
t: s.beatmapSetID + s.title + (s.titleUnicode ?? ""),
|
||||
a: s.artist + (s.artistUnicode ?? ""),
|
||||
c: s.creator,
|
||||
tags: s.tags,
|
||||
bpm: averageBPM(s.bpm, s.duration * 1_000)
|
||||
});
|
||||
|
||||
for (let i = 0; i < s.tags.length; i++) {
|
||||
const entry = tags.get(s.tags[i]);
|
||||
if (entry === undefined) {
|
||||
tags.set(s.tags[i], [id]);
|
||||
continue;
|
||||
}
|
||||
|
||||
entry.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
return [indexes, tags];
|
||||
}
|
||||
|
||||
export function checkConfigChanges(songs: { [id: string]: Song }): void {
|
||||
let count = 0;
|
||||
const total = Object.values(songs).length;
|
||||
|
||||
for (const id in songs) {
|
||||
const s = songs[id];
|
||||
|
||||
const configSource = path.join(s.dir, "/" + s.config.fileName);
|
||||
if (!(fs.existsSync(configSource) && fs.lstatSync(configSource).ctime.toISOString() === s.config.ctime)) {
|
||||
count++;
|
||||
process.stdout.write("\r\x1b[KNeed to update: " + count + "/" + total);
|
||||
}
|
||||
}
|
||||
}
|
||||
35
src/main/router/queue.ts
Normal file
35
src/main/router/queue.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Router } from "../lib/route-pass/Router";
|
||||
import { Song } from '../../@types';
|
||||
|
||||
|
||||
|
||||
let queue: Song[];
|
||||
let index = 0;
|
||||
|
||||
Router.respond("queueCurrent", () => {
|
||||
return queue[index];
|
||||
});
|
||||
|
||||
Router.respond("queuePrevious", () => {
|
||||
if (--index < 0) {
|
||||
index = queue.length;
|
||||
}
|
||||
});
|
||||
|
||||
Router.respond("queueNext", () => {
|
||||
if (++index === queue.length) {
|
||||
Queue.rewind();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
export class Queue {
|
||||
static rewind(): void {
|
||||
index = 0;
|
||||
}
|
||||
|
||||
static load(songs: Song[]): void {
|
||||
queue = songs;
|
||||
}
|
||||
}
|
||||
11
src/preload/index.d.ts
vendored
Normal file
11
src/preload/index.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
import { ElectronAPI } from '@electron-toolkit/preload';
|
||||
import { API } from '../@types';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
api: {
|
||||
request<E extends keyof API>(event: E, ...data: Parameters<API[E]>): Promise<ReturnType<API[E]>>,
|
||||
listen(channel: string, listener: APIListener<any, any>): void
|
||||
}
|
||||
}
|
||||
}
|
||||
64
src/preload/index.ts
Normal file
64
src/preload/index.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { contextBridge, ipcRenderer } from 'electron';
|
||||
import type { API, APIListener, Packet, PacketType } from '../@types';
|
||||
|
||||
|
||||
|
||||
function createPacketPreload<T>(channel: string, token: string, data: T, type: PacketType = "DATA"): Packet<T> {
|
||||
return {
|
||||
channel, token, data, type
|
||||
}
|
||||
}
|
||||
|
||||
const apiListeners = new Map<string, APIListener<any, any>[]>();
|
||||
|
||||
ipcRenderer.on("communication/renderer", async (_evt, packet: Packet<any>) => {
|
||||
const p = createPacketPreload(packet.channel, packet.token, undefined as any);
|
||||
const listeners = apiListeners.get(packet.channel);
|
||||
|
||||
if (listeners === undefined) {
|
||||
p.type = "ERROR";
|
||||
p.reason = "No listeners found for channel: '" + packet.channel + "'";
|
||||
ipcRenderer.send("communication/main", p);
|
||||
return;
|
||||
}
|
||||
|
||||
const responses: any[] = [];
|
||||
const promises: Promise<any>[] = [];
|
||||
|
||||
for (let i = 0; i < listeners.length; i++) {
|
||||
const got = listeners[i](packet.data);
|
||||
|
||||
if (got instanceof Promise) {
|
||||
promises.push(got);
|
||||
got.then(x => {
|
||||
responses.push(x);
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
responses.push(got);
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
p.data = responses;
|
||||
ipcRenderer.send("communication/main", p);
|
||||
});
|
||||
|
||||
const api = {
|
||||
request<E extends keyof API>(event: E, ...args: Parameters<API[E]>): Promise<ReturnType<API[E]>> {
|
||||
return ipcRenderer.invoke(event, ...args) as any;
|
||||
},
|
||||
|
||||
listen(channel: string, listener: APIListener<any, any>): void {
|
||||
const entry = apiListeners.get(channel);
|
||||
|
||||
if (entry === undefined) {
|
||||
apiListeners.set(channel, [listener]);
|
||||
return;
|
||||
}
|
||||
|
||||
entry.push(listener);
|
||||
}
|
||||
};
|
||||
|
||||
contextBridge.exposeInMainWorld('api', api);
|
||||
17
src/renderer/index.html
Normal file
17
src/renderer/index.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang='en'>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Electron</title>
|
||||
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"
|
||||
/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
2
src/renderer/src/App.d.ts
vendored
Normal file
2
src/renderer/src/App.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
import type { JSX } from 'solid-js';
|
||||
export default function App(): JSX.Element;
|
||||
175
src/renderer/src/App.jsx
Normal file
175
src/renderer/src/App.jsx
Normal file
@@ -0,0 +1,175 @@
|
||||
import Gradient from './components/Gradient';
|
||||
import image from './assets/hololive IDOL PROJECT - BLUE CLAPPER_2761277.jpg';
|
||||
import { createSignal, onMount } from 'solid-js';
|
||||
export default function App() {
|
||||
const [topColor, setTopColor] = createSignal("dodgerblue");
|
||||
onMount(() => {
|
||||
window.addEventListener("click", () => {
|
||||
setTopColor("teal");
|
||||
});
|
||||
});
|
||||
window.api.listen("log", (...args) => {
|
||||
console.log(args);
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => resolve("poggers"), 500);
|
||||
});
|
||||
});
|
||||
return (<Gradient bottomColor="crimson" topColor={topColor()}>
|
||||
<div class="app">
|
||||
<nav>
|
||||
<button class="icon">Aa</button>
|
||||
<button class="icon">Aa</button>
|
||||
<button class="icon">Aa</button>
|
||||
<button class="icon">Aa</button>
|
||||
<button class="icon">Aa</button>
|
||||
</nav>
|
||||
|
||||
<div class="side-pane">
|
||||
<div class="search">
|
||||
<h1>Search...</h1>
|
||||
</div>
|
||||
|
||||
<div class="list">
|
||||
<div class="item">
|
||||
<img src={image} alt="art"/>
|
||||
<div class="column">
|
||||
<h3>BLUE CLAPPER</h3>
|
||||
<span>hololive IDOL PROJECT</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<img src={image} alt="art"/>
|
||||
<div class="column">
|
||||
<h3>BLUE CLAPPER</h3>
|
||||
<span>hololive IDOL PROJECT</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<img src={image} alt="art"/>
|
||||
<div class="column">
|
||||
<h3>BLUE CLAPPER</h3>
|
||||
<span>hololive IDOL PROJECT</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<img src={image} alt="art"/>
|
||||
<div class="column">
|
||||
<h3>BLUE CLAPPER</h3>
|
||||
<span>hololive IDOL PROJECT</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<img src={image} alt="art"/>
|
||||
<div class="column">
|
||||
<h3>BLUE CLAPPER</h3>
|
||||
<span>hololive IDOL PROJECT</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<img src={image} alt="art"/>
|
||||
<div class="column">
|
||||
<h3>BLUE CLAPPER</h3>
|
||||
<span>hololive IDOL PROJECT</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<img src={image} alt="art"/>
|
||||
<div class="column">
|
||||
<h3>BLUE CLAPPER</h3>
|
||||
<span>hololive IDOL PROJECT</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<img src={image} alt="art"/>
|
||||
<div class="column">
|
||||
<h3>BLUE CLAPPER</h3>
|
||||
<span>hololive IDOL PROJECT</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<img src={image} alt="art"/>
|
||||
<div class="column">
|
||||
<h3>BLUE CLAPPER</h3>
|
||||
<span>hololive IDOL PROJECT</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<img src={image} alt="art"/>
|
||||
<div class="column">
|
||||
<h3>BLUE CLAPPER</h3>
|
||||
<span>hololive IDOL PROJECT</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<img src={image} alt="art"/>
|
||||
<div class="column">
|
||||
<h3>BLUE CLAPPER</h3>
|
||||
<span>hololive IDOL PROJECT</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<img src={image} alt="art"/>
|
||||
<div class="column">
|
||||
<h3>BLUE CLAPPER</h3>
|
||||
<span>hololive IDOL PROJECT</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<img src={image} alt="art"/>
|
||||
<div class="column">
|
||||
<h3>BLUE CLAPPER</h3>
|
||||
<span>hololive IDOL PROJECT</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main class="center">
|
||||
<div class="container">
|
||||
<div class="song">
|
||||
<img src={image} alt="art"/>
|
||||
<h3>BLUE CLAPPER</h3>
|
||||
<span>hololive IDOL PROJECT</span>
|
||||
</div>
|
||||
|
||||
<div class="seeker">
|
||||
<div class="bar">
|
||||
<div class="filling"></div>
|
||||
</div>
|
||||
<div class="time">
|
||||
<span class="currently">0:01:09</span><span class="duration">0:04:20</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<div class="wrapper">
|
||||
<button class="icon"><span>||</span></button>
|
||||
<button class="icon"><span>|<</span></button>
|
||||
<button class="icon"><span>>|</span></button>
|
||||
<div class="dropdown">
|
||||
<button class="icon"><span>Vol</span></button>
|
||||
<div class="menu">
|
||||
<div class="menu-wrapper">
|
||||
<div class="selectors local">
|
||||
<span>Local</span>
|
||||
<div class="bar vertical">
|
||||
<div class="filling"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="selectors global">
|
||||
<span>Global</span>
|
||||
<div class="bar vertical">
|
||||
<div class="filling"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</Gradient>);
|
||||
}
|
||||
182
src/renderer/src/App.tsx
Normal file
182
src/renderer/src/App.tsx
Normal file
@@ -0,0 +1,182 @@
|
||||
import type { JSX } from 'solid-js';
|
||||
import Gradient from './components/Gradient';
|
||||
import image from './assets/hololive IDOL PROJECT - BLUE CLAPPER_2761277.jpg';
|
||||
import { createSignal, onMount } from 'solid-js';
|
||||
|
||||
export default function App(): JSX.Element {
|
||||
const [topColor, setTopColor] = createSignal("dodgerblue");
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener("click", () => {
|
||||
setTopColor("teal");
|
||||
});
|
||||
});
|
||||
|
||||
window.api.listen("log", (...args) => {
|
||||
console.log(args);
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => resolve("poggers"), 500);
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<Gradient bottomColor="crimson" topColor={topColor()}>
|
||||
<div class="app">
|
||||
<nav>
|
||||
<button class="icon">Aa</button>
|
||||
<button class="icon">Aa</button>
|
||||
<button class="icon">Aa</button>
|
||||
<button class="icon">Aa</button>
|
||||
<button class="icon">Aa</button>
|
||||
</nav>
|
||||
|
||||
<div class="side-pane">
|
||||
<div class="search">
|
||||
<h1>Search...</h1>
|
||||
</div>
|
||||
|
||||
<div class="list">
|
||||
<div class="item">
|
||||
<img src={image} alt="art" />
|
||||
<div class="column">
|
||||
<h3>BLUE CLAPPER</h3>
|
||||
<span>hololive IDOL PROJECT</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<img src={image} alt="art" />
|
||||
<div class="column">
|
||||
<h3>BLUE CLAPPER</h3>
|
||||
<span>hololive IDOL PROJECT</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<img src={image} alt="art" />
|
||||
<div class="column">
|
||||
<h3>BLUE CLAPPER</h3>
|
||||
<span>hololive IDOL PROJECT</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<img src={image} alt="art" />
|
||||
<div class="column">
|
||||
<h3>BLUE CLAPPER</h3>
|
||||
<span>hololive IDOL PROJECT</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<img src={image} alt="art" />
|
||||
<div class="column">
|
||||
<h3>BLUE CLAPPER</h3>
|
||||
<span>hololive IDOL PROJECT</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<img src={image} alt="art" />
|
||||
<div class="column">
|
||||
<h3>BLUE CLAPPER</h3>
|
||||
<span>hololive IDOL PROJECT</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<img src={image} alt="art" />
|
||||
<div class="column">
|
||||
<h3>BLUE CLAPPER</h3>
|
||||
<span>hololive IDOL PROJECT</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<img src={image} alt="art" />
|
||||
<div class="column">
|
||||
<h3>BLUE CLAPPER</h3>
|
||||
<span>hololive IDOL PROJECT</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<img src={image} alt="art" />
|
||||
<div class="column">
|
||||
<h3>BLUE CLAPPER</h3>
|
||||
<span>hololive IDOL PROJECT</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<img src={image} alt="art" />
|
||||
<div class="column">
|
||||
<h3>BLUE CLAPPER</h3>
|
||||
<span>hololive IDOL PROJECT</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<img src={image} alt="art" />
|
||||
<div class="column">
|
||||
<h3>BLUE CLAPPER</h3>
|
||||
<span>hololive IDOL PROJECT</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<img src={image} alt="art" />
|
||||
<div class="column">
|
||||
<h3>BLUE CLAPPER</h3>
|
||||
<span>hololive IDOL PROJECT</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<img src={image} alt="art" />
|
||||
<div class="column">
|
||||
<h3>BLUE CLAPPER</h3>
|
||||
<span>hololive IDOL PROJECT</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main class="center">
|
||||
<div class="container">
|
||||
<div class="song">
|
||||
<img src={image} alt="art" />
|
||||
<h3>BLUE CLAPPER</h3>
|
||||
<span>hololive IDOL PROJECT</span>
|
||||
</div>
|
||||
|
||||
<div class="seeker">
|
||||
<div class="bar">
|
||||
<div class="filling"></div>
|
||||
</div>
|
||||
<div class="time">
|
||||
<span class="currently">0:01:09</span><span class="duration">0:04:20</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<div class="wrapper">
|
||||
<button class="icon"><span>||</span></button>
|
||||
<button class="icon"><span>|<</span></button>
|
||||
<button class="icon"><span>>|</span></button>
|
||||
<div class="dropdown">
|
||||
<button class="icon"><span>Vol</span></button>
|
||||
<div class="menu">
|
||||
<div class="menu-wrapper">
|
||||
<div class="selectors local">
|
||||
<span>Local</span>
|
||||
<div class="bar vertical">
|
||||
<div class="filling"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="selectors global">
|
||||
<span>Global</span>
|
||||
<div class="bar vertical">
|
||||
<div class="filling"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</Gradient>
|
||||
);
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 571 KiB |
189
src/renderer/src/assets/index.css
Normal file
189
src/renderer/src/assets/index.css
Normal file
@@ -0,0 +1,189 @@
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-family: Roboto, -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Open Sans', sans-serif;
|
||||
color: #86a5b1;
|
||||
background-color: #2f3241;
|
||||
}
|
||||
|
||||
* {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
code {
|
||||
font-weight: 600;
|
||||
padding: 3px 5px;
|
||||
border-radius: 2px;
|
||||
background-color: #26282e;
|
||||
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
|
||||
font-size: 85%;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #9feaf9;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
border-bottom: 1px solid;
|
||||
}
|
||||
|
||||
.container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 840px;
|
||||
margin: 0 auto;
|
||||
padding: 15px 30px 0 30px;
|
||||
}
|
||||
|
||||
.versions {
|
||||
margin: 0 auto;
|
||||
float: none;
|
||||
clear: both;
|
||||
overflow: hidden;
|
||||
font-family: 'Menlo', 'Lucida Console', monospace;
|
||||
color: #c2f5ff;
|
||||
line-height: 1;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.versions li {
|
||||
display: block;
|
||||
float: left;
|
||||
border-right: 1px solid rgba(194, 245, 255, 0.4);
|
||||
padding: 0 20px;
|
||||
font-size: 13px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.versions li:last-child {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.hero-logo {
|
||||
margin-top: -0.4rem;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
@media (max-width: 840px) {
|
||||
.versions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hero-logo {
|
||||
margin-top: -1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.hero-text {
|
||||
font-weight: 400;
|
||||
color: #c2f5ff;
|
||||
text-align: center;
|
||||
margin-top: -0.5rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 660px) {
|
||||
.hero-logo {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hero-text {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.hero-tagline {
|
||||
text-align: center;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 24px;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.links a {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.links .link-item {
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.features {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: -6px;
|
||||
}
|
||||
|
||||
.features .feature-item {
|
||||
width: 33.33%;
|
||||
box-sizing: border-box;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.features article {
|
||||
background-color: rgba(194, 245, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
box-sizing: border-box;
|
||||
padding: 12px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.features span {
|
||||
color: #d4e8ef;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.features .title {
|
||||
font-size: 17px;
|
||||
font-weight: 500;
|
||||
color: #c2f5ff;
|
||||
line-height: 22px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.features .detail {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 22px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
@media (max-width: 660px) {
|
||||
.features .feature-item {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.links {
|
||||
flex-direction: column;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.links .link-dot {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.features .feature-item {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
32
src/renderer/src/assets/logo.svg
Normal file
32
src/renderer/src/assets/logo.svg
Normal file
@@ -0,0 +1,32 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 900 300">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<g class="hero-apps" style="fill: #71abb7;">
|
||||
<path d="M15 138l-4.9-.64L8 133l-2.1 4.36L1 138l3.6 3.26-.93 4.74L8 143.67l4.33 2.33-.93-4.74z"></path>
|
||||
<path d="M897.2 114.0912l-5.2 3.63v-2.72c0-.55-.45-1-1-1h-8c-.55 0-1 .45-1 1v9c0 .55.45 1 1 1h8c.55 0 1-.45 1-1v-2.72l5.2 3.63c.33.23.8 0 .8-.41v-10c0-.41-.47-.64-.8-.41z"></path>
|
||||
<path d="M65.4 188.625h-1.6c.88 0 1.6-.7313 1.6-1.625v-1.625c0-.8937-.72-1.625-1.6-1.625h-1.6c-.88 0-1.6.7313-1.6 1.625V187c0 .8937.72 1.625 1.6 1.625h-1.6c-.88 0-1.6.7313-1.6 1.625v3.25h1.6v4.875c0 .8937.72 1.625 1.6 1.625h1.6c.88 0 1.6-.7313 1.6-1.625V193.5H67v-3.25c0-.8937-.72-1.625-1.6-1.625zm-3.2-3.25h1.6V187h-1.6v-1.625zm3.2 6.5h-1.6v6.5h-1.6v-6.5h-1.6v-1.625h4.8v1.625zm3.344-5.6875c0-3.2175-2.576-5.8337-5.744-5.8337-3.168 0-5.744 2.6162-5.744 5.8337 0 .455.048.8937.144 1.3162v3.2175c-.976-1.2512-1.6-2.8112-1.6-4.55 0-4.03 3.232-7.3125 7.2-7.3125s7.2 3.2825 7.2 7.3125c0 1.7225-.624 3.2988-1.6 4.55v-3.2175c.096-.4387.144-.8612.144-1.3162zm6.256 0c0 4.68-2.608 8.7425-6.4 10.7738v-1.7063c2.976-1.885 4.944-5.2325 4.944-9.0675 0-5.915-4.72-10.7087-10.544-10.7087-5.824 0-10.544 4.7937-10.544 10.7087 0 3.835 1.968 7.1825 4.944 9.0675v1.7063c-3.792-2.0313-6.4-6.0938-6.4-10.7738C51 179.46 56.376 174 63 174s12 5.46 12 12.1875z"></path>
|
||||
<path d="M830.7143 142.3333c-.8643 0-1.5714.7125-1.5714 1.5834v3.1666c0 .871.707 1.5834 1.5713 1.5834h12.5714c.8643 0 1.5714-.7125 1.5714-1.5834v-3.1666c0-.871-.707-1.5834-1.5713-1.5834h-12.5714zm12.5714 2.771l-1.9643 1.979h-2.357L837 145.1043l-1.9643 1.979h-2.357l-1.9644-1.979v-1.1876h1.1786l1.964 1.979 1.9644-1.979h2.3572l1.9643 1.979 1.964-1.979h1.1787v1.1875zm-9.4286 5.1457h6.286v1.5833h-6.286V150.25zM837 136c-6.0657 0-11 4.6075-11 10.2917v7.125c0 .8708.707 1.5833 1.5714 1.5833h18.8572c.8643 0 1.5714-.7125 1.5714-1.5833v-7.125C848 140.6075 843.0657 136 837 136zm9.4286 17.4167h-18.8572v-7.125c0-4.8925 4.1486-8.851 9.4286-8.851 5.28 0 9.4286 3.9585 9.4286 8.851v7.125z"></path>
|
||||
<path d="M75 91.8065V96h4.1935L90.376 84.8174l-4.1934-4.1935L75 91.8064zm4.1935 2.7957h-2.7957v-2.7957h1.398v1.3978h1.3977v1.398zM93.591 81.6024l-1.817 1.817-4.1935-4.1934 1.817-1.817c.5453-.5453 1.426-.5453 1.971 0l2.2226 2.2224c.5453.5452.5453 1.4258 0 1.971z"></path>
|
||||
<path d="M797 187h4v4h-4v-4zm12-1v19c0 1.1-.9 2-2 2h-20c-1.1 0-2-.9-2-2v-24c0-1.1.9-2 2-2h15l7 7zm-2 1l-6-6h-14v22l6-10 4 8 4-4 6 6v-16z"></path>
|
||||
<path d="M138 125c-6.62 0-12 5-12 11 0 9.04 12 21 12 21s12-11.96 12-21c0-6-5.38-11-12-11zm0 29.1c-3.72-4.06-10-12.22-10-18.1 0-4.96 4.5-9 10-9 2.68 0 5.22.96 7.12 2.72 1.84 1.72 2.88 3.94 2.88 6.28 0 5.88-6.28 14.04-10 18.1zm4-18.1c0 2.22-1.78 4-4 4-2.22 0-4-1.78-4-4 0-2.22 1.78-4 4-4 2.22 0 4 1.78 4 4z"></path>
|
||||
<path d="M771 82h8v2h-8v-2zm0 6h8v-2h-8v2zm0 4h8v-2h-8v2zm22-10h-8v2h8v-2zm0 4h-8v2h8v-2zm0 4h-8v2h8v-2zm4-12v18c0 1.1-.9 2-2 2h-11l-2 2-2-2h-11c-1.1 0-2-.9-2-2V78c0-1.1.9-2 2-2h11l2 2 2-2h11c1.1 0 2 .9 2 2zm-16 1l-1-1h-11v18h12V79zm14-1h-11l-1 1v17h12V78z"></path>
|
||||
<path d="M176 203h-24c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h4v7l7-7h13c1.1 0 2-.9 2-2v-16c0-1.1-.9-2-2-2zm0 18h-14l-4 4v-4h-6v-16h24v16z"></path>
|
||||
<path d="M673 88.921c0 2.18-.9 4.18-2.34 5.66l-1.34-1.34c1.1-1.12 1.78-2.62 1.78-4.32 0-1.7-.68-3.22-1.78-4.32l1.34-1.34c1.44 1.44 2.34 3.44 2.34 5.66zm-8.56-11.48l-7.44 7.44h-4c-1.1 0-2 .9-2 2v4c0 1.1.9 2 2 2h4l7.44 7.44c.94.94 2.56.28 2.56-1.06v-20.76c0-1.34-1.62-2-2.56-1.06zm11.88.16l-1.34 1.34c2.56 2.56 4.12 6.06 4.12 9.96 0 3.88-1.56 7.4-4.12 9.96l1.34 1.34c2.9-2.9 4.68-6.9 4.68-11.32 0-4.44-1.78-8.44-4.68-11.32v.04zm-2.82 2.82l-1.38 1.34c1.84 1.84 2.96 4.38 2.96 7.16 0 2.78-1.12 5.32-2.96 7.12l1.38 1.34c2.16-2.16 3.5-5.16 3.5-8.46 0-3.3-1.34-6.32-3.5-8.5z"></path>
|
||||
<path d="M226 79h-16c0-1.1-.9-2-2-2h-8c-1.1 0-2 .9-2 2-1.1 0-2 .9-2 2v18c0 1.1.9 2 2 2h28c1.1 0 2-.9 2-2V81c0-1.1-.9-2-2-2zm-18 4h-8v-2h8v2zm9 14c-3.88 0-7-3.12-7-7s3.12-7 7-7 7 3.12 7 7-3.12 7-7 7zm5-7c0 2.76-2.26 5-5 5s-5-2.26-5-5 2.26-5 5-5 5 2.26 5 5z"></path>
|
||||
<path d="M725.8393 157h-15.6498c-1.1807 0-1.1807-.82-1.1807-2 0-1.18 0-2 1.1807-2h15.6298C727 153 727 153.82 727 155c0 1.18 0 2-1.1807 2h.02zm-11.6473-10c-1.1807 0-1.1807-.82-1.1807-2 0-1.18 0-2 1.1807-2h11.6273C727 143 727 143.82 727 145c0 1.18 0 2-1.1807 2H714.192zM695 146.82l2.8218-2.6 3.182 3.18 8.185-8.4 2.8218 2.82-11.0068 11-6.0038-6zM710.1895 163h15.6298C727 163 727 163.82 727 165c0 1.18 0 2-1.1807 2h-15.6298c-1.1807 0-1.1807-.82-1.1807-2 0-1.18 0-2 1.1807-2z"></path>
|
||||
<path d="M223 152v24c0 1.65 1.35 3 3 3h36c1.65 0 3-1.35 3-3v-24c0-1.65-1.35-3-3-3h-36c-1.65 0-3 1.35-3 3zm39 0l-18 15-18-15h36zm-36 4.5l12 9-12 9v-18zm3 19.5l10.5-9 4.5 4.5 4.5-4.5 10.5 9h-30zm33-1.5l-12-9 12-9v18z"></path>
|
||||
<path d="M648 182h-3v4.5c0 .84-.66 1.5-1.5 1.5h-6c-.84 0-1.5-.66-1.5-1.5V182h-9v4.5c0 .84-.66 1.5-1.5 1.5h-6c-.84 0-1.5-.66-1.5-1.5V182h-3c-1.65 0-3 1.35-3 3v33c0 1.65 1.35 3 3 3h33c1.65 0 3-1.35 3-3v-33c0-1.65-1.35-3-3-3zm0 36h-33v-27h33v27zm-24-33h-3v-6h3v6zm18 0h-3v-6h3v6zm-15 12h-3v-3h3v3zm6 0h-3v-3h3v3zm6 0h-3v-3h3v3zm6 0h-3v-3h3v3zm-24 6h-3v-3h3v3zm6 0h-3v-3h3v3zm6 0h-3v-3h3v3zm6 0h-3v-3h3v3zm6 0h-3v-3h3v3zm-24 6h-3v-3h3v3zm6 0h-3v-3h3v3zm6 0h-3v-3h3v3zm6 0h-3v-3h3v3zm6 0h-3v-3h3v3zm-24 6h-3v-3h3v3zm6 0h-3v-3h3v3zm6 0h-3v-3h3v3zm6 0h-3v-3h3v3z"></path>
|
||||
</g>
|
||||
<g class="hero-icons" style="fill: #c2f5ff;">
|
||||
<path d="M441.1132 69.724c7.681 0 13.9075-6.207 13.9075-13.8636 0-7.6565-6.2266-13.8634-13.9075-13.8634-7.681 0-13.9076 6.207-13.9076 13.8634 0 7.6566 6.2266 13.8635 13.9076 13.8635zm0-5.7932c-4.4713 0-8.096-3.6132-8.096-8.0704 0-4.457 3.6247-8.0703 8.096-8.0703 4.4712 0 8.096 3.6133 8.096 8.0704 0 4.4572-3.6248 8.0704-8.096 8.0704z"></path>
|
||||
<path d="M354.8995 220.2693c7.681 0 13.9075-6.207 13.9075-13.8635s-6.2266-13.8634-13.9075-13.8634c-7.681 0-13.9075 6.207-13.9075 13.8634 0 7.6566 6.2266 13.8635 13.9075 13.8635zm0-5.793c-4.4713 0-8.096-3.6133-8.096-8.0705 0-4.457 3.6247-8.0703 8.096-8.0703s8.096 3.6132 8.096 8.0703c0 4.4572-3.6247 8.0704-8.096 8.0704z"></path>
|
||||
<path d="M541.0343 206.4058c0-7.6565-6.2266-13.8634-13.9075-13.8634-7.681 0-13.9075 6.207-13.9075 13.8634 0 7.6566 6.2266 13.8635 13.9075 13.8635 7.681 0 13.9075-6.207 13.9075-13.8635zm-5.8115 0c0 4.4572-3.6247 8.0704-8.096 8.0704s-8.096-3.6132-8.096-8.0704c0-4.457 3.6247-8.0703 8.096-8.0703s8.096 3.6132 8.096 8.0703z"></path>
|
||||
<path d="M397.6943 214.5258c9.7012 27.0033 25.5723 43.629 43.419 43.629 13.0157 0 25.0578-8.8443 34.4482-24.4154.827-1.371.3822-3.1507-.9932-3.975-1.3755-.824-3.1607-.3808-3.9876.9902-8.439 13.9938-18.8052 21.6072-29.4675 21.6072-14.8247 0-28.9803-14.8288-37.9476-39.7892-.541-1.506-2.2044-2.2897-3.7153-1.7504-1.511.5394-2.297 2.1975-1.756 3.7036z"></path>
|
||||
<path d="M514.124 163.4733c18.5545-21.85 25.033-43.826 16.122-59.2117-6.557-11.321-20.419-17.2982-38.841-17.537-1.6047-.021-2.9225 1.259-2.9434 2.8586-.0208 1.5996 1.263 2.9132 2.8678 2.934 16.5683.2148 28.5106 5.3642 33.8836 14.641 7.4018 12.7797 1.6243 32.3774-15.5247 52.5722-1.037 1.221-.8844 3.0487.3405 4.0822 1.2248 1.0336 3.0584.8817 4.0952-.3393z"></path>
|
||||
<path d="M411.5672 88.457c-28.3373-5.1448-50.7424.24-59.672 15.6575-6.6635 11.505-4.7588 26.7585 4.6193 43.0637.7982 1.3878 2.574 1.8678 3.966 1.072 1.3923-.7956 1.874-2.5656 1.0756-3.9534-8.4477-14.688-10.0915-27.8524-4.628-37.2857 7.418-12.8074 27.403-17.6105 53.5978-12.8546 1.579.2866 3.092-.7568 3.3794-2.3307.2876-1.5738-.7592-3.082-2.338-3.3687z"></path>
|
||||
<path d="M486.3075 209.2436c5.022-15.998 7.7194-34.453 7.7194-53.6842 0-47.9875-16.849-89.3545-40.8478-99.977-1.4667-.649-3.1837.0098-3.835 1.472-.6512 1.462.01 3.1735 1.4766 3.8227 21.404 9.474 37.3945 48.7337 37.3945 94.6824 0 18.6574-2.612 36.5297-7.454 51.954-.4794 1.5268.3736 3.1518 1.9052 3.6295s3.1617-.3727 3.641-1.8994z"></path>
|
||||
<path d="M466.439 89.4215c-16.7763 3.583-34.6332 10.5886-51.7827 20.4585-42.434 24.4216-70.1147 60.4323-66.2703 86.5432.233 1.5828 1.709 2.6776 3.297 2.4453 1.5877-.2323 2.686-1.7037 2.453-3.2865-3.4135-23.1838 22.825-57.3183 63.426-80.685 16.6365-9.5746 33.9267-16.3578 50.0946-19.811 1.5692-.335 2.5687-1.8748 2.2325-3.439-.336-1.5642-1.8807-2.5606-3.45-2.2255z"></path>
|
||||
<path d="M371.2508 166.997c11.458 12.5516 26.3438 24.3243 43.3203 34.0947 41.106 23.6572 84.866 29.9805 106.4328 15.3217 1.326-.9013 1.668-2.7033.7638-4.025-.904-1.3217-2.712-1.6626-4.0378-.7614-19.302 13.1195-60.871 7.1128-100.253-15.5523-16.469-9.4783-30.8834-20.8782-41.9277-32.9767-1.08-1.1832-2.9178-1.2695-4.1048-.1928-1.187 1.0766-1.2735 2.9086-.1934 4.0918z"></path>
|
||||
<path d="M443.2374 165.3634c-5.432 1.17-10.7838-2.2712-11.9598-7.686-1.1714-5.415 2.2785-10.7498 7.7106-11.922 5.432-1.17 10.7838 2.2712 11.9598 7.686 1.1737 5.415-2.2785 10.7498-7.7106 11.922z"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.8 KiB |
400
src/renderer/src/assets/style.css
Normal file
400
src/renderer/src/assets/style.css
Normal file
@@ -0,0 +1,400 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
||||
|
||||
:root {
|
||||
--FIXED: 0;
|
||||
--NORMAL_CONTENT: 1;
|
||||
--border-radius: 10px;
|
||||
|
||||
--circle-0: dodgerblue;
|
||||
--circle-1: crimson;
|
||||
|
||||
--color-fg: 0, 0, 0;
|
||||
--color-bg: 255, 255, 255;
|
||||
|
||||
--level-bg: 0.64;
|
||||
--level-0: 0.08;
|
||||
--level-1: 0.16;
|
||||
--level-2: 0.24;
|
||||
|
||||
--font-size: 14px;
|
||||
--font-default: 'Inter', sans-serif;
|
||||
--font-monospace: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-fg: 255, 255, 255;
|
||||
--color-bg: 0, 0, 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(var(--color-fg), var(--level-0));
|
||||
border-radius: 999px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(var(--color-fg), var(--level-1));
|
||||
}
|
||||
|
||||
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
|
||||
button {
|
||||
background-color: rgba(var(--color-fg), var(--level-0));
|
||||
color: rgba(var(--color-fg));
|
||||
border: unset;
|
||||
padding: 8px;
|
||||
border-radius: var(--border-radius);
|
||||
outline: 1px solid rgba(var(--color-fg), 0);
|
||||
|
||||
transition: background-color 250ms, outline-color 250ms;
|
||||
}
|
||||
|
||||
button:focus {
|
||||
outline: 1px solid rgba(var(--color-fg), var(--level-2));
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: rgba(var(--color-fg), var(--level-1));
|
||||
cursor: pointer;
|
||||
|
||||
transition: background-color 0ms;
|
||||
}
|
||||
|
||||
button.icon {
|
||||
aspect-ratio: 1/1;
|
||||
font-family: var(--font-monospace);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.bar {
|
||||
--fill-per: 0%;
|
||||
|
||||
min-height: 0.5em;
|
||||
min-width: 100%;
|
||||
border-radius: var(--border-radius);
|
||||
background-color: rgba(var(--color-fg), var(--level-0));
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bar.vertical {
|
||||
min-height: 100%;
|
||||
min-width: 0.5em;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.bar .filling {
|
||||
background-color: rgba(var(--color-fg), var(--level-1));
|
||||
}
|
||||
|
||||
.bar:not(.vertical) .filling {
|
||||
min-height: 0.5em;
|
||||
width: var(--fill-per);
|
||||
}
|
||||
|
||||
.bar.vertical .filling {
|
||||
min-width: 0.5em;
|
||||
height: var(--fill-per);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.dropdown {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dropdown .menu {
|
||||
display: none;
|
||||
position: absolute;
|
||||
padding-bottom: 8px;
|
||||
bottom: 100%;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.dropdown:hover .menu {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dropdown .menu .menu-wrapper {
|
||||
display: flex;
|
||||
background-color: rgba(var(--color-fg), var(--level-0));
|
||||
backdrop-filter: blur(0.5em);
|
||||
padding: 8px;
|
||||
gap: 8px;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.controls .dropdown .menu-wrapper {
|
||||
width: calc(44px + 8px + 44px);
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.controls .dropdown .menu-wrapper .selectors {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.controls .dropdown .menu-wrapper .selectors span {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.controls .dropdown .menu-wrapper .selectors .bar.vertical {
|
||||
width: 0.5em;
|
||||
height: 80px;
|
||||
min-height: unset;
|
||||
min-width: unset;
|
||||
}
|
||||
|
||||
|
||||
|
||||
html {
|
||||
background-color: rgba(var(--color-fg));
|
||||
color: rgba(var(--color-bg));
|
||||
}
|
||||
|
||||
body {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
|
||||
overflow: hidden;
|
||||
font-family: var(--font-default);
|
||||
|
||||
user-select: none;
|
||||
font-size: var(--font-size);
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.gradient {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
background: radial-gradient(
|
||||
circle at top left,
|
||||
var(--circle-0),
|
||||
transparent max(125vw, 125vh)
|
||||
), radial-gradient(
|
||||
circle at bottom right,
|
||||
var(--circle-1),
|
||||
transparent max(125vw, 125vh)
|
||||
);
|
||||
|
||||
transition: background 500ms;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
z-index: var(--NORMAL_CONTENT);
|
||||
overflow: hidden;
|
||||
display: grid;
|
||||
grid-template-columns: 64px 320px 1fr;
|
||||
|
||||
color: rgba(var(--color-fg));
|
||||
background-color: rgba(var(--color-bg), var(--level-bg));
|
||||
/* backdrop-filter: blur(max(25vh, 25vw)); */
|
||||
}
|
||||
|
||||
.app > * {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.app * {
|
||||
z-index: inherit;
|
||||
}
|
||||
|
||||
|
||||
|
||||
nav {
|
||||
background-color: rgba(var(--color-fg), var(--level-1));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 8px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
nav > .icon {
|
||||
width: 100%;
|
||||
aspect-ratio: 1/1;
|
||||
background-color: rgba(var(--color-fg), var(--level-0));
|
||||
border-radius: var(--border-radius);
|
||||
|
||||
transition: background-color 250ms;
|
||||
}
|
||||
|
||||
nav > .icon:hover {
|
||||
background-color: rgba(var(--color-fg), var(--level-1));
|
||||
cursor: pointer;
|
||||
|
||||
transition: background-color 0ms;
|
||||
}
|
||||
|
||||
nav > div.icon {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.side-pane {
|
||||
background-color: rgba(var(--color-fg), var(--level-0));
|
||||
display: grid;
|
||||
grid-template-rows: 64px 1fr;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.side-pane > :not(.no-pd) {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.side-pane .list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.side-pane .list .item {
|
||||
min-height: 80px;
|
||||
width: 100%;
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
grid-template-columns: 80px 1fr;
|
||||
overflow: hidden;
|
||||
font-size: 0.85em;
|
||||
border-radius: var(--border-radius);
|
||||
|
||||
transition: background-color 250ms;
|
||||
}
|
||||
|
||||
.side-pane .list .item:hover {
|
||||
background-color: rgba(var(--color-fg), var(--level-0));
|
||||
|
||||
transition: background-color 0ms;
|
||||
}
|
||||
|
||||
.side-pane .list .item img {
|
||||
height: 80px;
|
||||
padding: 4px;
|
||||
border-radius: var(--border-radius);
|
||||
aspect-ratio: 1/1;
|
||||
place-self: center;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.side-pane .list .item .column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
|
||||
|
||||
main {
|
||||
background-color: rgba(var(--color-fg), 0);
|
||||
}
|
||||
|
||||
main > .container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
main .song {
|
||||
padding: 16px;
|
||||
width: max(232px, 25vw);
|
||||
border-radius: var(--border-radius);
|
||||
transition: background-color 250ms;
|
||||
}
|
||||
|
||||
main .song:hover {
|
||||
background-color: rgba(var(--color-fg), var(--level-0));
|
||||
|
||||
transition: background-color 0ms;
|
||||
}
|
||||
|
||||
main .song img {
|
||||
width: 100%;
|
||||
border-radius: var(--border-radius);
|
||||
aspect-ratio: 1/1;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
main .song h3 {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
main .seeker {
|
||||
width: calc(max(232px, 25vw) - 32px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
main .seeker span {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
main .seeker .time {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
|
||||
|
||||
main .controls {
|
||||
max-width: max(200px, 25vw);
|
||||
}
|
||||
|
||||
main .controls .wrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
main .controls button {
|
||||
width: 44px;
|
||||
}
|
||||
15
src/renderer/src/components/Gradient.tsx
Normal file
15
src/renderer/src/components/Gradient.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Component, createEffect } from 'solid-js';
|
||||
|
||||
const Gradient: Component<{ topColor: string, bottomColor: string, children }> = (props) => {
|
||||
createEffect(() => {
|
||||
document.documentElement.style.setProperty("--circle-0", props.topColor);
|
||||
});
|
||||
|
||||
return (
|
||||
<div class="gradient">
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Gradient;
|
||||
1
src/renderer/src/env.d.ts
vendored
Normal file
1
src/renderer/src/env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
5
src/renderer/src/main.tsx
Normal file
5
src/renderer/src/main.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { render } from 'solid-js/web';
|
||||
import './assets/style.css';
|
||||
import App from './App';
|
||||
|
||||
render(() => <App />, document.getElementById('root') as HTMLElement);
|
||||
4
tsconfig.json
Normal file
4
tsconfig.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [{ "path": "./tsconfig.node.json" }, { "path": "./tsconfig.web.json" }]
|
||||
}
|
||||
8
tsconfig.node.json
Normal file
8
tsconfig.node.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@electron-toolkit/tsconfig/tsconfig.node.json",
|
||||
"include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"types": ["electron-vite/node"]
|
||||
}
|
||||
}
|
||||
19
tsconfig.web.json
Normal file
19
tsconfig.web.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"extends": "@electron-toolkit/tsconfig/tsconfig.web.json",
|
||||
"include": [
|
||||
"src/renderer/src/env.d.ts",
|
||||
"src/renderer/src/**/*",
|
||||
"src/renderer/src/**/*.tsx",
|
||||
"src/preload/*.d.ts"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"jsxImportSource": "solid-js",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@renderer/*": [
|
||||
"src/renderer/src/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user