mirror of
https://github.com/LmeSzinc/StarRailCopilot.git
synced 2024-11-21 16:28:17 +00:00
Add: [ALAS] webapp
This commit is contained in:
parent
0067e343b2
commit
badbe52b11
18
webapp/.editorconfig
Normal file
18
webapp/.editorconfig
Normal file
@ -0,0 +1,18 @@
|
||||
# EditorConfig is awesome: http://EditorConfig.org
|
||||
|
||||
# https://github.com/jokeyrhyme/standard-editorconfig
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# defaults
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
28
webapp/.electron-builder.config.js
Normal file
28
webapp/.electron-builder.config.js
Normal file
@ -0,0 +1,28 @@
|
||||
/**
|
||||
* But currently electron-builder doesn't support ESM configs
|
||||
* @see https://github.com/develar/read-config-file/issues/10
|
||||
*/
|
||||
|
||||
/**
|
||||
* @type {() => import('electron-builder').Configuration}
|
||||
* @see https://www.electron.build/configuration/configuration
|
||||
*/
|
||||
module.exports = async function () {
|
||||
const {getVersion} = await import('./version/getVersion.mjs');
|
||||
|
||||
return {
|
||||
directories: {
|
||||
output: 'dist',
|
||||
buildResources: 'buildResources',
|
||||
},
|
||||
files: ['packages/**/dist/**'],
|
||||
extraMetadata: {
|
||||
version: getVersion(),
|
||||
},
|
||||
|
||||
// Specify linux target just for disabling snap compilation
|
||||
linux: {
|
||||
target: 'deb',
|
||||
},
|
||||
};
|
||||
};
|
0
webapp/.env.development
Normal file
0
webapp/.env.development
Normal file
55
webapp/.eslintrc.json
Normal file
55
webapp/.eslintrc.json
Normal file
@ -0,0 +1,55 @@
|
||||
{
|
||||
"root": true,
|
||||
"env": {
|
||||
"es2021": true,
|
||||
"node": true,
|
||||
"browser": false
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
/** @see https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin#recommended-configs */
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier",
|
||||
"plugin:prettier/recommended"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 12,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"ignorePatterns": ["node_modules/**", "**/dist/**"],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
"argsIgnorePattern": "^_",
|
||||
"varsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
"@typescript-eslint/consistent-type-imports": "error",
|
||||
/**
|
||||
* Having a semicolon helps the optimizer interpret your code correctly.
|
||||
* This avoids rare errors in optimized code.
|
||||
* @see https://twitter.com/alex_kozack/status/1364210394328408066
|
||||
*/
|
||||
"semi": ["error", "always"],
|
||||
/**
|
||||
* This will make the history of changes in the hit a little cleaner
|
||||
*/
|
||||
"comma-dangle": ["warn", "always-multiline"],
|
||||
/**
|
||||
* Just for beauty
|
||||
*/
|
||||
"quotes": [
|
||||
"warn",
|
||||
"single",
|
||||
{
|
||||
"avoidEscape": true
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/ban-types": "off"
|
||||
}
|
||||
}
|
4
webapp/.gitattributes
vendored
Normal file
4
webapp/.gitattributes
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
.github/actions/**/*.js linguist-detectable=false
|
||||
scripts/*.js linguist-detectable=false
|
||||
*.config.js linguist-detectable=false
|
||||
* text=auto eol=lf
|
3
webapp/.github/FUNDING.yml
vendored
Normal file
3
webapp/.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
custom: ["https://www.buymeacoffee.com/kozack/", "https://send.monobank.ua/6SmojkkR9i"]
|
28
webapp/.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
28
webapp/.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: cawa-93
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
5
webapp/.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
webapp/.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Questions & Discussions
|
||||
url: https://github.com/cawa-93/vite-electron-builder/discussions/categories/q-a
|
||||
about: Use GitHub discussions for message-board style questions and discussions.
|
20
webapp/.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
webapp/.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: cawa-93
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
23
webapp/.github/actions/release-notes/action.yml
vendored
Normal file
23
webapp/.github/actions/release-notes/action.yml
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
name: 'Release Notes'
|
||||
description: 'Return release notes based on Git Commits'
|
||||
inputs:
|
||||
from:
|
||||
description: 'Commit from which start log'
|
||||
required: true
|
||||
to:
|
||||
description: 'Commit to which end log'
|
||||
required: true
|
||||
include-commit-body:
|
||||
description: 'Should the commit body be in notes'
|
||||
required: false
|
||||
default: 'false'
|
||||
include-abbreviated-commit:
|
||||
description: 'Should the commit sha be in notes'
|
||||
required: false
|
||||
default: 'true'
|
||||
outputs:
|
||||
release-note: # id of output
|
||||
description: 'Release notes'
|
||||
runs:
|
||||
using: 'node12'
|
||||
main: 'main.js'
|
346
webapp/.github/actions/release-notes/main.js
vendored
Normal file
346
webapp/.github/actions/release-notes/main.js
vendored
Normal file
@ -0,0 +1,346 @@
|
||||
// TODO: Refactor this action
|
||||
|
||||
const {execSync} = require('child_process');
|
||||
|
||||
/**
|
||||
* Gets the value of an input. The value is also trimmed.
|
||||
*
|
||||
* @param name name of the input to get
|
||||
* @param options optional. See InputOptions.
|
||||
* @returns string
|
||||
*/
|
||||
function getInput(name, options) {
|
||||
const val = process.env[`INPUT_${name.replace(/ /g, '_').toUpperCase()}`] || '';
|
||||
if (options && options.required && !val) {
|
||||
throw new Error(`Input required and not supplied: ${name}`);
|
||||
}
|
||||
|
||||
return val.trim();
|
||||
}
|
||||
|
||||
const START_FROM = getInput('from');
|
||||
const END_TO = getInput('to');
|
||||
const INCLUDE_COMMIT_BODY = getInput('include-commit-body') === 'true';
|
||||
const INCLUDE_ABBREVIATED_COMMIT = getInput('include-abbreviated-commit') === 'true';
|
||||
|
||||
/**
|
||||
* @typedef {Object} ICommit
|
||||
* @property {string | undefined} abbreviated_commit
|
||||
* @property {string | undefined} subject
|
||||
* @property {string | undefined} body
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {ICommit & {type: string | undefined, scope: string | undefined}} ICommitExtended
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Any unique string that is guaranteed not to be used in committee text.
|
||||
* Used to split data in the commit line
|
||||
* @type {string}
|
||||
*/
|
||||
const commitInnerSeparator = '~~~~';
|
||||
|
||||
|
||||
/**
|
||||
* Any unique string that is guaranteed not to be used in committee text.
|
||||
* Used to split each commit line
|
||||
* @type {string}
|
||||
*/
|
||||
const commitOuterSeparator = '₴₴₴₴';
|
||||
|
||||
|
||||
/**
|
||||
* Commit data to be obtained.
|
||||
* @type {Map<string, string>}
|
||||
*
|
||||
* @see https://git-scm.com/docs/git-log#Documentation/git-log.txt-emnem
|
||||
*/
|
||||
const commitDataMap = new Map([
|
||||
['subject', '%s'], // Required
|
||||
]);
|
||||
|
||||
if (INCLUDE_COMMIT_BODY) {
|
||||
commitDataMap.set('body', '%b');
|
||||
}
|
||||
|
||||
if (INCLUDE_ABBREVIATED_COMMIT) {
|
||||
commitDataMap.set('abbreviated_commit', '%h');
|
||||
}
|
||||
|
||||
/**
|
||||
* The type used to group commits that do not comply with the convention
|
||||
* @type {string}
|
||||
*/
|
||||
const fallbackType = 'other';
|
||||
|
||||
|
||||
/**
|
||||
* List of all desired commit groups and in what order to display them.
|
||||
* @type {string[]}
|
||||
*/
|
||||
const supportedTypes = [
|
||||
'feat',
|
||||
'fix',
|
||||
'perf',
|
||||
'refactor',
|
||||
'style',
|
||||
'docs',
|
||||
'test',
|
||||
'build',
|
||||
'ci',
|
||||
'chore',
|
||||
'revert',
|
||||
'deps',
|
||||
fallbackType,
|
||||
];
|
||||
|
||||
/**
|
||||
* @param {string} commitString
|
||||
* @returns {ICommit}
|
||||
*/
|
||||
function parseCommit(commitString) {
|
||||
/** @type {ICommit} */
|
||||
const commitDataObj = {};
|
||||
const commitDataArray =
|
||||
commitString
|
||||
.split(commitInnerSeparator)
|
||||
.map(s => s.trim());
|
||||
|
||||
for (const [key] of commitDataMap) {
|
||||
commitDataObj[key] = commitDataArray.shift();
|
||||
}
|
||||
|
||||
return commitDataObj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of commits since the last git tag
|
||||
* @return {ICommit[]}
|
||||
*/
|
||||
function getCommits() {
|
||||
|
||||
const format = Array.from(commitDataMap.values()).join(commitInnerSeparator) + commitOuterSeparator;
|
||||
|
||||
const logs = String(execSync(`git --no-pager log ${START_FROM}..${END_TO} --pretty=format:"${format}" --reverse`));
|
||||
|
||||
return logs
|
||||
.trim()
|
||||
.split(commitOuterSeparator)
|
||||
.filter(r => !!r.trim()) // Skip empty lines
|
||||
.map(parseCommit);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ICommit} commit
|
||||
* @return {ICommitExtended}
|
||||
*/
|
||||
function setCommitTypeAndScope(commit) {
|
||||
|
||||
const matchRE = new RegExp(`^(?:(${supportedTypes.join('|')})(?:\\((\\S+)\\))?:)?(.*)`, 'i');
|
||||
|
||||
let [, type, scope, clearSubject] = commit.subject.match(matchRE);
|
||||
|
||||
/**
|
||||
* Additional rules for checking committees that do not comply with the convention, but for which it is possible to determine the type.
|
||||
*/
|
||||
// Commits like `revert something`
|
||||
if (type === undefined && commit.subject.startsWith('revert')) {
|
||||
type = 'revert';
|
||||
}
|
||||
|
||||
return {
|
||||
...commit,
|
||||
type: (type || fallbackType).toLowerCase().trim(),
|
||||
scope: (scope || '').toLowerCase().trim(),
|
||||
subject: (clearSubject || commit.subject).trim(),
|
||||
};
|
||||
}
|
||||
|
||||
class CommitGroup {
|
||||
constructor() {
|
||||
this.scopes = new Map;
|
||||
this.commits = [];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ICommitExtended[]} array
|
||||
* @param {ICommitExtended} commit
|
||||
*/
|
||||
static _pushOrMerge(array, commit) {
|
||||
const similarCommit = array.find(c => c.subject === commit.subject);
|
||||
if (similarCommit) {
|
||||
if (commit.abbreviated_commit !== undefined) {
|
||||
similarCommit.abbreviated_commit += `, ${commit.abbreviated_commit}`;
|
||||
}
|
||||
} else {
|
||||
array.push(commit);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ICommitExtended} commit
|
||||
*/
|
||||
push(commit) {
|
||||
if (!commit.scope) {
|
||||
CommitGroup._pushOrMerge(this.commits, commit);
|
||||
return;
|
||||
}
|
||||
|
||||
const scope = this.scopes.get(commit.scope) || {commits: []};
|
||||
CommitGroup._pushOrMerge(scope.commits, commit);
|
||||
this.scopes.set(commit.scope, scope);
|
||||
}
|
||||
|
||||
get isEmpty() {
|
||||
return this.commits.length === 0 && this.scopes.size === 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Groups all commits by type and scopes
|
||||
* @param {ICommit[]} commits
|
||||
* @returns {Map<string, CommitGroup>}
|
||||
*/
|
||||
function getGroupedCommits(commits) {
|
||||
const parsedCommits = commits.map(setCommitTypeAndScope);
|
||||
|
||||
const types = new Map(
|
||||
supportedTypes.map(id => ([id, new CommitGroup()])),
|
||||
);
|
||||
|
||||
for (const parsedCommit of parsedCommits) {
|
||||
const typeId = parsedCommit.type;
|
||||
const type = types.get(typeId);
|
||||
type.push(parsedCommit);
|
||||
}
|
||||
|
||||
return types;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return markdown list with commits
|
||||
* @param {ICommitExtended[]} commits
|
||||
* @param {string} pad
|
||||
* @returns {string}
|
||||
*/
|
||||
function getCommitsList(commits, pad = '') {
|
||||
let changelog = '';
|
||||
for (const commit of commits) {
|
||||
changelog += `${pad}- ${commit.subject}.`;
|
||||
|
||||
if (commit.abbreviated_commit !== undefined) {
|
||||
changelog += ` (${commit.abbreviated_commit})`;
|
||||
}
|
||||
|
||||
changelog += '\r\n';
|
||||
|
||||
if (commit.body === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const body = commit.body.replace('[skip ci]', '').trim();
|
||||
if (body !== '') {
|
||||
changelog += `${
|
||||
body
|
||||
.split(/\r*\n+/)
|
||||
.filter(s => !!s.trim())
|
||||
.map(s => `${pad} ${s}`)
|
||||
.join('\r\n')
|
||||
}${'\r\n'}`;
|
||||
}
|
||||
}
|
||||
|
||||
return changelog;
|
||||
}
|
||||
|
||||
|
||||
function replaceHeader(str) {
|
||||
switch (str) {
|
||||
case 'feat':
|
||||
return 'New Features';
|
||||
case 'fix':
|
||||
return 'Bug Fixes';
|
||||
case 'docs':
|
||||
return 'Documentation Changes';
|
||||
case 'build':
|
||||
return 'Build System';
|
||||
case 'chore':
|
||||
return 'Chores';
|
||||
case 'ci':
|
||||
return 'Continuous Integration';
|
||||
case 'refactor':
|
||||
return 'Refactors';
|
||||
case 'style':
|
||||
return 'Code Style Changes';
|
||||
case 'test':
|
||||
return 'Tests';
|
||||
case 'perf':
|
||||
return 'Performance improvements';
|
||||
case 'revert':
|
||||
return 'Reverts';
|
||||
case 'deps':
|
||||
return 'Dependency updates';
|
||||
case 'other':
|
||||
return 'Other Changes';
|
||||
default:
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return markdown string with changelog
|
||||
* @param {Map<string, CommitGroup>} groups
|
||||
*/
|
||||
function getChangeLog(groups) {
|
||||
|
||||
let changelog = '';
|
||||
|
||||
for (const [typeId, group] of groups) {
|
||||
if (group.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
|
||||
changelog += `### ${replaceHeader(typeId)}${'\r\n'}`;
|
||||
|
||||
for (const [scopeId, scope] of group.scopes) {
|
||||
if (scope.commits.length) {
|
||||
changelog += `- #### ${replaceHeader(scopeId)}${'\r\n'}`;
|
||||
changelog += getCommitsList(scope.commits, ' ');
|
||||
}
|
||||
}
|
||||
|
||||
if (group.commits.length) {
|
||||
changelog += getCommitsList(group.commits);
|
||||
}
|
||||
|
||||
changelog += ('\r\n' + '\r\n');
|
||||
}
|
||||
|
||||
return changelog.trim();
|
||||
}
|
||||
|
||||
|
||||
function escapeData(s) {
|
||||
return String(s)
|
||||
.replace(/%/g, '%25')
|
||||
.replace(/\r/g, '%0D')
|
||||
.replace(/\n/g, '%0A');
|
||||
}
|
||||
|
||||
try {
|
||||
const commits = getCommits();
|
||||
const grouped = getGroupedCommits(commits);
|
||||
const changelog = getChangeLog(grouped);
|
||||
process.stdout.write('::set-output name=release-note::' + escapeData(changelog) + '\r\n');
|
||||
// require('fs').writeFileSync('../CHANGELOG.md', changelog, {encoding: 'utf-8'})
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
}
|
27
webapp/.github/renovate.json
vendored
Normal file
27
webapp/.github/renovate.json
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"extends": [
|
||||
"config:base",
|
||||
":semanticCommits",
|
||||
":semanticCommitTypeAll(deps)",
|
||||
":semanticCommitScopeDisabled",
|
||||
":automergeAll",
|
||||
":automergeBranch",
|
||||
":disableDependencyDashboard",
|
||||
":pinVersions",
|
||||
":onlyNpm",
|
||||
":label(dependencies)"
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"groupName": "Vite packages",
|
||||
"matchUpdateTypes": "major",
|
||||
"matchSourceUrlPrefixes": [
|
||||
"https://github.com/vitejs/"
|
||||
]
|
||||
}
|
||||
],
|
||||
"gitNoVerify": [
|
||||
"commit",
|
||||
"push"
|
||||
]
|
||||
}
|
47
webapp/.github/workflows/ci.yml
vendored
Normal file
47
webapp/.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
# This workflow is the entry point for all CI processes.
|
||||
# It is from here that all other workflows are launched.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- 'renovate/**'
|
||||
paths-ignore:
|
||||
- '.github/**'
|
||||
- '!.github/workflows/ci.yml'
|
||||
- '!.github/workflows/typechecking.yml'
|
||||
- '!.github/workflows/tests.yml'
|
||||
- '!.github/workflows/release.yml'
|
||||
- '**.md'
|
||||
- .editorconfig
|
||||
- .gitignore
|
||||
- '.idea/**'
|
||||
- '.vscode/**'
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- '.github/**'
|
||||
- '!.github/workflows/ci.yml'
|
||||
- '!.github/workflows/typechecking.yml'
|
||||
- '!.github/workflows/tests.yml'
|
||||
- '!.github/workflows/release.yml'
|
||||
- '**.md'
|
||||
- .editorconfig
|
||||
- .gitignore
|
||||
- '.idea/**'
|
||||
- '.vscode/**'
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
typechecking:
|
||||
uses: ./.github/workflows/typechecking.yml
|
||||
tests:
|
||||
uses: ./.github/workflows/tests.yml
|
||||
draft_release:
|
||||
with:
|
||||
dry-run: ${{ github.event_name != 'push' || github.ref_name != 'main' }}
|
||||
needs: [ typechecking, tests ]
|
||||
uses: ./.github/workflows/release.yml
|
||||
|
65
webapp/.github/workflows/lint.yml
vendored
Normal file
65
webapp/.github/workflows/lint.yml
vendored
Normal file
@ -0,0 +1,65 @@
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- '**.js'
|
||||
- '**.mjs'
|
||||
- '**.cjs'
|
||||
- '**.jsx'
|
||||
- '**.ts'
|
||||
- '**.mts'
|
||||
- '**.cts'
|
||||
- '**.tsx'
|
||||
- '**.vue'
|
||||
- '**.json'
|
||||
pull_request:
|
||||
paths:
|
||||
- '**.js'
|
||||
- '**.mjs'
|
||||
- '**.cjs'
|
||||
- '**.jsx'
|
||||
- '**.ts'
|
||||
- '**.mts'
|
||||
- '**.cts'
|
||||
- '**.tsx'
|
||||
- '**.vue'
|
||||
- '**.json'
|
||||
|
||||
concurrency:
|
||||
group: lint-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: 'bash'
|
||||
|
||||
jobs:
|
||||
eslint:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16 # Need for npm >=7.7
|
||||
cache: 'npm'
|
||||
|
||||
- run: npm ci
|
||||
env:
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||
|
||||
- run: npm run lint --if-present
|
||||
|
||||
# This job just check code style for in-template contributions.
|
||||
code-style:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16 # Need for npm >=7.7
|
||||
cache: 'npm'
|
||||
|
||||
- run: npm i prettier
|
||||
- run: npx prettier --check "**/*.{js,mjs,cjs,jsx,ts,mts,cts,tsx,vue,json}"
|
61
webapp/.github/workflows/release.yml
vendored
Normal file
61
webapp/.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,61 @@
|
||||
name: Release
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
dry-run:
|
||||
description: 'Compiles the app but not upload artifacts to distribution server'
|
||||
default: false
|
||||
required: false
|
||||
type: boolean
|
||||
|
||||
concurrency:
|
||||
group: release-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: 'bash'
|
||||
|
||||
|
||||
jobs:
|
||||
draft_release:
|
||||
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
os: [ macos-latest, ubuntu-latest, windows-latest ]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16 # Need for npm >=7.7
|
||||
cache: 'npm'
|
||||
|
||||
- run: npm ci
|
||||
env:
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||
|
||||
- run: npm run build
|
||||
|
||||
- name: Compile artifacts ${{ inputs.dry-run && '' || 'and upload them to github release' }}
|
||||
# I use this action because it is capable of retrying multiple times if there are any issues with the distribution server
|
||||
uses: nick-fields/retry@v2
|
||||
with:
|
||||
timeout_minutes: 15
|
||||
max_attempts: 6
|
||||
retry_wait_seconds: 15
|
||||
retry_on: error
|
||||
shell: 'bash'
|
||||
command: npx --no-install electron-builder --config .electron-builder.config.js --publish ${{ inputs.dry-run && 'never' || 'always' }}
|
||||
env:
|
||||
# Code Signing params
|
||||
# See https://www.electron.build/code-signing
|
||||
# CSC_LINK: ''
|
||||
# CSC_KEY_PASSWORD: ''
|
||||
# Publishing artifacts
|
||||
GH_TOKEN: ${{ secrets.github_token }} # GitHub token, automatically provided (No need to define this secret in the repo settings)
|
38
webapp/.github/workflows/tests.yml
vendored
Normal file
38
webapp/.github/workflows/tests.yml
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
name: Tests
|
||||
on: [ workflow_call ]
|
||||
|
||||
concurrency:
|
||||
group: tests-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: 'bash'
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ windows-latest, ubuntu-latest, macos-latest ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
cache: 'npm'
|
||||
- run: npm ci
|
||||
env:
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||
- run: npm run test:main --if-present
|
||||
- run: npm run test:preload --if-present
|
||||
- run: npm run test:renderer --if-present
|
||||
|
||||
# I ran into problems trying to run an electron window in ubuntu due to a missing graphics server.
|
||||
# That's why this special command for Ubuntu is here
|
||||
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test:e2e --if-present
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
|
||||
- run: npm run test:e2e --if-present
|
||||
if: matrix.os != 'ubuntu-latest'
|
27
webapp/.github/workflows/typechecking.yml
vendored
Normal file
27
webapp/.github/workflows/typechecking.yml
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
name: Typechecking
|
||||
on: [ workflow_call ]
|
||||
|
||||
concurrency:
|
||||
group: typechecking-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: 'bash'
|
||||
|
||||
jobs:
|
||||
typescript:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16 # Need for npm >=7.7
|
||||
cache: 'npm'
|
||||
|
||||
- run: npm ci
|
||||
env:
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||
|
||||
- run: npm run typecheck --if-present
|
44
webapp/.github/workflows/update-electron-vendors.yml
vendored
Normal file
44
webapp/.github/workflows/update-electron-vendors.yml
vendored
Normal file
@ -0,0 +1,44 @@
|
||||
name: Update Electon vendors versions
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'package-lock.json'
|
||||
|
||||
|
||||
concurrency:
|
||||
group: update-electron-vendors-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: 'bash'
|
||||
|
||||
|
||||
jobs:
|
||||
node-chrome:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16 # Need for npm >=7.7
|
||||
cache: 'npm'
|
||||
|
||||
# TODO: Install not all dependencies, but only those required for this workflow
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- run: node ./scripts/update-electron-vendors.js
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v3
|
||||
with:
|
||||
delete-branch: true
|
||||
commit-message: Update electron vendors
|
||||
branch: autoupdates/electron-vendors
|
||||
title: Update electron vendors
|
||||
body: Updated versions of electron vendors in `electron-vendors.config.json` and `.browserslistrc` files
|
58
webapp/.gitignore
vendored
Normal file
58
webapp/.gitignore
vendored
Normal file
@ -0,0 +1,58 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
*.local
|
||||
thumbs.db
|
||||
|
||||
.eslintcache
|
||||
.browserslistrc
|
||||
.electron-vendors.cache.json
|
||||
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
.idea/artifacts
|
||||
.idea/compiler.xml
|
||||
.idea/jarRepositories.xml
|
||||
.idea/modules.xml
|
||||
.idea/*.iml
|
||||
.idea/modules
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
/.idea/csv-plugin.xml
|
24
webapp/.nano-staged.mjs
Normal file
24
webapp/.nano-staged.mjs
Normal file
@ -0,0 +1,24 @@
|
||||
import {resolve, sep} from 'path';
|
||||
|
||||
export default {
|
||||
'*.{js,mjs,cjs,ts,mts,cts,vue}': 'eslint --cache --fix',
|
||||
|
||||
/**
|
||||
* Run typechecking if any type-sensitive files or project dependencies was changed
|
||||
* @param {string[]} filenames
|
||||
* @return {string[]}
|
||||
*/
|
||||
'{package-lock.json,packages/**/{*.ts,*.vue,tsconfig.json}}': ({filenames}) => {
|
||||
// if dependencies was changed run type checking for all packages
|
||||
if (filenames.some(f => f.endsWith('package-lock.json'))) {
|
||||
return ['npm run typecheck --if-present'];
|
||||
}
|
||||
|
||||
// else run type checking for staged packages
|
||||
const fileNameToPackageName = filename =>
|
||||
filename.replace(resolve(process.cwd(), 'packages') + sep, '').split(sep)[0];
|
||||
return [...new Set(filenames.map(fileNameToPackageName))].map(
|
||||
p => `npm run typecheck:${p} --if-present`,
|
||||
);
|
||||
},
|
||||
};
|
1
webapp/.npmrc
Normal file
1
webapp/.npmrc
Normal file
@ -0,0 +1 @@
|
||||
auto-install-peers=true
|
10
webapp/.prettierignore
Normal file
10
webapp/.prettierignore
Normal file
@ -0,0 +1,10 @@
|
||||
**/node_modules
|
||||
**/dist
|
||||
**/*.svg
|
||||
|
||||
package.json
|
||||
package-lock.json
|
||||
.electron-vendors.cache.json
|
||||
|
||||
.github
|
||||
.idea
|
21
webapp/.prettierrc
Normal file
21
webapp/.prettierrc
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"printWidth": 100,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/*.css", "**/*.scss", "**/*.html"],
|
||||
"options": {
|
||||
"singleQuote": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": false,
|
||||
"arrowParens": "avoid",
|
||||
"proseWrap": "never",
|
||||
"htmlWhitespaceSensitivity": "strict",
|
||||
"vueIndentScriptAndStyle": false,
|
||||
"endOfLine": "lf",
|
||||
"singleAttributePerLine": true
|
||||
}
|
3
webapp/.simple-git-hooks.json
Normal file
3
webapp/.simple-git-hooks.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"pre-commit": "npx nano-staged"
|
||||
}
|
13
webapp/.vscode/launch.json
vendored
Normal file
13
webapp/.vscode/launch.json
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Debug Main Process",
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"program": "${workspaceFolder}\\scripts\\watch.mjs",
|
||||
"autoAttachChildProcesses": true
|
||||
}
|
||||
]
|
||||
}
|
47
webapp/.yarnclean
Normal file
47
webapp/.yarnclean
Normal file
@ -0,0 +1,47 @@
|
||||
# test directories
|
||||
__tests__
|
||||
test
|
||||
tests
|
||||
powered-test
|
||||
|
||||
# asset directories
|
||||
docs
|
||||
website
|
||||
images
|
||||
assets
|
||||
|
||||
# examples
|
||||
example
|
||||
examples
|
||||
|
||||
# code coverage directories
|
||||
coverage
|
||||
.nyc_output
|
||||
|
||||
# build scripts
|
||||
Makefile
|
||||
Gulpfile.js
|
||||
Gruntfile.js
|
||||
|
||||
# configs
|
||||
appveyor.yml
|
||||
circle.yml
|
||||
codeship-services.yml
|
||||
codeship-steps.yml
|
||||
wercker.yml
|
||||
.tern-project
|
||||
.gitattributes
|
||||
.editorconfig
|
||||
.*ignore
|
||||
.eslintrc
|
||||
.jshintrc
|
||||
.flowconfig
|
||||
.documentup.json
|
||||
.yarn-metadata.json
|
||||
.travis.yml
|
||||
|
||||
# misc
|
||||
*.md
|
||||
LICENSE
|
||||
*.txt
|
||||
!path.txt
|
21
webapp/LICENSE
Normal file
21
webapp/LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Alex Kozack
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
297
webapp/README.md
Normal file
297
webapp/README.md
Normal file
@ -0,0 +1,297 @@
|
||||
# Vite Electron Builder Boilerplate
|
||||
|
||||
This is a template for secure electron applications. Written following the latest safety requirements, recommendations
|
||||
and best practices.
|
||||
|
||||
Under the hood is [Vite] — A next-generation blazing fast bundler, and [electron-builder] for packaging.
|
||||
|
||||
## Get started
|
||||
|
||||
Follow these steps to get started with the template:
|
||||
|
||||
1. Click the **[Use this template](https://github.com/cawa-93/vite-electron-builder/generate)** button (you must be
|
||||
logged in) or just clone this repo.
|
||||
2. If you want to use another package manager you may need to edit [`.github/workflows`](/.github/workflows) — [it
|
||||
uses `npm` by default](https://github.com/search?q=npm+repo%3Acawa-93%2Fvite-electron-builder+path%3A.github%2Fworkflows&type=Code&ref=advsearch&l=&l=).
|
||||
3. If you like this template, don't forget to give a github star or send support! ⭐♥
|
||||
|
||||
That's all you need. 😉
|
||||
|
||||
> **Note**:
|
||||
> This template uses npm v7 feature — [**Installing Peer Dependencies
|
||||
Automatically**](https://github.com/npm/rfcs/blob/latest/implemented/0025-install-peer-deps.md). If you are using a
|
||||
different package manager, you may need to install some peerDependencies manually.
|
||||
|
||||
## Features
|
||||
|
||||
### Electron [![Electron version](https://img.shields.io/github/package-json/dependency-version/cawa-93/vite-electron-builder/dev/electron?label=%20)][electron]
|
||||
|
||||
- This template uses the latest electron version with all the latest security patches.
|
||||
- The architecture of the application is built according to the
|
||||
security [guides](https://www.electronjs.org/docs/tutorial/security) and best practices.
|
||||
- The latest version of the [electron-builder] is used to package the application.
|
||||
|
||||
### Vite [![Vite version](https://img.shields.io/github/package-json/dependency-version/cawa-93/vite-electron-builder/dev/vite?label=%20)][vite]
|
||||
|
||||
- [Vite] is used to bundle all source codes. It's an extremely fast bundler, that has a vast array of amazing features.
|
||||
You can learn more about how it is arranged in [this](https://www.youtube.com/watch?v=xXrhg26VCSc) video.
|
||||
- Vite [supports](https://vitejs.dev/guide/env-and-mode.html) reading `.env` files. You can also specify the types of
|
||||
your environment variables in [`types/env.d.ts`](types/env.d.ts).
|
||||
- Automatic hot-reloads for the `Main` and `Renderer` processes.
|
||||
|
||||
Vite provides many useful features, such as: `TypeScript`, `TSX/JSX`, `CSS/JSON Importing`, `CSS Modules`
|
||||
, `Web Assembly` and much more.
|
||||
|
||||
> [See all Vite features](https://vitejs.dev/guide/features.html).
|
||||
|
||||
### TypeScript [![TypeScript version](https://img.shields.io/github/package-json/dependency-version/cawa-93/vite-electron-builder/dev/typescript?label=%20)][typescript] (optional)
|
||||
|
||||
- The latest version of TypeScript is used for all the source code.
|
||||
- **Vite** supports TypeScript out of the box. However, it does not support type checking.
|
||||
- Code formatting rules follow the latest TypeScript recommendations and best practices thanks
|
||||
to [@typescript-eslint/eslint-plugin](https://www.npmjs.com/package/@typescript-eslint/eslint-plugin).
|
||||
|
||||
> [Guide to disable typescript and remove dependencies](https://github.com/cawa-93/vite-electron-builder/discussions/339)
|
||||
|
||||
### Vue [![Vue version](https://img.shields.io/github/package-json/dependency-version/cawa-93/vite-electron-builder/dev/vue?label=%20&)][vue] (optional)
|
||||
|
||||
- By default, web pages are built using [Vue]. However, you can easily change that. Or not use additional frameworks at
|
||||
all.
|
||||
- Code formatting rules follow the latest Vue recommendations and best practices thanks to [eslint-plugin-vue].
|
||||
|
||||
> [Find more forks 🔱 for others frameworks or setups](https://github.com/cawa-93/vite-electron-builder/discussions/categories/forks)
|
||||
|
||||
### Continuous Integration
|
||||
|
||||
- The configured workflow will check the types for each push and PR.
|
||||
- The configured workflow will check the code style for each push and PR.
|
||||
- **Automatic tests**
|
||||
used [Vitest ![Vitest version](https://img.shields.io/github/package-json/dependency-version/cawa-93/vite-electron-builder/dev/vitest?label=%20&color=yellow)][vitest]
|
||||
-- A blazing fast test framework powered by Vite.
|
||||
- Unit tests are placed within each package and are ran separately.
|
||||
- End-to-end tests are placed in the root [`tests`](tests) directory and use [playwright].
|
||||
|
||||
![Workflow graph](https://user-images.githubusercontent.com/1662812/213429323-ef4bcc87-c273-4f2f-b77f-c04cf6dbc36d.png)
|
||||
|
||||
|
||||
### Publishing
|
||||
|
||||
- Each time you push changes to the `main` branch, the [`release`](.github/workflows/release.yml) workflow starts, which creates a new draft release. For each next commit will be created and replaced artifacts. That way you will always have draft with latest artifacts, and the release can be published once it is ready.
|
||||
- Code signing supported. See [`release` workflow](.github/workflows/release.yml).
|
||||
- **Auto-update is supported**. After the release is published, all client applications will download the new version
|
||||
and install updates silently.
|
||||
|
||||
> **Note**:
|
||||
> This template **configured only for GitHub public repository**, but electron-builder also supports other update distribution servers. Find more in [electron-builder docs](https://www.electron.build/configuration/publish).
|
||||
|
||||
## How it works
|
||||
|
||||
The template requires a minimum amount [dependencies](package.json). Only **Vite** is used for building, nothing more.
|
||||
|
||||
### Project Structure
|
||||
|
||||
The structure of this template is very similar to a monorepo. The entire source code of the project is divided into three modules (packages) that are each bundled independently:
|
||||
|
||||
- [`packages/renderer`](packages/renderer). Responsible for the contents of the application window. In fact, it is a
|
||||
regular web application. In developer mode, you can even open it in a browser. The development and build process is
|
||||
the same as for classic web applications. Access to low-level API electrons or Node.js is done through the _preload_
|
||||
layer.
|
||||
- [`packages/preload`](packages/preload). Contain Electron [**preload scripts**](https://www.electronjs.org/docs/latest/tutorial/tutorial-preload). Acts as an intermediate bridge between the _renderer_ process and the API
|
||||
exposed by electron and Node.js. Runs in an _isolated browser context_, but has direct access to the full Node.js
|
||||
functionality.
|
||||
- [`packages/main`](packages/main)
|
||||
Contain Electron [**main script**](https://www.electronjs.org/docs/tutorial/quick-start#create-the-main-script-file). This is
|
||||
the main process that powers the application. It manages creating and handling the spawned BrowserWindow, setting and
|
||||
enforcing secure permissions and request handlers. You can also configure it to do much more as per your need, such
|
||||
as: logging, reporting statistics and health status among others.
|
||||
|
||||
Schematically, the structure of the application and the method of communication between packages can be depicted as follows:
|
||||
```mermaid
|
||||
flowchart TB;
|
||||
|
||||
packages/preload <-. IPC Messages .-> packages/main
|
||||
|
||||
subgraph packages/main["packages/main (Shared beatween all windows)"]
|
||||
M[index.ts] --> EM[Electron Main Process Modules]
|
||||
M --> N2[Node.js API]
|
||||
end
|
||||
|
||||
subgraph Window["Browser Window"]
|
||||
subgraph packages/preload["packages/preload (Works in isolated context)"]
|
||||
P[index.ts] --> N[Node.js API]
|
||||
P --> ED[External dependencies]
|
||||
P --> ER[Electron Renderer Process Modules]
|
||||
end
|
||||
|
||||
|
||||
subgraph packages/renderer
|
||||
R[index.html] --> W[Web API]
|
||||
R --> BD[Bundled dependencies]
|
||||
R --> F[Web Frameforks]
|
||||
end
|
||||
end
|
||||
|
||||
packages/renderer -- Call Exposed API --> P
|
||||
```
|
||||
### Build web resources
|
||||
|
||||
The `main` and `preload` packages are built in [library mode](https://vitejs.dev/guide/build.html#library-mode) as it is
|
||||
simple javascript.
|
||||
The `renderer` package builds as a regular web app.
|
||||
|
||||
### Compile App
|
||||
|
||||
The next step is to package a ready to distribute Electron app for macOS, Windows and Linux with "auto update" support
|
||||
out of the box.
|
||||
|
||||
To do this, use [electron-builder]:
|
||||
|
||||
- Using the npm script `compile`: This script is configured to compile the application as quickly as possible. It is not
|
||||
ready for distribution, it is compiled only for the current platform and is used for debugging.
|
||||
- Using GitHub Actions: The application is compiled for any platform and ready-to-distribute files are automatically
|
||||
added as a draft to the GitHub releases page.
|
||||
|
||||
### Working with dependencies
|
||||
|
||||
Because the `renderer` works and builds like a _regular web application_, you can only use dependencies that support the
|
||||
browser or compile to a browser-friendly format.
|
||||
|
||||
This means that in the `renderer` you are free to use any frontend dependencies such as Vue, React, lodash, axios and so
|
||||
on. However, you _CANNOT_ use any native Node.js APIs, such as, `systeminformation`. These APIs are _only_ available in
|
||||
a Node.js runtime environment and will cause your application to crash if used in the `renderer` layer. Instead, if you
|
||||
need access to Node.js runtime APIs in your frontend, export a function form the `preload` package.
|
||||
|
||||
All dependencies that require Node.js api can be used in
|
||||
the [`preload` script](https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts).
|
||||
|
||||
#### Expose in main world
|
||||
Here is an example. Let's say you need to read some data from the file system or database in the renderer.
|
||||
|
||||
In the preload context, create a function that reads and returns data. To make the function announced in the preload
|
||||
available in the render, you usually need to call
|
||||
the [`electron.contextBridge.exposeInMainWorld`](https://www.electronjs.org/ru/docs/latest/api/context-bridge). However,
|
||||
this template uses the [unplugin-auto-expose](https://github.com/cawa-93/unplugin-auto-expose) plugin, so you just need
|
||||
to export the method from the preload. The `exposeInMainWorld` will be called automatically.
|
||||
|
||||
```ts
|
||||
// preload/index.ts
|
||||
import { readFile } from 'node:fs/promises';
|
||||
|
||||
// Encapsulate types if you use typescript
|
||||
interface UserData {
|
||||
prop: string
|
||||
}
|
||||
|
||||
// Encapsulate all node.js api
|
||||
// Everything you exported from preload/index.ts may be called in renderer
|
||||
export function getUserData(): Promise<UserData> {
|
||||
return readFile('/path/to/file/in/user/filesystem.json', {encoding:'utf8'}).then(JSON.parse);
|
||||
}
|
||||
```
|
||||
|
||||
Now you can import and call the method in renderer
|
||||
|
||||
```ts
|
||||
// renderer/anywere/component.ts
|
||||
import { getUserData } from '#preload'
|
||||
const userData = await getUserData()
|
||||
```
|
||||
|
||||
> Find more in [Context Isolation tutorial](https://www.electronjs.org/docs/tutorial/context-isolation#security-considerations).
|
||||
|
||||
### Working with Electron API
|
||||
|
||||
Although the preload has access to all of Node.js's API, it **still runs in the BrowserWindow context**, so a limited
|
||||
electron modules are available in it. Check the [electron docs](https://www.electronjs.org/ru/docs/latest/api/clipboard)
|
||||
for full list of available methods.
|
||||
|
||||
All other electron methods can be invoked in the `main`.
|
||||
|
||||
As a result, the architecture of interaction between all modules is as follows:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
renderer->>+preload: Read data from file system
|
||||
preload->>-renderer: Data
|
||||
renderer->>preload: Maximize window
|
||||
activate preload
|
||||
preload-->>main: Invoke IPC command
|
||||
activate main
|
||||
main-->>preload: IPC response
|
||||
deactivate main
|
||||
preload->>renderer: Window maximized
|
||||
deactivate preload
|
||||
```
|
||||
|
||||
> Find more in [Inter-Process Communication tutorial](https://www.electronjs.org/docs/latest/tutorial/ipc).
|
||||
|
||||
### Modes and Environment Variables
|
||||
|
||||
All environment variables are set as part of the `import.meta`, so you can access them vie the following
|
||||
way: `import.meta.env`.
|
||||
|
||||
> **Note**:
|
||||
> If you are using TypeScript and want to get code completion you must add all the environment variables to
|
||||
the [`ImportMetaEnv` in `types/env.d.ts`](types/env.d.ts).
|
||||
|
||||
The mode option is used to specify the value of `import.meta.env.MODE` and the corresponding environment variables files
|
||||
that need to be loaded.
|
||||
|
||||
By default, there are two modes:
|
||||
|
||||
- `production` is used by default
|
||||
- `development` is used by `npm run watch` script
|
||||
|
||||
When running the build script, the environment variables are loaded from the following files in your project root:
|
||||
|
||||
```
|
||||
.env # loaded in all cases
|
||||
.env.local # loaded in all cases, ignored by git
|
||||
.env.[mode] # only loaded in specified env mode
|
||||
.env.[mode].local # only loaded in specified env mode, ignored by git
|
||||
```
|
||||
|
||||
> **Warning**:
|
||||
> To prevent accidentally leaking env variables to the client, only variables prefixed with `VITE_` are exposed to your
|
||||
Vite-processed code.
|
||||
|
||||
For example let's take the following `.env` file:
|
||||
|
||||
```
|
||||
DB_PASSWORD=foobar
|
||||
VITE_SOME_KEY=123
|
||||
```
|
||||
|
||||
Only `VITE_SOME_KEY` will be exposed as `import.meta.env.VITE_SOME_KEY` to your client source code, but `DB_PASSWORD`
|
||||
will not.
|
||||
|
||||
You can change that prefix or add another. See [`envPrefix`](https://vitejs.dev/config/shared-options.html#envprefix)
|
||||
|
||||
## Contribution
|
||||
|
||||
See [Contributing Guide](contributing.md).
|
||||
|
||||
|
||||
[vite]: https://github.com/vitejs/vite/
|
||||
|
||||
[electron]: https://github.com/electron/electron
|
||||
|
||||
[electron-builder]: https://github.com/electron-userland/electron-builder
|
||||
|
||||
[vue]: https://github.com/vuejs/vue-next
|
||||
|
||||
[vue-router]: https://github.com/vuejs/vue-router-next/
|
||||
|
||||
[typescript]: https://github.com/microsoft/TypeScript/
|
||||
|
||||
[playwright]: https://playwright.dev
|
||||
|
||||
[vitest]: https://vitest.dev
|
||||
|
||||
[vue-tsc]: https://github.com/johnsoncodehk/vue-tsc
|
||||
|
||||
[eslint-plugin-vue]: https://github.com/vuejs/eslint-plugin-vue
|
||||
|
||||
[cawa-93-github]: https://github.com/cawa-93/
|
||||
|
||||
[cawa-93-sponsor]: https://www.patreon.com/Kozack/
|
0
webapp/buildResources/.gitkeep
Normal file
0
webapp/buildResources/.gitkeep
Normal file
BIN
webapp/buildResources/icon.icns
Normal file
BIN
webapp/buildResources/icon.icns
Normal file
Binary file not shown.
BIN
webapp/buildResources/icon.ico
Normal file
BIN
webapp/buildResources/icon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 193 KiB |
BIN
webapp/buildResources/icon.png
Normal file
BIN
webapp/buildResources/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
34
webapp/contributing.md
Normal file
34
webapp/contributing.md
Normal file
@ -0,0 +1,34 @@
|
||||
# Contributing
|
||||
|
||||
First and foremost, thank you! We appreciate that you want to contribute to vite-electron-builder, your time is
|
||||
valuable, and your contributions mean a lot to us.
|
||||
|
||||
## Issues
|
||||
|
||||
Do not create issues about bumping dependencies unless a bug has been identified, and you can demonstrate that it
|
||||
effects this library.
|
||||
|
||||
**Help us to help you**
|
||||
|
||||
Remember that we’re here to help, but not to make guesses about what you need help with:
|
||||
|
||||
- Whatever bug or issue you're experiencing, assume that it will not be as obvious to the maintainers as it is to you.
|
||||
- Spell it out completely. Keep in mind that maintainers need to think about _all potential use cases_ of a library.
|
||||
It's important that you explain how you're using a library so that maintainers can make that connection and solve the
|
||||
issue.
|
||||
|
||||
_It can't be understated how frustrating and draining it can be to maintainers to have to ask clarifying questions on
|
||||
the most basic things, before it's even possible to start debugging. Please try to make the best use of everyone's time
|
||||
involved, including yourself, by providing this information up front._
|
||||
|
||||
## Repo Setup
|
||||
|
||||
The package manager used to install and link dependencies must be npm v7 or later.
|
||||
|
||||
1. Clone repo
|
||||
1. `npm run watch` start electron app in watch mode.
|
||||
1. `npm run compile` build app but for local debugging only.
|
||||
1. `npm run lint` lint your code.
|
||||
1. `npm run typecheck` Run typescript check.
|
||||
1. `npm run test` Run app test.
|
||||
1. `npm run format` Reformat all codebase to project code style.
|
23
webapp/electron-builder.config.js
Normal file
23
webapp/electron-builder.config.js
Normal file
@ -0,0 +1,23 @@
|
||||
if (process.env.VITE_APP_VERSION === undefined) {
|
||||
const now = new Date();
|
||||
process.env.VITE_APP_VERSION = `${now.getUTCFullYear() - 2000}.${
|
||||
now.getUTCMonth() + 1
|
||||
}.${now.getUTCDate()}-${now.getUTCHours() * 60 + now.getUTCMinutes()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {import('electron-builder').Configuration}
|
||||
* @see https://www.electron.build/configuration/configuration
|
||||
*/
|
||||
const config = {
|
||||
directories: {
|
||||
output: 'dist',
|
||||
buildResources: 'buildResources',
|
||||
},
|
||||
files: ['packages/**/dist/**'],
|
||||
extraMetadata: {
|
||||
version: process.env.VITE_APP_VERSION,
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = config;
|
4
webapp/electron-vendors.config.json
Normal file
4
webapp/electron-vendors.config.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"chrome": "94",
|
||||
"node": "16"
|
||||
}
|
10990
webapp/package-lock.json
generated
Normal file
10990
webapp/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
87
webapp/package.json
Normal file
87
webapp/package.json
Normal file
@ -0,0 +1,87 @@
|
||||
{
|
||||
"name": "alas",
|
||||
"description": "AzurLaneAutoScript desktop app",
|
||||
"version": "0.4.0",
|
||||
"private": true,
|
||||
"main": "packages/main/dist/index.cjs",
|
||||
"scripts": {
|
||||
"build": "npm run build:main && npm run build:preload && npm run build:renderer",
|
||||
"build:main": "cd ./packages/main && vite build",
|
||||
"build:preload": "cd ./packages/preload && vite build",
|
||||
"build:renderer": "cd ./packages/renderer && vite build",
|
||||
"compile": "cross-env MODE=production npm run build && electron-builder build --config .electron-builder.config.js --dir",
|
||||
"test": "npm run test:main && npm run test:preload && npm run test:renderer && npm run test:e2e",
|
||||
"test:e2e": "npm run build && vitest run",
|
||||
"test:main": "vitest run -r packages/main --passWithNoTests",
|
||||
"test:preload": "vitest run -r packages/preload --passWithNoTests",
|
||||
"test:renderer": "vitest run -r packages/renderer --passWithNoTests",
|
||||
"watch": "node scripts/watch.mjs",
|
||||
"lint": "eslint . --ext js,mjs,cjs,ts,mts,cts,vue",
|
||||
"typecheck:main": "tsc --noEmit -p packages/main/tsconfig.json",
|
||||
"typecheck:preload": "tsc --noEmit -p packages/preload/tsconfig.json",
|
||||
"typecheck:renderer": "vue-tsc --noEmit -p packages/renderer/tsconfig.json",
|
||||
"typecheck": "npm run typecheck:main && npm run typecheck:preload && npm run typecheck:renderer",
|
||||
"postinstall": "cross-env ELECTRON_RUN_AS_NODE=1 electron scripts/update-electron-vendors.mjs",
|
||||
"format": "npx prettier --write \"**/*.{js,mjs,cjs,ts,mts,cts,vue,json}\""
|
||||
},
|
||||
"devDependencies": {
|
||||
"@arco-design/web-vue": "^2.45.2",
|
||||
"@arco-plugins/vite-vue": "^1.4.5",
|
||||
"@arco-themes/vue-am-alas": "^0.0.1",
|
||||
"@intlify/unplugin-vue-i18n": "^0.10.0",
|
||||
"@types/fs-extra": "^11.0.1",
|
||||
"@types/lodash-es": "^4.17.7",
|
||||
"@types/node": "18.15.3",
|
||||
"@typescript-eslint/eslint-plugin": "5.55.0",
|
||||
"@typescript-eslint/parser": "^5.58.0",
|
||||
"@vitejs/plugin-vue": "4.0.0",
|
||||
"@vue/test-utils": "2.3.1",
|
||||
"consola": "^3.1.0",
|
||||
"cross-env": "7.0.3",
|
||||
"electron": "22.3.4",
|
||||
"electron-builder": "23.6.0",
|
||||
"eslint": "8.36.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-vue": "9.9.0",
|
||||
"happy-dom": "8.9.0",
|
||||
"less": "^4.1.3",
|
||||
"nano-staged": "0.8.0",
|
||||
"playwright": "1.31.2",
|
||||
"prettier": "^2.8.7",
|
||||
"prettier-eslint": "^15.0.1",
|
||||
"simple-git-hooks": "2.8.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "4.9.5",
|
||||
"unocss": "^0.51.8",
|
||||
"unplugin-auto-expose": "0.0.4",
|
||||
"unplugin-auto-import": "^0.15.3",
|
||||
"unplugin-vue-components": "^0.24.1",
|
||||
"vite": "^4.3.1",
|
||||
"vite-plugin-style-import": "^2.0.0",
|
||||
"vitest": "0.29.3",
|
||||
"vue": "^3.2.47",
|
||||
"vue-tsc": "1.2.0",
|
||||
"vue-types": "^5.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons-vue": "^6.1.0",
|
||||
"@vueuse/core": "^10.0.2",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"dayjs": "^1.11.7",
|
||||
"electron-log": "^4.4.8",
|
||||
"electron-updater": "5.3.0",
|
||||
"fast-glob": "^3.2.12",
|
||||
"fs-extra": "^11.1.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mustache": "^4.2.0",
|
||||
"nanoid": "^4.0.2",
|
||||
"pinia": "^2.0.34",
|
||||
"postcss": "^8.4.21",
|
||||
"python-shell": "^5.0.0",
|
||||
"tree-kill": "^1.2.2",
|
||||
"vue-i18n": "^9.2.2",
|
||||
"vue-router": "^4.1.6",
|
||||
"yaml": "^2.2.1"
|
||||
}
|
||||
}
|
28
webapp/packages/common/constant/config.ts
Normal file
28
webapp/packages/common/constant/config.ts
Normal file
@ -0,0 +1,28 @@
|
||||
export const ALAS_CONFIG_YAML = 'deploy.yaml';
|
||||
export const ALAS_CONFIG_TEMPLATE_YAML = 'deploy.template.yaml';
|
||||
export const ALAS_CONFIG_TEST_TEMPLATE_YAML = 'deploy.test.template.yaml';
|
||||
export const ALAS_INSTR_FILE = 'installer.py';
|
||||
export const ALAS_RELAUNCH_ARGV = '--relaunch';
|
||||
|
||||
export const RepositoryMap = {
|
||||
china: 'cn',
|
||||
global: 'global',
|
||||
};
|
||||
|
||||
export const GitExecutableMap = {
|
||||
windows: './toolkit/Git/mingw64/bin/git.exe',
|
||||
macos: '/usr/bin/git',
|
||||
linux: '/usr/bin/git',
|
||||
};
|
||||
|
||||
export const AdbExecutableMap = {
|
||||
windows: './toolkit/Lib/site-packages/adbutils/binaries/adb.exe',
|
||||
macos: '/usr/bin/adb',
|
||||
linux: '/usr/bin/adb',
|
||||
};
|
||||
|
||||
export const PythonExecutableMap = {
|
||||
windows: './toolkit/python.exe',
|
||||
macos: '/usr/bin/python',
|
||||
linux: '/usr/bin/python',
|
||||
};
|
9
webapp/packages/common/constant/eventNames.ts
Normal file
9
webapp/packages/common/constant/eventNames.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export const UPDATE_APP = 'Update app.asar';
|
||||
export const ALAS_LOG = 'alas-log';
|
||||
export const ALAS_CONFIG = 'alas-config';
|
||||
export const WINDOW_READY = 'window-ready';
|
||||
export const INSTALLER_READY = 'installer-ready';
|
||||
export const ALAS_READY = 'alas-config-path';
|
||||
export const ELECTRON_THEME = 'electron-theme';
|
||||
|
||||
export const PAGE_ERROR = 'page-error';
|
10
webapp/packages/common/constant/theme.ts
Normal file
10
webapp/packages/common/constant/theme.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export const ThemeObj: {[k in string]: 'light' | 'dark'} = {
|
||||
default: 'light',
|
||||
light: 'light',
|
||||
dark: 'dark',
|
||||
};
|
||||
|
||||
export const AlasGuiTheme = {
|
||||
light: 'default',
|
||||
dark: 'dark',
|
||||
};
|
13
webapp/packages/common/utils/checkIsFirst.ts
Normal file
13
webapp/packages/common/utils/checkIsFirst.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import getAlasABSPath from './getAlasABSPath';
|
||||
import fs from 'fs';
|
||||
import {join} from 'path';
|
||||
import {ALAS_CONFIG_TEMPLATE_YAML, ALAS_CONFIG_TEST_TEMPLATE_YAML} from '../constant/config';
|
||||
export function checkIsFirst(): boolean {
|
||||
const absPath = getAlasABSPath();
|
||||
return fs.existsSync(
|
||||
join(
|
||||
absPath,
|
||||
`/config/${import.meta.env.DEV ? ALAS_CONFIG_TEST_TEMPLATE_YAML : ALAS_CONFIG_TEMPLATE_YAML}`,
|
||||
),
|
||||
);
|
||||
}
|
35
webapp/packages/common/utils/copyFilesToDir.ts
Normal file
35
webapp/packages/common/utils/copyFilesToDir.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import type {CopyOptions} from 'fs-extra';
|
||||
import fsExtra from 'fs-extra';
|
||||
import {join, sep, normalize} from 'path';
|
||||
|
||||
export interface CopyToDirOptions {
|
||||
successCallback?: (pathStr: string) => void;
|
||||
filedCallback?: (pathStr: string, error: any) => void;
|
||||
fsExtraOptions?: CopyOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param pathList
|
||||
* @param targetDirPath
|
||||
* @param options
|
||||
*/
|
||||
export async function copyFilesToDir(
|
||||
pathList: string[],
|
||||
targetDirPath: string,
|
||||
options?: CopyToDirOptions | undefined,
|
||||
) {
|
||||
const {fsExtraOptions, successCallback, filedCallback} = options || {};
|
||||
for (const pathStr of pathList) {
|
||||
try {
|
||||
await fsExtra.copy(
|
||||
pathStr,
|
||||
join(normalize(targetDirPath) + sep + pathStr.split(sep).pop()),
|
||||
fsExtraOptions,
|
||||
);
|
||||
successCallback?.(pathStr);
|
||||
} catch (err) {
|
||||
filedCallback?.(pathStr, err);
|
||||
}
|
||||
}
|
||||
}
|
3
webapp/packages/common/utils/env.ts
Normal file
3
webapp/packages/common/utils/env.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export const isWindows = process.platform === 'win32';
|
||||
export const isMacintosh = process.platform === 'darwin';
|
||||
export const isLinux = process.platform === 'linux';
|
72
webapp/packages/common/utils/getAlasABSPath.ts
Normal file
72
webapp/packages/common/utils/getAlasABSPath.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import {app} from 'electron';
|
||||
import {isMacintosh} from './env';
|
||||
import fs from 'fs';
|
||||
/**
|
||||
* Get the absolute path of the project root directory
|
||||
* @param files
|
||||
* @param rootName
|
||||
*/
|
||||
const getAlasABSPath = (
|
||||
files: string[] = ['**/config/deploy.yaml', '**/config/deploy.template.yaml'],
|
||||
rootName: string | string[] = ['AzurLaneAutoScript', 'Alas'],
|
||||
) => {
|
||||
const path = require('path');
|
||||
const sep = path.sep;
|
||||
const fg = require('fast-glob');
|
||||
let appAbsPath = process.cwd();
|
||||
if (isMacintosh && import.meta.env.PROD) {
|
||||
appAbsPath = app?.getAppPath() || process.execPath;
|
||||
}
|
||||
|
||||
while (fs.lstatSync(appAbsPath).isFile()) {
|
||||
appAbsPath = appAbsPath.split(sep).slice(0, -1).join(sep);
|
||||
}
|
||||
|
||||
let alasABSPath = '';
|
||||
|
||||
let hasRootName = false;
|
||||
|
||||
if (typeof rootName === 'string') {
|
||||
hasRootName = appAbsPath.includes(rootName);
|
||||
} else if (Array.isArray(rootName)) {
|
||||
hasRootName = rootName.some(item =>
|
||||
appAbsPath.toLocaleLowerCase().includes(item.toLocaleLowerCase()),
|
||||
);
|
||||
}
|
||||
|
||||
if (hasRootName) {
|
||||
const appAbsPathArr = appAbsPath.split(sep);
|
||||
let flag = false;
|
||||
while (hasRootName && !flag) {
|
||||
const entries = fg.sync(files, {
|
||||
dot: true,
|
||||
cwd: appAbsPathArr.join(sep) as string,
|
||||
});
|
||||
if (entries.length > 0) {
|
||||
flag = true;
|
||||
alasABSPath = appAbsPathArr.join(sep);
|
||||
}
|
||||
appAbsPathArr.pop();
|
||||
}
|
||||
} else {
|
||||
let step = 4;
|
||||
const appAbsPathArr = appAbsPath.split(sep);
|
||||
let flag = false;
|
||||
while (step > 0 && !flag) {
|
||||
appAbsPathArr.pop();
|
||||
const entries = fg.sync(files, {
|
||||
dot: true,
|
||||
cwd: appAbsPathArr.join(sep) as string,
|
||||
});
|
||||
if (entries.length > 0) {
|
||||
flag = true;
|
||||
alasABSPath = appAbsPathArr.join(sep);
|
||||
}
|
||||
step--;
|
||||
}
|
||||
}
|
||||
|
||||
return alasABSPath.endsWith(sep) ? alasABSPath : alasABSPath + sep;
|
||||
};
|
||||
|
||||
export default getAlasABSPath;
|
7
webapp/packages/common/utils/index.ts
Normal file
7
webapp/packages/common/utils/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import getAlasABSPath from './getAlasABSPath';
|
||||
import {checkIsFirst} from './checkIsFirst';
|
||||
import {copyFilesToDir} from './copyFilesToDir';
|
||||
import {modifyYaml} from './modifyYaml';
|
||||
import {isMacintosh, isLinux, isWindows} from './env';
|
||||
|
||||
export {getAlasABSPath, checkIsFirst, copyFilesToDir, modifyYaml, isWindows, isLinux, isMacintosh};
|
25
webapp/packages/common/utils/modifyYaml.ts
Normal file
25
webapp/packages/common/utils/modifyYaml.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import type {Pair} from 'yaml';
|
||||
import {parseDocument, visit} from 'yaml';
|
||||
|
||||
const fs = require('fs');
|
||||
/**
|
||||
* Modify yaml file https://eemeli.org/yaml/#modifying-nodes
|
||||
* @param filePath
|
||||
* @param keyObj
|
||||
*/
|
||||
export function modifyYaml(filePath: string, keyObj: {[k in string]: any}) {
|
||||
try {
|
||||
const doc = parseDocument(fs.readFileSync(filePath, 'utf8'));
|
||||
const keysMap = new Map(Object.entries(keyObj));
|
||||
visit(doc, {
|
||||
Pair: (_node, pair: Pair<any, any>) => {
|
||||
if (keysMap.has(pair?.key?.value)) {
|
||||
pair.value.value = keysMap.get(pair.key.value);
|
||||
}
|
||||
},
|
||||
});
|
||||
fs.writeFileSync(filePath, doc.toString(), 'utf8');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
51
webapp/packages/common/utils/validate.ts
Normal file
51
webapp/packages/common/utils/validate.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import fsExtra from 'fs-extra';
|
||||
import {join} from 'path';
|
||||
import {
|
||||
AdbExecutableMap,
|
||||
ALAS_CONFIG_TEMPLATE_YAML,
|
||||
ALAS_CONFIG_YAML,
|
||||
GitExecutableMap,
|
||||
PythonExecutableMap,
|
||||
RepositoryMap,
|
||||
} from '../constant/config';
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
import mustache from 'mustache';
|
||||
import {isMacintosh} from './env';
|
||||
import {app} from 'electron';
|
||||
|
||||
/**
|
||||
* 校验配置文件是否存在 不存在则根据系统版本,语言环境进行创建
|
||||
* @param dirPath
|
||||
*/
|
||||
export function validateConfigFile(dirPath: string) {
|
||||
const targetPath = join(dirPath, ALAS_CONFIG_YAML);
|
||||
const result = fsExtra.existsSync(targetPath);
|
||||
if (result) return true;
|
||||
/**
|
||||
* TODO 创建配置文件
|
||||
*/
|
||||
fsExtra.ensureFileSync(targetPath);
|
||||
const resultTpl = fsExtra.existsSync(join(dirPath, ALAS_CONFIG_TEMPLATE_YAML));
|
||||
if (resultTpl) {
|
||||
fsExtra.copyFileSync(join(dirPath, ALAS_CONFIG_TEMPLATE_YAML), join(dirPath, ALAS_CONFIG_YAML));
|
||||
return true;
|
||||
}
|
||||
const tpl = fsExtra.readFileSync('./deploy.yaml.tpl', {encoding: 'utf-8'});
|
||||
const system = isMacintosh ? 'macos' : 'windows';
|
||||
const localCode = app.getLocaleCountryCode().toLocaleLowerCase();
|
||||
let local: 'global' | 'china' = 'global';
|
||||
if (localCode === 'cn') {
|
||||
local = 'china';
|
||||
}
|
||||
const deployTpl = mustache.render(tpl, {
|
||||
repository: RepositoryMap[local],
|
||||
gitExecutable: GitExecutableMap[system],
|
||||
pythonExecutable: PythonExecutableMap[system],
|
||||
adbExecutable: AdbExecutableMap[system],
|
||||
language: local === 'china' ? 'zh-CN' : 'en-US',
|
||||
theme: 'default',
|
||||
});
|
||||
fsExtra.writeFileSync(targetPath, deployTpl, {encoding: 'utf-8'});
|
||||
return true;
|
||||
}
|
165
webapp/packages/main/public/deploy.yaml.tpl
Normal file
165
webapp/packages/main/public/deploy.yaml.tpl
Normal file
@ -0,0 +1,165 @@
|
||||
Deploy:
|
||||
Git:
|
||||
# URL of AzurLaneAutoScript repository
|
||||
# [CN user] Use 'git://git.lyoko.io/AzurLaneAutoScript' for faster and more stable download
|
||||
# [Other] Use 'https://github.com/LmeSzinc/AzurLaneAutoScript'
|
||||
Repository: {{repository}}
|
||||
# Branch of Alas
|
||||
# [Developer] Use 'dev', 'app', etc, to try new features
|
||||
# [Other] Use 'master', the stable branch
|
||||
Branch: master
|
||||
# Filepath of git executable `git.exe`
|
||||
# [Easy installer] Use './toolkit/Git/mingw64/bin/git.exe'
|
||||
# [Other] Use you own git
|
||||
GitExecutable: {{gitExecutable}}
|
||||
# Set git proxy
|
||||
# [CN user] Use your local http proxy (http://127.0.0.1:{port}) or socks5 proxy (socks5://127.0.0.1:{port})
|
||||
# [Other] Use null
|
||||
GitProxy: null
|
||||
# Set SSL Verify
|
||||
# [In most cases] Use true
|
||||
# [Other] Use false to when connected to an untrusted network
|
||||
SSLVerify: true
|
||||
# Update Alas at startup
|
||||
# [In most cases] Use true
|
||||
AutoUpdate: true
|
||||
# Whether to keep local changes during update
|
||||
# User settings, logs and screenshots will be kept, no mather this is true or false
|
||||
# [Developer] Use true, if you modified the code
|
||||
# [Other] Use false
|
||||
KeepLocalChanges: false
|
||||
|
||||
Python:
|
||||
# Filepath of python executable `python.exe`
|
||||
# [Easy installer] Use './toolkit/python.exe'
|
||||
# [Other] Use you own python, and its version should be 3.7.6 64bit
|
||||
PythonExecutable: {{pythonExecutable}}
|
||||
# URL of pypi mirror
|
||||
# [CN user] Use 'https://pypi.tuna.tsinghua.edu.cn/simple' for faster and more stable download
|
||||
# [Other] Use null
|
||||
PypiMirror: null
|
||||
# Install dependencies at startup
|
||||
# [In most cases] Use true
|
||||
InstallDependencies: true
|
||||
# Path to requirements.txt
|
||||
# [In most cases] Use 'requirements.txt'
|
||||
# [In AidLux] Use './deploy/AidLux/{version}/requirements.txt', version is default to 0.92
|
||||
RequirementsFile: requirements.txt
|
||||
|
||||
Adb:
|
||||
# Filepath of ADB executable `adb.exe`
|
||||
# [Easy installer] Use './toolkit/Lib/site-packages/adbutils/binaries/adb.exe'
|
||||
# [Other] Use you own latest ADB, but not the ADB in your emulator
|
||||
AdbExecutable: {{adbExecutable}}
|
||||
# Whether to replace ADB
|
||||
# Chinese emulators (NoxPlayer, LDPlayer, MemuPlayer, MuMuPlayer) use their own ADB, instead of the latest.
|
||||
# Different ADB servers will terminate each other at startup, resulting in disconnection.
|
||||
# For compatibility, we have to replace them all.
|
||||
# This will do:
|
||||
# 1. Terminate current ADB server
|
||||
# 2. Rename ADB from all emulators to *.bak and replace them by the AdbExecutable set above
|
||||
# 3. Brute-force connect to all available emulator instances
|
||||
# [In most cases] Use true
|
||||
# [In few cases] Use false, if you have other programs using ADB.
|
||||
ReplaceAdb: true
|
||||
# Brute-force connect to all available emulator instances
|
||||
# [In most cases] Use true
|
||||
AutoConnect: true
|
||||
# Re-install uiautomator2
|
||||
# [In most cases] Use true
|
||||
InstallUiautomator2: true
|
||||
|
||||
Ocr:
|
||||
# Run Ocr as a service, can reduce memory usage by not import mxnet everytime you start an alas instance
|
||||
|
||||
# Whether to use ocr server
|
||||
# [Default] false
|
||||
UseOcrServer: false
|
||||
# Whether to start ocr server when start GUI
|
||||
# [Default] false
|
||||
StartOcrServer: false
|
||||
# Port of ocr server runs by GUI
|
||||
# [Default] 22268
|
||||
OcrServerPort: 22268
|
||||
# Address of ocr server for alas instance to connect
|
||||
# [Default] 127.0.0.1:22268
|
||||
OcrClientAddress: 127.0.0.1:22268
|
||||
|
||||
Update:
|
||||
# Use auto update and builtin updater feature
|
||||
# This may cause problem https://github.com/LmeSzinc/AzurLaneAutoScript/issues/876
|
||||
EnableReload: true
|
||||
# Check update every X minute
|
||||
# [Disable] 0
|
||||
# [Default] 5
|
||||
CheckUpdateInterval: 5
|
||||
# Scheduled restart time
|
||||
# If there are updates, Alas will automatically restart and update at this time every day
|
||||
# and run all alas instances that running before restarted
|
||||
# [Disable] null
|
||||
# [Default] 03:50
|
||||
AutoRestartTime: 03:50
|
||||
|
||||
Misc:
|
||||
# Enable discord rich presence
|
||||
DiscordRichPresence: false
|
||||
|
||||
RemoteAccess:
|
||||
# Enable remote access (using ssh reverse tunnel serve by https://github.com/wang0618/localshare)
|
||||
# ! You need to set Password below to enable remote access since everyone can access to your alas if they have your url.
|
||||
# See here (http://app.azurlane.cloud/en.html) for more infomation.
|
||||
EnableRemoteAccess: false
|
||||
# Username when login into ssh server
|
||||
# [Default] null (will generate a random one when startup)
|
||||
SSHUser: null
|
||||
# Server to connect
|
||||
# [Default] null
|
||||
# [Format] host:port
|
||||
SSHServer: null
|
||||
# Filepath of SSH executable `ssh.exe`
|
||||
# [Default] ssh (find ssh in system PATH)
|
||||
# If you don't have one, install OpenSSH or download it here (https://github.com/PowerShell/Win32-OpenSSH/releases)
|
||||
SSHExecutable: ssh
|
||||
|
||||
Webui:
|
||||
# --host. Host to listen
|
||||
# [Use IPv6] '::'
|
||||
# [In most cases] Default to '0.0.0.0'
|
||||
WebuiHost: 0.0.0.0
|
||||
# --port. Port to listen
|
||||
# You will be able to access webui via `http://{host}:{port}`
|
||||
# [In most cases] Default to 22267
|
||||
WebuiPort: 22267
|
||||
# Language to use on web ui
|
||||
# 'zh-CN' for Chinese simplified
|
||||
# 'en-US' for English
|
||||
# 'ja-JP' for Japanese
|
||||
# 'zh-TW' for Chinese traditional
|
||||
Language: {{language}}
|
||||
# Theme of web ui
|
||||
# 'default' for light theme
|
||||
# 'dark' for dark theme
|
||||
Theme: {{theme}}
|
||||
# Follow system DPI scaling
|
||||
# [In most cases] true
|
||||
# [In few cases] false to make Alas smaller, if you have a low resolution but high DPI scaling.
|
||||
DpiScaling: true
|
||||
# --key. Password of web ui
|
||||
# Useful when expose Alas to the public network
|
||||
Password: null
|
||||
# --cdn. Use jsdelivr cdn for pywebio static files (css, js).
|
||||
# 'true' for jsdelivr cdn
|
||||
# 'false' for self host cdn (automatically)
|
||||
# 'https://path.to.your/cdn' to use custom cdn
|
||||
CDN: false
|
||||
# --run. Auto-run specified config when startup
|
||||
# 'null' default no specified config
|
||||
# '["alas"]' specified "alas" config
|
||||
# '["alas","alas2"]' specified "alas" "alas2" configs
|
||||
Run: null
|
||||
# To update app.asar
|
||||
# [In most cases] true
|
||||
AppAsarUpdate: true
|
||||
# --no-sandbox. https://github.com/electron/electron/issues/30966
|
||||
# Some Windows systems cannot call the GPU normally for virtualization, and you need to manually turn off sandbox mode
|
||||
NoSandbox: false
|
BIN
webapp/packages/main/public/icon.png
Normal file
BIN
webapp/packages/main/public/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 123 KiB |
49
webapp/packages/main/src/addIpcMainListener.ts
Normal file
49
webapp/packages/main/src/addIpcMainListener.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import type {CoreService} from '/@/coreService';
|
||||
import type {BrowserWindow} from 'electron';
|
||||
import {app, ipcMain, nativeTheme} from 'electron';
|
||||
import {
|
||||
ELECTRON_THEME,
|
||||
INSTALLER_READY,
|
||||
PAGE_ERROR,
|
||||
WINDOW_READY,
|
||||
} from '@common/constant/eventNames';
|
||||
import {ThemeObj} from '@common/constant/theme';
|
||||
import logger from '/@/logger';
|
||||
|
||||
export const addIpcMainListener = async (mainWindow: BrowserWindow, coreService: CoreService) => {
|
||||
// Minimize, maximize, close window.
|
||||
ipcMain.on('window-tray', function () {
|
||||
mainWindow?.hide();
|
||||
});
|
||||
ipcMain.on('window-minimize', function () {
|
||||
mainWindow?.minimize();
|
||||
});
|
||||
ipcMain.on('window-maximize', function () {
|
||||
mainWindow?.isMaximized() ? mainWindow?.restore() : mainWindow?.maximize();
|
||||
});
|
||||
ipcMain.on('window-close', function () {
|
||||
coreService?.kill();
|
||||
mainWindow?.close();
|
||||
app.exit(0);
|
||||
});
|
||||
|
||||
ipcMain.on(WINDOW_READY, async function (_, args) {
|
||||
logger.info('-----WINDOW_READY-----');
|
||||
args && (await coreService.run());
|
||||
});
|
||||
|
||||
ipcMain.on(INSTALLER_READY, function () {
|
||||
logger.info('-----INSTALLER_READY-----');
|
||||
coreService.next();
|
||||
});
|
||||
|
||||
ipcMain.on(ELECTRON_THEME, (_, args) => {
|
||||
logger.info('-----ELECTRON_THEME-----');
|
||||
nativeTheme.themeSource = ThemeObj[args];
|
||||
});
|
||||
|
||||
ipcMain.on(PAGE_ERROR, (_, args) => {
|
||||
logger.info('-----PAGE_ERROR-----');
|
||||
logger.error(args);
|
||||
});
|
||||
};
|
85
webapp/packages/main/src/config.ts
Normal file
85
webapp/packages/main/src/config.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import {isMacintosh} from '@common/utils/env';
|
||||
import getAlasABSPath from '@common/utils/getAlasABSPath';
|
||||
import {ALAS_INSTR_FILE} from '@common/constant/config';
|
||||
import {validateConfigFile} from '@common/utils/validate';
|
||||
import {join} from 'path';
|
||||
import logger from '/@/logger';
|
||||
|
||||
const yaml = require('yaml');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function getAlasPath() {
|
||||
let file;
|
||||
const currentFilePath = process.cwd();
|
||||
const pathLookup = [
|
||||
// Current
|
||||
'./',
|
||||
// Running from AzurLaneAutoScript/toolkit/WebApp/alas.exe
|
||||
'../../',
|
||||
// Running from AzurLaneAutoScript/webapp/dist/win-unpacked/alas.exe
|
||||
'../../../',
|
||||
// Running from `yarn watch`
|
||||
'./../',
|
||||
];
|
||||
for (const i in pathLookup) {
|
||||
file = path.join(currentFilePath, pathLookup[i], './config/deploy.yaml');
|
||||
if (fs.existsSync(file)) {
|
||||
return path.join(currentFilePath, pathLookup[i]);
|
||||
}
|
||||
}
|
||||
for (const i in pathLookup) {
|
||||
file = path.join(currentFilePath, pathLookup[i], './config/deploy.template.yaml');
|
||||
if (fs.existsSync(file)) {
|
||||
return path.join(currentFilePath, pathLookup[i]);
|
||||
}
|
||||
}
|
||||
return currentFilePath;
|
||||
}
|
||||
|
||||
function getLauncherPath(alasPath: string) {
|
||||
const pathLookup = ['./Alas.exe', './Alas.bat', './deploy/launcher/Alas.bat'];
|
||||
for (const i in pathLookup) {
|
||||
const file = path.join(alasPath, pathLookup[i]);
|
||||
if (fs.existsSync(file)) {
|
||||
return path.join(alasPath, pathLookup[i]);
|
||||
}
|
||||
}
|
||||
return path.join(alasPath, './Alas.exe');
|
||||
}
|
||||
|
||||
export const alasPath = isMacintosh && import.meta.env.PROD ? getAlasABSPath() : getAlasPath();
|
||||
|
||||
try {
|
||||
validateConfigFile(join(alasPath, '/config'));
|
||||
} catch (e) {
|
||||
logger.error((e as unknown as any).toString());
|
||||
}
|
||||
|
||||
const file = fs.readFileSync(path.join(alasPath, './config/deploy.yaml'), 'utf8');
|
||||
const config = yaml.parse(file) as DefAlasConfig;
|
||||
const PythonExecutable = config.Deploy.Python.PythonExecutable;
|
||||
const WebuiPort = config.Deploy.Webui.WebuiPort.toString();
|
||||
const Theme = config.Deploy.Webui.Theme;
|
||||
|
||||
export const ThemeObj: {[k in string]: 'light' | 'dark'} = {
|
||||
default: 'light',
|
||||
light: 'light',
|
||||
dark: 'dark',
|
||||
system: 'light',
|
||||
};
|
||||
|
||||
export const pythonPath = path.isAbsolute(PythonExecutable)
|
||||
? PythonExecutable
|
||||
: path.join(alasPath, PythonExecutable);
|
||||
export const installerPath = ALAS_INSTR_FILE;
|
||||
export const installerArgs = import.meta.env.DEV ? ['--print-test'] : [];
|
||||
export const webuiUrl = `http://127.0.0.1:${WebuiPort}`;
|
||||
export const webuiPath = 'gui.py';
|
||||
export const webuiArgs = ['--port', WebuiPort, '--electron'];
|
||||
export const dpiScaling =
|
||||
Boolean(config.Deploy.Webui.DpiScaling) || config.Deploy.Webui.DpiScaling === undefined;
|
||||
|
||||
export const webuiTheme = ThemeObj[Theme] || 'light';
|
||||
|
||||
export const noSandbox = config.Deploy.Webui.NoSandbox;
|
87
webapp/packages/main/src/coreService.ts
Normal file
87
webapp/packages/main/src/coreService.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import type {PyShell} from '/@/pyshell';
|
||||
import {createAlas, createInstaller} from '/@/serviceLogic';
|
||||
import {ALAS_LOG} from '@common/constant/eventNames';
|
||||
import {BrowserWindow} from 'electron';
|
||||
import logger from '/@/logger';
|
||||
|
||||
export interface CoreServiceOption {
|
||||
appABSPath?: string;
|
||||
isFirstRun?: boolean;
|
||||
theme?: 'light' | 'dark';
|
||||
mainWindow: Electron.BrowserWindow | null;
|
||||
}
|
||||
|
||||
const defOptions = {
|
||||
appABSPath: '',
|
||||
theme: 'light',
|
||||
isFirstRun: false,
|
||||
mainWindow: BrowserWindow.getAllWindows()[0] || null,
|
||||
};
|
||||
|
||||
export type CallbackFun<T = any> = (
|
||||
coreService: CoreService,
|
||||
next: (...args: any[]) => Promise<PyShell | null>,
|
||||
...args1: (any | T)[]
|
||||
) => Promise<PyShell | null>;
|
||||
|
||||
export class CoreService {
|
||||
public appABSPath: string;
|
||||
public theme = 'light';
|
||||
public mainWindow: Electron.BrowserWindow | null = null;
|
||||
private currentService: PyShell | null = null;
|
||||
private eventQueue: Array<CallbackFun> = [createInstaller, createAlas];
|
||||
private stepIndex = 0;
|
||||
|
||||
constructor(options?: CoreServiceOption) {
|
||||
const {appABSPath, theme, mainWindow} = Object.assign(defOptions, options || {});
|
||||
this.appABSPath = appABSPath;
|
||||
this.theme = theme;
|
||||
this.mainWindow = mainWindow;
|
||||
}
|
||||
|
||||
async run(...rags: any[]) {
|
||||
logger.info('---------------run---------------');
|
||||
logger.info('stepIndex:' + this.stepIndex);
|
||||
const cb = this.eventQueue[this.stepIndex++];
|
||||
const next = (...rags1: any[]) => {
|
||||
return this.run(...rags1);
|
||||
};
|
||||
try {
|
||||
cb && (this.currentService = await cb(this, next, ...rags));
|
||||
} catch (e) {
|
||||
/**
|
||||
* 1. 事件执行失败,记录日志
|
||||
*/
|
||||
logger.error('currentService:' + (e as unknown as any).toString());
|
||||
}
|
||||
return this.curService;
|
||||
}
|
||||
|
||||
async next(...rags: any[]) {
|
||||
return this.run(...rags);
|
||||
}
|
||||
|
||||
get curService() {
|
||||
return this.currentService;
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.stepIndex = 0;
|
||||
}
|
||||
|
||||
sendLaunchLog(message: string) {
|
||||
if (!this.mainWindow || this.mainWindow.isDestroyed()) return;
|
||||
logger.info(`pyShellLaunch: ${message}`);
|
||||
this.mainWindow?.webContents.send(ALAS_LOG, message);
|
||||
}
|
||||
|
||||
kill(callback?: () => void) {
|
||||
this.curService?.kill(callback || this.cb);
|
||||
}
|
||||
|
||||
cb() {
|
||||
/**
|
||||
*
|
||||
*/
|
||||
}
|
||||
}
|
67
webapp/packages/main/src/createApp.ts
Normal file
67
webapp/packages/main/src/createApp.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import {createMainWindow} from '/@/createMainWindow';
|
||||
import {addIpcMainListener} from '/@/addIpcMainListener';
|
||||
import {CoreService} from '/@/coreService';
|
||||
import logger from '/@/logger';
|
||||
import {app, nativeImage, Tray} from 'electron';
|
||||
import {join} from 'node:path';
|
||||
import {isMacintosh} from '@common/utils';
|
||||
export const createApp = async () => {
|
||||
logger.info('-----createApp-----');
|
||||
logger.info('-----createMainWindow-----');
|
||||
const mainWindow = await createMainWindow();
|
||||
const coreService = new CoreService({mainWindow});
|
||||
|
||||
// Hide menu
|
||||
const {Menu} = require('electron');
|
||||
Menu.setApplicationMenu(null);
|
||||
const icon = nativeImage.createFromPath(join(__dirname, './icon.png'));
|
||||
const dockerIcon = icon.resize({width: 16, height: 16});
|
||||
// Tray
|
||||
const tray = new Tray(isMacintosh ? dockerIcon : icon);
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: 'Show',
|
||||
click: function () {
|
||||
mainWindow?.show();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Hide',
|
||||
click: function () {
|
||||
mainWindow?.hide();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Exit',
|
||||
click: function () {
|
||||
coreService.curService?.kill(() => {
|
||||
logger.info('kill coreService');
|
||||
});
|
||||
app.quit();
|
||||
process.exit(0);
|
||||
},
|
||||
},
|
||||
]);
|
||||
tray.setToolTip('Alas');
|
||||
tray.setContextMenu(contextMenu);
|
||||
tray.on('click', () => {
|
||||
if (mainWindow?.isVisible()) {
|
||||
if (mainWindow?.isMinimized()) {
|
||||
mainWindow?.show();
|
||||
} else {
|
||||
mainWindow?.hide();
|
||||
}
|
||||
} else {
|
||||
mainWindow?.show();
|
||||
}
|
||||
});
|
||||
tray.on('right-click', () => {
|
||||
tray.popUpContextMenu(contextMenu);
|
||||
});
|
||||
|
||||
await addIpcMainListener(mainWindow, coreService);
|
||||
return {
|
||||
mainWindow,
|
||||
coreService,
|
||||
};
|
||||
};
|
96
webapp/packages/main/src/createMainWindow.ts
Normal file
96
webapp/packages/main/src/createMainWindow.ts
Normal file
@ -0,0 +1,96 @@
|
||||
import {app, BrowserWindow, globalShortcut, nativeTheme} from 'electron';
|
||||
import {join} from 'node:path';
|
||||
import {URL} from 'node:url';
|
||||
import {ThemeObj} from '@common/constant/theme';
|
||||
import logger from '/@/logger';
|
||||
|
||||
export const createMainWindow = async () => {
|
||||
nativeTheme.themeSource = ThemeObj['light'];
|
||||
const browserWindow = new BrowserWindow({
|
||||
width: 1280,
|
||||
height: 880,
|
||||
show: false, // Use 'ready-to-show' event to show window
|
||||
frame: false,
|
||||
icon: join(__dirname, './icon.png'),
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
sandbox: false, // Sandbox disabled because the demo of preload script depend on the Node.js api
|
||||
webviewTag: false, // The webview tag is not recommended. Consider alternatives like an iframe or Electron's BrowserView. @see https://www.electronjs.org/docs/latest/api/webview-tag#warning
|
||||
preload: join(app.getAppPath(), 'packages/preload/dist/index.cjs'),
|
||||
},
|
||||
});
|
||||
|
||||
browserWindow.setMinimumSize(576, 396);
|
||||
|
||||
browserWindow.webContents.on('preload-error', (event, preloadPath, error) => {
|
||||
logger.error('------------preload-error------------');
|
||||
logger.error(`event:${JSON.stringify(event)}`);
|
||||
logger.error(`preloadPath:${preloadPath}`);
|
||||
logger.error(`error:${error}`);
|
||||
});
|
||||
|
||||
browserWindow.webContents.on('console-message', (event, level, message, line, sourceId) => {
|
||||
if (level === 2) {
|
||||
logger.warn(`console-message:${message} line:${line} sourceId:${sourceId}`);
|
||||
return;
|
||||
}
|
||||
if (level === 3) {
|
||||
logger.info('------------console-message------------');
|
||||
logger.error(`event:${JSON.stringify(event)}`);
|
||||
logger.error(`console-message:${message} line:${line} sourceId:${sourceId}`);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* If the 'show' property of the BrowserWindow's constructor is omitted from the initialization options,
|
||||
* it then defaults to 'true'. This can cause flickering as the window loads the html content,
|
||||
* and it also has show problematic behaviour with the closing of the window.
|
||||
* Use `show: false` and listen to the `ready-to-show` event to show the window.
|
||||
*
|
||||
* @see https://github.com/electron/electron/issues/25012 for the afford mentioned issue.
|
||||
*/
|
||||
browserWindow.on('ready-to-show', () => {
|
||||
logger.info('-----ready-to-show-----');
|
||||
browserWindow?.show();
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
browserWindow?.webContents.openDevTools();
|
||||
}
|
||||
});
|
||||
|
||||
browserWindow.on('focus', function () {
|
||||
// Dev tools
|
||||
globalShortcut.register('Ctrl+Shift+I', function () {
|
||||
if (browserWindow?.webContents.isDevToolsOpened()) {
|
||||
browserWindow?.webContents.closeDevTools();
|
||||
} else {
|
||||
browserWindow?.webContents.openDevTools();
|
||||
}
|
||||
});
|
||||
// Refresh
|
||||
globalShortcut.register('Ctrl+R', function () {
|
||||
browserWindow?.reload();
|
||||
});
|
||||
globalShortcut.register('Ctrl+Shift+R', function () {
|
||||
browserWindow?.reload();
|
||||
});
|
||||
});
|
||||
browserWindow.on('blur', function () {
|
||||
globalShortcut.unregisterAll();
|
||||
});
|
||||
|
||||
/**
|
||||
* URL for main window.
|
||||
* Vite dev server for development.
|
||||
* `file://../renderer/index.html` for production and test
|
||||
*/
|
||||
const pageUrl =
|
||||
import.meta.env.DEV && import.meta.env.VITE_DEV_SERVER_URL !== undefined
|
||||
? import.meta.env.VITE_DEV_SERVER_URL
|
||||
: new URL('../renderer/dist/index.html', 'file://' + __dirname).toString();
|
||||
await browserWindow.loadURL(pageUrl);
|
||||
|
||||
return browserWindow;
|
||||
};
|
129
webapp/packages/main/src/index.ts
Normal file
129
webapp/packages/main/src/index.ts
Normal file
@ -0,0 +1,129 @@
|
||||
import {app, BrowserWindow} from 'electron';
|
||||
import './security-restrictions';
|
||||
import {createApp} from '/@/createApp';
|
||||
import logger from '/@/logger';
|
||||
import {noSandbox} from '/@/config';
|
||||
|
||||
/**
|
||||
* Prevent electron from running multiple instances.
|
||||
*/
|
||||
const isSingleInstance = app.requestSingleInstanceLock();
|
||||
logger.info(`isSingleInstance:${isSingleInstance}`);
|
||||
if (!isSingleInstance) {
|
||||
app.quit();
|
||||
} else {
|
||||
app.on('second-instance', async () => {
|
||||
logger.info('second-instance');
|
||||
const [curWindow] = BrowserWindow.getAllWindows();
|
||||
if (!curWindow) {
|
||||
logger.info('------createApp------');
|
||||
await createApp();
|
||||
} else {
|
||||
logger.info('------curWindow.focus------');
|
||||
curWindow.focus?.();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable Hardware Acceleration to save more system resources.
|
||||
* Also `in-process-gpu` to avoid creating a gpu process which may `exited unexpectedly`
|
||||
* See https://github.com/electron/electron/issues/30966
|
||||
*/
|
||||
app.disableHardwareAcceleration();
|
||||
app.commandLine.appendSwitch('disable-gpu');
|
||||
app.commandLine.appendSwitch('disable-software-rasterizer');
|
||||
app.commandLine.appendSwitch('disable-gpu-compositing');
|
||||
app.commandLine.appendSwitch('disable-gpu-rasterization');
|
||||
app.commandLine.appendSwitch('disable-gpu-sandbox');
|
||||
app.commandLine.appendSwitch('in-process-gpu');
|
||||
app.commandLine.appendSwitch('no-sandbox');
|
||||
|
||||
/**
|
||||
*Set App Error Log Path
|
||||
*/
|
||||
// app.setAppLogsPath(join(app.getAppPath(), '/AlasAppError'));
|
||||
|
||||
/**
|
||||
* Shout down background process if all windows was closed
|
||||
*/
|
||||
app.on('window-all-closed', () => {
|
||||
app.quit();
|
||||
});
|
||||
|
||||
/**
|
||||
* @see https://www.electronjs.org/docs/latest/api/app#event-activate-macos Event: 'activate'.
|
||||
*/
|
||||
// app.on('activate', createWindow);
|
||||
/**
|
||||
* Create the application window when the background process is ready.
|
||||
*/
|
||||
// app
|
||||
// .whenReady()
|
||||
// .then(createWindow)
|
||||
// .then(loadURL)
|
||||
// .catch(e => console.error('Failed create window:', e));
|
||||
|
||||
/**
|
||||
* Install Vue.js or any other extension in development mode only.
|
||||
* Note: You must install `electron-devtools-installer` manually
|
||||
*/
|
||||
// if (import.meta.env.DEV) {
|
||||
// app
|
||||
// .whenReady()
|
||||
// .then(() => import('electron-devtools-installer'))
|
||||
// .then(module => {
|
||||
// const {default: installExtension, VUEJS3_DEVTOOLS} =
|
||||
// // @ts-expect-error Hotfix for https://github.com/cawa-93/vite-electron-builder/issues/915
|
||||
// typeof module.default === 'function' ? module : (module.default as typeof module);
|
||||
//
|
||||
// return installExtension(VUEJS3_DEVTOOLS, {
|
||||
// loadExtensionOptions: {
|
||||
// allowFileAccess: true,
|
||||
// },
|
||||
// });
|
||||
// })
|
||||
// .catch(e => console.error('Failed install extension:', e));
|
||||
// }
|
||||
|
||||
/**
|
||||
* Check for app updates, install it in background and notify user that new version was installed.
|
||||
* No reason run this in non-production build.
|
||||
* @see https://www.electron.build/auto-update.html#quick-setup-guide
|
||||
*
|
||||
* Note: It may throw "ENOENT: no such file app-update.yml"
|
||||
* if you compile production app without publishing it to distribution server.
|
||||
* Like `npm run compile` does. It's ok 😅
|
||||
*/
|
||||
// if (import.meta.env.PROD) {
|
||||
// app
|
||||
// .whenReady()
|
||||
// .then(() => import('electron-updater'))
|
||||
// .then(module => {
|
||||
// const autoUpdater =
|
||||
// module.autoUpdater ||
|
||||
// // @ts-expect-error Hotfix for https://github.com/electron-userland/electron-builder/issues/7338
|
||||
// (module.default.autoUpdater as (typeof module)['autoUpdater']);
|
||||
// return autoUpdater.checkForUpdatesAndNotify();
|
||||
// })
|
||||
// .catch(e => console.error('Failed check and install updates:', e));
|
||||
// }
|
||||
|
||||
app
|
||||
.whenReady()
|
||||
.then(createApp)
|
||||
.catch(e => {
|
||||
logger.error('Failed create window:' + e);
|
||||
});
|
||||
|
||||
app.on('activate', async () => {
|
||||
logger.info('------app activate------');
|
||||
const [curWindow] = BrowserWindow.getAllWindows();
|
||||
if (!curWindow) {
|
||||
logger.info('------createApp------');
|
||||
await createApp();
|
||||
} else {
|
||||
logger.info('------curWindow.focus------');
|
||||
curWindow.focus();
|
||||
}
|
||||
});
|
31
webapp/packages/main/src/logger.ts
Normal file
31
webapp/packages/main/src/logger.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import logger from 'electron-log';
|
||||
import {join} from 'node:path';
|
||||
import {getAlasABSPath} from '@common/utils';
|
||||
const dayjs = require('dayjs');
|
||||
|
||||
logger.transports.file.level = 'info';
|
||||
logger.transports.file.maxSize = 1024 * 1024;
|
||||
logger.transports.file.format = '[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}]{scope} {text}';
|
||||
const dateStr = dayjs(new Date()).format('YYYY-MM-DD');
|
||||
const logPath = join(getAlasABSPath(), `./log/${dateStr}_webapp.txt`);
|
||||
logger.transports.file.resolvePath = () => logPath;
|
||||
export default {
|
||||
info(params: string) {
|
||||
logger.info(params);
|
||||
},
|
||||
warn(params: string) {
|
||||
logger.warn(params);
|
||||
},
|
||||
error(params: string) {
|
||||
logger.error(params);
|
||||
},
|
||||
debug(params: string) {
|
||||
logger.debug(params);
|
||||
},
|
||||
verbose(params: string) {
|
||||
logger.verbose(params);
|
||||
},
|
||||
silly(params: string) {
|
||||
logger.silly(params);
|
||||
},
|
||||
};
|
270
webapp/packages/main/src/mainWindow.ts
Normal file
270
webapp/packages/main/src/mainWindow.ts
Normal file
@ -0,0 +1,270 @@
|
||||
import {
|
||||
app,
|
||||
BrowserWindow,
|
||||
globalShortcut,
|
||||
ipcMain,
|
||||
Menu,
|
||||
nativeTheme,
|
||||
Tray,
|
||||
nativeImage,
|
||||
} from 'electron';
|
||||
import {URL} from 'node:url';
|
||||
import {PyShell} from '/@/pyshell';
|
||||
import {
|
||||
dpiScaling,
|
||||
webuiTheme,
|
||||
webuiArgs,
|
||||
webuiPath,
|
||||
installerPath,
|
||||
installerArgs,
|
||||
} from '/@/config';
|
||||
import {isMacintosh} from '@common/utils/env';
|
||||
import relaunchApp from '/@/relaunchApp';
|
||||
import {ALAS_LOG, UPDATE_APP} from '@common/constant/eventNames';
|
||||
|
||||
const path = require('path');
|
||||
/**
|
||||
* Load deploy settings and start Alas web server.
|
||||
*/
|
||||
let installer: PyShell | null = null;
|
||||
let alas: PyShell | null = null;
|
||||
|
||||
let browserWindow: BrowserWindow | null = null;
|
||||
|
||||
nativeTheme.themeSource = webuiTheme;
|
||||
|
||||
export async function createWindow() {
|
||||
browserWindow = new BrowserWindow({
|
||||
width: 1280,
|
||||
height: 880,
|
||||
show: false, // Use 'ready-to-show' event to show window
|
||||
frame: false,
|
||||
icon: path.join(__dirname, './icon.png'),
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: true,
|
||||
sandbox: false, // Sandbox disabled because the demo of preload script depend on the Node.js api
|
||||
webviewTag: false, // The webview tag is not recommended. Consider alternatives like an iframe or Electron's BrowserView. @see https://www.electronjs.org/docs/latest/api/webview-tag#warning
|
||||
preload: path.join(app.getAppPath(), 'packages/preload/dist/index.cjs'),
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* If the 'show' property of the BrowserWindow's constructor is omitted from the initialization options,
|
||||
* it then defaults to 'true'. This can cause flickering as the window loads the html content,
|
||||
* and it also has show problematic behaviour with the closing of the window.
|
||||
* Use `show: false` and listen to the `ready-to-show` event to show the window.
|
||||
*
|
||||
* @see https://github.com/electron/electron/issues/25012 for the afford mentioned issue.
|
||||
*/
|
||||
browserWindow.on('ready-to-show', () => {
|
||||
browserWindow?.show();
|
||||
|
||||
// Hide menu
|
||||
const {Menu} = require('electron');
|
||||
Menu.setApplicationMenu(null);
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
browserWindow?.webContents.openDevTools();
|
||||
}
|
||||
});
|
||||
|
||||
browserWindow.on('focus', function () {
|
||||
// Dev tools
|
||||
globalShortcut.register('Ctrl+Shift+I', function () {
|
||||
if (browserWindow?.webContents.isDevToolsOpened()) {
|
||||
browserWindow?.webContents.closeDevTools();
|
||||
} else {
|
||||
browserWindow?.webContents.openDevTools();
|
||||
}
|
||||
});
|
||||
// Refresh
|
||||
globalShortcut.register('Ctrl+R', function () {
|
||||
browserWindow?.reload();
|
||||
});
|
||||
globalShortcut.register('Ctrl+Shift+R', function () {
|
||||
browserWindow?.reload();
|
||||
});
|
||||
});
|
||||
browserWindow.on('blur', function () {
|
||||
globalShortcut.unregisterAll();
|
||||
});
|
||||
|
||||
ipcMain.on('window-ready', async function (_, args) {
|
||||
args && (await initWindowEvents());
|
||||
});
|
||||
|
||||
/*
|
||||
* Fix oversize icon on bar in macOS
|
||||
*/
|
||||
const icon = nativeImage.createFromPath(path.join(__dirname, './icon.png'));
|
||||
const dockerIcon = icon.resize({width: 16, height: 16});
|
||||
// Tray
|
||||
const tray = new Tray(isMacintosh ? dockerIcon : icon);
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: 'Show',
|
||||
click: function () {
|
||||
browserWindow?.show();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Hide',
|
||||
click: function () {
|
||||
browserWindow?.hide();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Exit',
|
||||
click: function () {
|
||||
alas?.kill(function () {
|
||||
browserWindow?.close();
|
||||
});
|
||||
},
|
||||
},
|
||||
]);
|
||||
tray.setToolTip('Alas');
|
||||
tray.setContextMenu(contextMenu);
|
||||
tray.on('click', () => {
|
||||
if (browserWindow?.isVisible()) {
|
||||
if (browserWindow?.isMinimized()) {
|
||||
browserWindow?.show();
|
||||
} else {
|
||||
browserWindow?.hide();
|
||||
}
|
||||
} else {
|
||||
browserWindow?.show();
|
||||
}
|
||||
});
|
||||
tray.on('right-click', () => {
|
||||
tray.popUpContextMenu(contextMenu);
|
||||
});
|
||||
|
||||
return browserWindow;
|
||||
}
|
||||
|
||||
// No DPI scaling
|
||||
if (!dpiScaling) {
|
||||
app.commandLine.appendSwitch('high-dpi-support', '1');
|
||||
app.commandLine.appendSwitch('force-device-scale-factor', '1');
|
||||
}
|
||||
|
||||
export function loadURL() {
|
||||
/**
|
||||
* URL for main window.
|
||||
* Vite dev server for development.
|
||||
* `file://../renderer/index.html` for production and test
|
||||
*/
|
||||
const pageUrl =
|
||||
import.meta.env.DEV && import.meta.env.VITE_DEV_SERVER_URL !== undefined
|
||||
? import.meta.env.VITE_DEV_SERVER_URL
|
||||
: new URL('../renderer/dist/index.html', 'file://' + __dirname).toString();
|
||||
browserWindow?.loadURL(pageUrl);
|
||||
}
|
||||
|
||||
// Minimize, maximize, close window.
|
||||
ipcMain.on('window-tray', function () {
|
||||
browserWindow?.hide();
|
||||
});
|
||||
ipcMain.on('window-min', function () {
|
||||
browserWindow?.minimize();
|
||||
});
|
||||
ipcMain.on('window-max', function () {
|
||||
browserWindow?.isMaximized() ? browserWindow?.restore() : browserWindow?.maximize();
|
||||
});
|
||||
ipcMain.on('window-close', function () {
|
||||
if (installer) {
|
||||
installer?.removeAllListeners('stderr');
|
||||
installer?.removeAllListeners('message');
|
||||
installer?.removeAllListeners('stdout');
|
||||
installer?.kill(function () {
|
||||
browserWindow?.close();
|
||||
browserWindow = null;
|
||||
installer = null;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
alas?.removeAllListeners('stderr');
|
||||
alas?.removeAllListeners('message');
|
||||
alas?.removeAllListeners('stdout');
|
||||
alas?.kill(function () {
|
||||
browserWindow?.close();
|
||||
browserWindow = null;
|
||||
});
|
||||
|
||||
browserWindow?.close();
|
||||
});
|
||||
|
||||
async function initWindowEvents() {
|
||||
// Start installer and wait for it to finish.
|
||||
await runInstaller();
|
||||
|
||||
ipcMain.on('install-success', async function () {
|
||||
installer = null;
|
||||
// Start Alas web server.
|
||||
runAlas();
|
||||
});
|
||||
}
|
||||
|
||||
async function runInstaller() {
|
||||
installer = new PyShell(installerPath, installerArgs);
|
||||
installer?.end(function (err: string) {
|
||||
sendLaunchLog(err);
|
||||
if (err) throw err;
|
||||
});
|
||||
installer?.on('stdout', function (message) {
|
||||
sendLaunchLog(message);
|
||||
});
|
||||
installer?.on('message', function (message) {
|
||||
sendLaunchLog(message);
|
||||
});
|
||||
installer?.on('stderr', function (message: string) {
|
||||
sendLaunchLog(message);
|
||||
});
|
||||
}
|
||||
|
||||
function runAlas() {
|
||||
alas = new PyShell(webuiPath, webuiArgs);
|
||||
alas?.end(function (err: string) {
|
||||
sendLaunchLog(err);
|
||||
if (err) throw err;
|
||||
});
|
||||
alas?.on('stdout', function (message) {
|
||||
sendLaunchLog(message);
|
||||
});
|
||||
|
||||
alas?.on('message', function (message) {
|
||||
sendLaunchLog(message);
|
||||
});
|
||||
alas?.on('stderr', function (message: string) {
|
||||
sendLaunchLog(message);
|
||||
/**
|
||||
* Receive logs, judge if Alas is ready
|
||||
* For starlette backend, there will have:
|
||||
* `INFO: Uvicorn running on http://0.0.0.0:22267 (Press CTRL+C to quit)`
|
||||
* Or backend has started already
|
||||
* `[Errno 10048] error while attempting to bind on address ('0.0.0.0', 22267): `
|
||||
*/
|
||||
if (message.includes('Application startup complete') || message.includes('bind on address')) {
|
||||
alas?.removeAllListeners('stderr');
|
||||
alas?.removeAllListeners('message');
|
||||
alas?.removeAllListeners('stdout');
|
||||
// loadURL();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function sendLaunchLog(message: string) {
|
||||
message?.includes(UPDATE_APP) && relaunchApp();
|
||||
browserWindow?.webContents.send(ALAS_LOG, message);
|
||||
}
|
||||
|
||||
export async function restoreWindow() {
|
||||
// Someone tried to run a second instance, we should focus our window.
|
||||
if (browserWindow) {
|
||||
if (browserWindow.isMinimized()) browserWindow.restore();
|
||||
if (!browserWindow.isVisible()) browserWindow.show();
|
||||
browserWindow.focus();
|
||||
}
|
||||
}
|
29
webapp/packages/main/src/pyshell.ts
Normal file
29
webapp/packages/main/src/pyshell.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import {alasPath, pythonPath} from '/@/config';
|
||||
import logger from '/@/logger';
|
||||
|
||||
const {PythonShell} = require('python-shell');
|
||||
const treeKill = require('tree-kill');
|
||||
|
||||
export class PyShell extends PythonShell {
|
||||
constructor(script: string, args: Array<string> = []) {
|
||||
const options = {
|
||||
mode: 'text',
|
||||
args: args,
|
||||
pythonPath: pythonPath,
|
||||
scriptPath: alasPath,
|
||||
};
|
||||
logger.info(`${pythonPath} ${script} ${args}`);
|
||||
super(script, options);
|
||||
}
|
||||
|
||||
on(event: string, listener: (...args: any[]) => void): this {
|
||||
this.removeAllListeners(event);
|
||||
super.on(event, listener);
|
||||
return this;
|
||||
}
|
||||
|
||||
kill(callback: (...args: any[]) => void): this {
|
||||
treeKill(this.childProcess.pid, 'SIGTERM', callback);
|
||||
return this;
|
||||
}
|
||||
}
|
15
webapp/packages/main/src/relaunchApp.ts
Normal file
15
webapp/packages/main/src/relaunchApp.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import {app} from 'electron';
|
||||
import {ALAS_RELAUNCH_ARGV} from '@common/constant/config';
|
||||
|
||||
export const isRelaunch = process.argv.includes(ALAS_RELAUNCH_ARGV);
|
||||
|
||||
function relaunchApp() {
|
||||
/**
|
||||
* TODO Some events need to be rehandled for restart operations
|
||||
*/
|
||||
if (!isRelaunch) {
|
||||
app.relaunch({args: process.argv.slice(1).concat([ALAS_RELAUNCH_ARGV])});
|
||||
}
|
||||
}
|
||||
|
||||
export default relaunchApp;
|
128
webapp/packages/main/src/security-restrictions.ts
Normal file
128
webapp/packages/main/src/security-restrictions.ts
Normal file
@ -0,0 +1,128 @@
|
||||
import type {Session} from 'electron';
|
||||
import {app, shell} from 'electron';
|
||||
import {URL} from 'node:url';
|
||||
|
||||
/**
|
||||
* Union for all existing permissions in electron
|
||||
*/
|
||||
type Permission = Parameters<
|
||||
Exclude<Parameters<Session['setPermissionRequestHandler']>[0], null>
|
||||
>[1];
|
||||
|
||||
/**
|
||||
* A list of origins that you allow open INSIDE the application and permissions for them.
|
||||
*
|
||||
* In development mode you need allow open `VITE_DEV_SERVER_URL`.
|
||||
*/
|
||||
const ALLOWED_ORIGINS_AND_PERMISSIONS = new Map<string, Set<Permission>>(
|
||||
import.meta.env.DEV && import.meta.env.VITE_DEV_SERVER_URL
|
||||
? [[new URL(import.meta.env.VITE_DEV_SERVER_URL).origin, new Set()]]
|
||||
: [],
|
||||
);
|
||||
|
||||
/**
|
||||
* A list of origins that you allow open IN BROWSER.
|
||||
* Navigation to the origins below is only possible if the link opens in a new window.
|
||||
*
|
||||
* @example
|
||||
* <a
|
||||
* target="_blank"
|
||||
* href="https://github.com/"
|
||||
* >
|
||||
*/
|
||||
const ALLOWED_EXTERNAL_ORIGINS = new Set<`https://${string}`>(['https://github.com']);
|
||||
|
||||
app.on('web-contents-created', (_, contents) => {
|
||||
/**
|
||||
* Block navigation to origins not on the allowlist.
|
||||
*
|
||||
* Navigation exploits are quite common. If an attacker can convince the app to navigate away from its current page,
|
||||
* they can possibly force the app to open arbitrary web resources/websites on the web.
|
||||
*
|
||||
* @see https://www.electronjs.org/docs/latest/tutorial/security#13-disable-or-limit-navigation
|
||||
*/
|
||||
contents.on('will-navigate', (event, url) => {
|
||||
const {origin} = new URL(url);
|
||||
if (ALLOWED_ORIGINS_AND_PERMISSIONS.has(origin)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent navigation
|
||||
event.preventDefault();
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
console.warn(`Blocked navigating to disallowed origin: ${origin}`);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Block requests for disallowed permissions.
|
||||
* By default, Electron will automatically approve all permission requests.
|
||||
*
|
||||
* @see https://www.electronjs.org/docs/latest/tutorial/security#5-handle-session-permission-requests-from-remote-content
|
||||
*/
|
||||
contents.session.setPermissionRequestHandler((webContents, permission, callback) => {
|
||||
const {origin} = new URL(webContents.getURL());
|
||||
|
||||
const permissionGranted = !!ALLOWED_ORIGINS_AND_PERMISSIONS.get(origin)?.has(permission);
|
||||
callback(permissionGranted);
|
||||
|
||||
if (!permissionGranted && import.meta.env.DEV) {
|
||||
console.warn(`${origin} requested permission for '${permission}', but was rejected.`);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Hyperlinks leading to allowed sites are opened in the default browser.
|
||||
*
|
||||
* The creation of new `webContents` is a common attack vector. Attackers attempt to convince the app to create new windows,
|
||||
* frames, or other renderer processes with more privileges than they had before; or with pages opened that they couldn't open before.
|
||||
* You should deny any unexpected window creation.
|
||||
*
|
||||
* @see https://www.electronjs.org/docs/latest/tutorial/security#14-disable-or-limit-creation-of-new-windows
|
||||
* @see https://www.electronjs.org/docs/latest/tutorial/security#15-do-not-use-openexternal-with-untrusted-content
|
||||
*/
|
||||
contents.setWindowOpenHandler(({url}) => {
|
||||
const {origin} = new URL(url);
|
||||
|
||||
if (ALLOWED_EXTERNAL_ORIGINS.has(origin as `https://${string}`)) {
|
||||
// Open url in default browser.
|
||||
shell.openExternal(url).catch(console.error);
|
||||
} else if (import.meta.env.DEV) {
|
||||
console.warn(`Blocked the opening of a disallowed origin: ${origin}`);
|
||||
}
|
||||
|
||||
// Prevent creating a new window.
|
||||
return {action: 'deny'};
|
||||
});
|
||||
|
||||
/**
|
||||
* Verify webview options before creation.
|
||||
*
|
||||
* Strip away preload scripts, disable Node.js integration, and ensure origins are on the allowlist.
|
||||
*
|
||||
* @see https://www.electronjs.org/docs/latest/tutorial/security#12-verify-webview-options-before-creation
|
||||
*/
|
||||
contents.on('will-attach-webview', (event, webPreferences, params) => {
|
||||
const {origin} = new URL(params.src);
|
||||
if (!ALLOWED_ORIGINS_AND_PERMISSIONS.has(origin)) {
|
||||
if (import.meta.env.DEV) {
|
||||
console.warn(`A webview tried to attach ${params.src}, but was blocked.`);
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
// Strip away preload scripts if unused or verify their location is legitimate.
|
||||
delete webPreferences.preload;
|
||||
// @ts-expect-error `preloadURL` exists. - @see https://www.electronjs.org/docs/latest/api/web-contents#event-will-attach-webview
|
||||
delete webPreferences.preloadURL;
|
||||
|
||||
// Disable Node.js integration
|
||||
webPreferences.nodeIntegration = false;
|
||||
|
||||
// Enable contextIsolation
|
||||
webPreferences.contextIsolation = true;
|
||||
});
|
||||
});
|
42
webapp/packages/main/src/serviceLogic/createAlas.ts
Normal file
42
webapp/packages/main/src/serviceLogic/createAlas.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import {webuiArgs, webuiPath} from '/@/config';
|
||||
import {PyShell} from '/@/pyshell';
|
||||
import type {CallbackFun} from '/@/coreService';
|
||||
import logger from '/@/logger';
|
||||
|
||||
export const createAlas: CallbackFun = async ctx => {
|
||||
const alas = new PyShell(webuiPath, webuiArgs);
|
||||
alas.on('error', function (err: string) {
|
||||
if(!err) return;
|
||||
logger.error('alas.error:' + err);
|
||||
ctx.sendLaunchLog(err);
|
||||
});
|
||||
alas.end(function (err: string) {
|
||||
if(!err) return;
|
||||
logger.info('alas.end:' + err);
|
||||
ctx.sendLaunchLog(err);
|
||||
throw err;
|
||||
});
|
||||
alas.on('stdout', function (message) {
|
||||
ctx.sendLaunchLog(message);
|
||||
});
|
||||
|
||||
alas.on('message', function (message) {
|
||||
ctx.sendLaunchLog(message);
|
||||
});
|
||||
alas.on('stderr', function (message: string) {
|
||||
ctx.sendLaunchLog(message);
|
||||
/**
|
||||
* Receive logs, judge if Alas is ready
|
||||
* For starlette backend, there will have:
|
||||
* `INFO: Uvicorn running on http://0.0.0.0:22267 (Press CTRL+C to quit)`
|
||||
* Or backend has started already
|
||||
* `[Errno 10048] error while attempting to bind on address ('0.0.0.0', 22267): `
|
||||
*/
|
||||
if (message.includes('Application startup complete') || message.includes('bind on address')) {
|
||||
alas.removeAllListeners('stderr');
|
||||
alas.removeAllListeners('message');
|
||||
alas.removeAllListeners('stdout');
|
||||
}
|
||||
});
|
||||
return alas;
|
||||
};
|
32
webapp/packages/main/src/serviceLogic/createInstaller.ts
Normal file
32
webapp/packages/main/src/serviceLogic/createInstaller.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import type {CallbackFun} from '/@/coreService';
|
||||
import {PyShell} from '/@/pyshell';
|
||||
import {installerArgs, installerPath} from '/@/config';
|
||||
import {ALAS_RELAUNCH_ARGV} from '@common/constant/config';
|
||||
import logger from '/@/logger';
|
||||
export const createInstaller: CallbackFun = async (ctx, next) => {
|
||||
if (process.argv.includes(ALAS_RELAUNCH_ARGV)) {
|
||||
return next();
|
||||
}
|
||||
const installer = new PyShell(installerPath, installerArgs);
|
||||
installer.on('error', function (err: string) {
|
||||
if(!err) return;
|
||||
logger.error('installer.error:' + err);
|
||||
ctx.sendLaunchLog(err);
|
||||
});
|
||||
installer?.end(function (err: string) {
|
||||
if(!err) return;
|
||||
logger.info('installer.end:' + err);
|
||||
ctx.sendLaunchLog(err);
|
||||
throw err;
|
||||
});
|
||||
installer?.on('stdout', function (message) {
|
||||
ctx.sendLaunchLog(message);
|
||||
});
|
||||
installer?.on('message', function (message) {
|
||||
ctx.sendLaunchLog(message);
|
||||
});
|
||||
installer?.on('stderr', function (message: string) {
|
||||
ctx.sendLaunchLog(message);
|
||||
});
|
||||
return installer;
|
||||
};
|
4
webapp/packages/main/src/serviceLogic/index.ts
Normal file
4
webapp/packages/main/src/serviceLogic/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import {createInstaller} from './createInstaller';
|
||||
import {createAlas} from './createAlas';
|
||||
|
||||
export {createInstaller, createAlas};
|
72
webapp/packages/main/tests/unit.spec.ts
Normal file
72
webapp/packages/main/tests/unit.spec.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import type {MockedClass} from 'vitest';
|
||||
import {beforeEach, expect, test, vi} from 'vitest';
|
||||
import {restoreOrCreateWindow} from '../src/mainWindow';
|
||||
|
||||
import {BrowserWindow} from 'electron';
|
||||
|
||||
/**
|
||||
* Mock real electron BrowserWindow API
|
||||
*/
|
||||
vi.mock('electron', () => {
|
||||
// Use "as unknown as" because vi.fn() does not have static methods
|
||||
const bw = vi.fn() as unknown as MockedClass<typeof BrowserWindow>;
|
||||
bw.getAllWindows = vi.fn(() => bw.mock.instances);
|
||||
bw.prototype.loadURL = vi.fn((_: string, __?: Electron.LoadURLOptions) => Promise.resolve());
|
||||
// Use "any" because the on function is overloaded
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
bw.prototype.on = vi.fn<any>();
|
||||
bw.prototype.destroy = vi.fn();
|
||||
bw.prototype.isDestroyed = vi.fn();
|
||||
bw.prototype.isMinimized = vi.fn();
|
||||
bw.prototype.focus = vi.fn();
|
||||
bw.prototype.restore = vi.fn();
|
||||
|
||||
const app: Pick<Electron.App, 'getAppPath'> = {
|
||||
getAppPath(): string {
|
||||
return '';
|
||||
},
|
||||
};
|
||||
|
||||
return {BrowserWindow: bw, app};
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test('Should create a new window', async () => {
|
||||
const {mock} = vi.mocked(BrowserWindow);
|
||||
expect(mock.instances).toHaveLength(0);
|
||||
|
||||
await restoreOrCreateWindow();
|
||||
expect(mock.instances).toHaveLength(1);
|
||||
expect(mock.instances[0].loadURL).toHaveBeenCalledOnce();
|
||||
expect(mock.instances[0].loadURL).toHaveBeenCalledWith(expect.stringMatching(/index\.html$/));
|
||||
});
|
||||
|
||||
test('Should restore an existing window', async () => {
|
||||
const {mock} = vi.mocked(BrowserWindow);
|
||||
|
||||
// Create a window and minimize it.
|
||||
await restoreOrCreateWindow();
|
||||
expect(mock.instances).toHaveLength(1);
|
||||
const appWindow = vi.mocked(mock.instances[0]);
|
||||
appWindow.isMinimized.mockReturnValueOnce(true);
|
||||
|
||||
await restoreOrCreateWindow();
|
||||
expect(mock.instances).toHaveLength(1);
|
||||
expect(appWindow.restore).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
test('Should create a new window if the previous one was destroyed', async () => {
|
||||
const {mock} = vi.mocked(BrowserWindow);
|
||||
|
||||
// Create a window and destroy it.
|
||||
await restoreOrCreateWindow();
|
||||
expect(mock.instances).toHaveLength(1);
|
||||
const appWindow = vi.mocked(mock.instances[0]);
|
||||
appWindow.isDestroyed.mockReturnValueOnce(true);
|
||||
|
||||
await restoreOrCreateWindow();
|
||||
expect(mock.instances).toHaveLength(2);
|
||||
});
|
19
webapp/packages/main/tsconfig.json
Normal file
19
webapp/packages/main/tsconfig.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "esnext",
|
||||
"target": "esnext",
|
||||
"sourceMap": false,
|
||||
"moduleResolution": "Node",
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"isolatedModules": true,
|
||||
"types": ["node"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"/@/*": ["./src/*"],
|
||||
"@common/*": ["../common/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "../../types/**/*.d.ts","../common/**/*.ts"],
|
||||
"exclude": ["**/*.spec.ts", "**/*.test.ts"]
|
||||
}
|
44
webapp/packages/main/vite.config.js
Normal file
44
webapp/packages/main/vite.config.js
Normal file
@ -0,0 +1,44 @@
|
||||
import {node} from '../../.electron-vendors.cache.json';
|
||||
import {join} from 'node:path';
|
||||
import {injectAppVersion} from '../../version/inject-app-version-plugin.mjs';
|
||||
|
||||
const PACKAGE_ROOT = __dirname;
|
||||
const PROJECT_ROOT = join(PACKAGE_ROOT, '../..');
|
||||
|
||||
/**
|
||||
* @type {import('vite').UserConfig}
|
||||
* @see https://vitejs.dev/config/
|
||||
*/
|
||||
const config = {
|
||||
mode: process.env.MODE,
|
||||
root: PACKAGE_ROOT,
|
||||
envDir: PROJECT_ROOT,
|
||||
resolve: {
|
||||
alias: {
|
||||
'/@/': join(PACKAGE_ROOT, 'src') + '/',
|
||||
'@common': join(PACKAGE_ROOT, '../common/'),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
ssr: true,
|
||||
sourcemap: 'inline',
|
||||
target: `node${node}`,
|
||||
outDir: 'dist',
|
||||
assetsDir: '.',
|
||||
minify: process.env.MODE !== 'development',
|
||||
lib: {
|
||||
entry: 'src/index.ts',
|
||||
formats: ['cjs'],
|
||||
},
|
||||
rollupOptions: {
|
||||
output: {
|
||||
entryFileNames: '[name].cjs',
|
||||
},
|
||||
},
|
||||
emptyOutDir: true,
|
||||
reportCompressedSize: false,
|
||||
},
|
||||
plugins: [injectAppVersion()],
|
||||
};
|
||||
|
||||
export default config;
|
59
webapp/packages/preload/src/alasConfig.ts
Normal file
59
webapp/packages/preload/src/alasConfig.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import {ALAS_CONFIG_YAML} from '@common/constant/config';
|
||||
import {getAlasABSPath, checkIsFirst} from '@common/utils';
|
||||
import {ThemeObj} from '@common/constant/theme';
|
||||
import {Dirent} from 'fs';
|
||||
const path = require('path');
|
||||
const yaml = require('yaml');
|
||||
const fs = require('fs');
|
||||
|
||||
let alasConfig: AlasConfig | null = null;
|
||||
export async function getAlasConfig() {
|
||||
if (alasConfig === null) {
|
||||
const alasPath = getAlasABSPath();
|
||||
const file = fs.readFileSync(path.join(alasPath, `./config/${ALAS_CONFIG_YAML}`), 'utf8');
|
||||
const config = yaml.parse(file) as DefAlasConfig;
|
||||
const WebuiPort = config.Deploy.Webui.WebuiPort.toString();
|
||||
const Theme = config.Deploy.Webui.Theme;
|
||||
alasConfig = {
|
||||
webuiUrl: `http://127.0.0.1:${WebuiPort}`,
|
||||
theme: ThemeObj[Theme] || 'light',
|
||||
language: config.Deploy.Webui.Language || 'en-US',
|
||||
repository: config.Deploy.Git.Repository as any,
|
||||
alasPath,
|
||||
};
|
||||
}
|
||||
return alasConfig;
|
||||
}
|
||||
|
||||
export function checkIsNeedInstall() {
|
||||
return checkIsFirst();
|
||||
}
|
||||
|
||||
interface fileInfoItem {
|
||||
name: string;
|
||||
path: string;
|
||||
lastModifyTime: Date;
|
||||
}
|
||||
export function getAlasConfigDirFiles() {
|
||||
const alasPath = getAlasABSPath();
|
||||
const configPath = path.join(alasPath, `./config`);
|
||||
const files: Dirent[] = fs.readdirSync(configPath, {withFileTypes: true});
|
||||
const filesInfoList: fileInfoItem[] = files.map((file: Dirent) => {
|
||||
const name = file.name;
|
||||
const filePath = path.join(configPath, name);
|
||||
return {
|
||||
name,
|
||||
path: filePath,
|
||||
lastModifyTime: getFileUpdateDate(filePath),
|
||||
};
|
||||
});
|
||||
return {
|
||||
configPath,
|
||||
files: filesInfoList,
|
||||
};
|
||||
}
|
||||
|
||||
export function getFileUpdateDate(path: string) {
|
||||
const stat = fs.statSync(path);
|
||||
return stat.mtime;
|
||||
}
|
13
webapp/packages/preload/src/electronApi.ts
Normal file
13
webapp/packages/preload/src/electronApi.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import {ipcRenderer} from 'electron';
|
||||
import IpcRenderer = Electron.IpcRenderer;
|
||||
|
||||
export function ipcRendererSend(channel: string, ...args: any[]): void {
|
||||
ipcRenderer.send(channel, ...args);
|
||||
}
|
||||
|
||||
export function ipcRendererOn(
|
||||
channel: string,
|
||||
listener: (event: Electron.IpcRendererEvent, ...args: any[]) => void,
|
||||
): IpcRenderer {
|
||||
return ipcRenderer.on(channel, listener);
|
||||
}
|
10
webapp/packages/preload/src/index.ts
Normal file
10
webapp/packages/preload/src/index.ts
Normal file
@ -0,0 +1,10 @@
|
||||
/**
|
||||
* @module preload
|
||||
*/
|
||||
|
||||
export {sha256sum} from './nodeCrypto';
|
||||
export {versions} from './versions';
|
||||
export {ipcRendererSend, ipcRendererOn} from './electronApi';
|
||||
export {getAlasConfig, checkIsNeedInstall, getAlasConfigDirFiles} from './alasConfig';
|
||||
export {copyFilesToDir} from '@common/utils/copyFilesToDir';
|
||||
export {modifyConfigYaml} from './modifyConfigYaml';
|
8
webapp/packages/preload/src/modifyConfigYaml.ts
Normal file
8
webapp/packages/preload/src/modifyConfigYaml.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import {join, normalize} from 'path';
|
||||
import {modifyYaml} from '@common/utils';
|
||||
import {ALAS_CONFIG_YAML} from '@common/constant/config';
|
||||
|
||||
export function modifyConfigYaml(path: string, keyObj: {[k in string]: any}) {
|
||||
const configYamlPath = join(normalize(path) + `./config/${ALAS_CONFIG_YAML}`);
|
||||
return modifyYaml(configYamlPath, keyObj);
|
||||
}
|
5
webapp/packages/preload/src/nodeCrypto.ts
Normal file
5
webapp/packages/preload/src/nodeCrypto.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import {type BinaryLike, createHash} from 'node:crypto';
|
||||
|
||||
export function sha256sum(data: BinaryLike) {
|
||||
return createHash('sha256').update(data).digest('hex');
|
||||
}
|
1
webapp/packages/preload/src/versions.ts
Normal file
1
webapp/packages/preload/src/versions.ts
Normal file
@ -0,0 +1 @@
|
||||
export {versions} from 'node:process';
|
15
webapp/packages/preload/tests/modifyYaml.test.ts
Normal file
15
webapp/packages/preload/tests/modifyYaml.test.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import {test, expect} from 'vitest';
|
||||
import {modifyYaml} from '../../common/utils/modifyYaml';
|
||||
import getAlasABSPath from '../../common/utils/getAlasABSPath';
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
test('test write yaml', () => {
|
||||
const absPath = getAlasABSPath();
|
||||
const yamlPath = path.join(absPath, './config/deploy.yaml');
|
||||
modifyYaml(yamlPath, {Branch: 'dev'});
|
||||
const newYamlConfig1 = require('yaml').parse(fs.readFileSync(yamlPath, 'utf8'));
|
||||
expect(newYamlConfig1.Deploy.Git.Branch).toBe('dev');
|
||||
modifyYaml(yamlPath, {Branch: 'master'});
|
||||
const newYamlConfig2 = require('yaml').parse(fs.readFileSync(yamlPath, 'utf8'));
|
||||
expect(newYamlConfig2.Deploy.Git.Branch).toBe('master');
|
||||
});
|
15
webapp/packages/preload/tests/unit.spec.ts
Normal file
15
webapp/packages/preload/tests/unit.spec.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import {createHash} from 'crypto';
|
||||
import {expect, test} from 'vitest';
|
||||
import {sha256sum, versions} from '../src';
|
||||
|
||||
test('versions', async () => {
|
||||
expect(versions).toBe(process.versions);
|
||||
});
|
||||
|
||||
test('nodeCrypto', async () => {
|
||||
// Test hashing a random string.
|
||||
const testString = Math.random().toString(36).slice(2, 7);
|
||||
const expectedHash = createHash('sha256').update(testString).digest('hex');
|
||||
|
||||
expect(sha256sum(testString)).toBe(expectedHash);
|
||||
});
|
18
webapp/packages/preload/tsconfig.json
Normal file
18
webapp/packages/preload/tsconfig.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "esnext",
|
||||
"target": "esnext",
|
||||
"sourceMap": false,
|
||||
"moduleResolution": "Node",
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"isolatedModules": true,
|
||||
"types": ["node"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@common/*": ["../common/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "../../types/**/*.d.ts","../common/**/*.ts"],
|
||||
"exclude": ["**/*.spec.ts", "**/*.test.ts"]
|
||||
}
|
8
webapp/packages/preload/types/electron-api.d.ts
vendored
Normal file
8
webapp/packages/preload/types/electron-api.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
interface ElectronApi {
|
||||
readonly versions: Readonly<NodeJS.ProcessVersions>;
|
||||
}
|
||||
|
||||
declare interface Window {
|
||||
electron: Readonly<ElectronApi>;
|
||||
electronRequire?: NodeRequire;
|
||||
}
|
47
webapp/packages/preload/vite.config.js
Normal file
47
webapp/packages/preload/vite.config.js
Normal file
@ -0,0 +1,47 @@
|
||||
import {chrome} from '../../.electron-vendors.cache.json';
|
||||
import {preload} from 'unplugin-auto-expose';
|
||||
import {join} from 'node:path';
|
||||
import {injectAppVersion} from '../../version/inject-app-version-plugin.mjs';
|
||||
|
||||
const PACKAGE_ROOT = __dirname;
|
||||
const PROJECT_ROOT = join(PACKAGE_ROOT, '../..');
|
||||
|
||||
/**
|
||||
* @type {import('vite').UserConfig}
|
||||
* @see https://vitejs.dev/config/
|
||||
*/
|
||||
const config = {
|
||||
mode: process.env.MODE,
|
||||
root: PACKAGE_ROOT,
|
||||
envDir: PROJECT_ROOT,
|
||||
resolve: {
|
||||
alias: [
|
||||
{
|
||||
find: '@common',
|
||||
replacement: join(PACKAGE_ROOT, '../common'),
|
||||
},
|
||||
],
|
||||
},
|
||||
build: {
|
||||
ssr: true,
|
||||
sourcemap: 'inline',
|
||||
target: `chrome${chrome}`,
|
||||
outDir: 'dist',
|
||||
assetsDir: '.',
|
||||
minify: process.env.MODE !== 'development',
|
||||
lib: {
|
||||
entry: 'src/index.ts',
|
||||
formats: ['cjs'],
|
||||
},
|
||||
rollupOptions: {
|
||||
output: {
|
||||
entryFileNames: '[name].cjs',
|
||||
},
|
||||
},
|
||||
emptyOutDir: true,
|
||||
reportCompressedSize: false,
|
||||
},
|
||||
plugins: [preload.vite(), injectAppVersion()],
|
||||
};
|
||||
|
||||
export default config;
|
20
webapp/packages/renderer/.eslintrc.json
Normal file
20
webapp/packages/renderer/.eslintrc.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": false
|
||||
},
|
||||
"extends": [
|
||||
/** @see https://eslint.vuejs.org/rules/ */
|
||||
"plugin:vue/vue3-recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"ecmaVersion": 12,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
/** These rules are disabled because they are incompatible with prettier */
|
||||
"vue/html-self-closing": "off",
|
||||
"vue/singleline-html-element-content-newline": "off"
|
||||
}
|
||||
}
|
8
webapp/packages/renderer/auto-imports.d.ts
vendored
Normal file
8
webapp/packages/renderer/auto-imports.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-auto-import
|
||||
export {}
|
||||
declare global {
|
||||
|
||||
}
|
21
webapp/packages/renderer/components.d.ts
vendored
Normal file
21
webapp/packages/renderer/components.d.ts
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
import '@vue/runtime-core'
|
||||
|
||||
export {}
|
||||
|
||||
declare module '@vue/runtime-core' {
|
||||
export interface GlobalComponents {
|
||||
Alas: typeof import('./src/components/Alas.vue')['default']
|
||||
AlasTitle: typeof import('./src/components/AlasTitle.vue')['default']
|
||||
AppHeader: typeof import('./src/components/AppHeader.vue')['default']
|
||||
CountTo: typeof import('./src/components/CountTo.vue')['default']
|
||||
ProgressBar: typeof import('./src/components/ProgressBar.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
Spin: typeof import('./src/components/Spin.vue')['default']
|
||||
}
|
||||
}
|
22
webapp/packages/renderer/index.html
Normal file
22
webapp/packages/renderer/index.html
Normal file
@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html
|
||||
lang="en-US"
|
||||
id="htmlRoot"
|
||||
>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta content="script-src 'self' blob:" http-equiv="Content-Security-Policy">
|
||||
<meta
|
||||
content="width=device-width, initial-scale=1.0"
|
||||
name="viewport"
|
||||
/>
|
||||
<title>Alas</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script
|
||||
src="./src/index.ts"
|
||||
type="module"
|
||||
></script>
|
||||
</body>
|
||||
</html>
|
91
webapp/packages/renderer/src/App.vue
Normal file
91
webapp/packages/renderer/src/App.vue
Normal file
@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<a-config-provider :locale="locale">
|
||||
<div
|
||||
id="app"
|
||||
class="bg-white dark:bg-dark text-slate dark:text-neutral"
|
||||
>
|
||||
<app-header></app-header>
|
||||
<router-view>
|
||||
<template #default="{Component, route}">
|
||||
<transition
|
||||
name="fade-slide"
|
||||
mode="out-in"
|
||||
appear
|
||||
>
|
||||
<component
|
||||
:is="Component"
|
||||
:key="route.fullPath"
|
||||
/>
|
||||
</transition>
|
||||
</template>
|
||||
</router-view>
|
||||
</div>
|
||||
</a-config-provider>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent, unref} from 'vue';
|
||||
import AppHeader from '/@/components/AppHeader.vue';
|
||||
import {useLocale} from '/@/locales/useLocale';
|
||||
import {setTheme} from '/@/settings/themeSetting';
|
||||
import {useAppStoreWithOut} from '/@/store/modules/app';
|
||||
import zhCN from '@arco-design/web-vue/es/locale/lang/zh-cn';
|
||||
import enUS from '@arco-design/web-vue/es/locale/lang/en-us';
|
||||
import jaJP from '@arco-design/web-vue/es/locale/lang/ja-jp';
|
||||
import zhTW from '@arco-design/web-vue/es/locale/lang/zh-tw';
|
||||
import type {ArcoLang} from '@arco-design/web-vue/es/locale/interface';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'App',
|
||||
components: {
|
||||
AppHeader,
|
||||
},
|
||||
setup() {
|
||||
const {getLocale} = useLocale();
|
||||
const locales = {
|
||||
'zh-CN': zhCN,
|
||||
'en-US': enUS,
|
||||
'ja-JP': jaJP,
|
||||
'zh-TW': zhTW,
|
||||
};
|
||||
|
||||
const locale = computed<ArcoLang>(() => {
|
||||
const language = unref(getLocale);
|
||||
return locales[language];
|
||||
});
|
||||
const appStore = useAppStoreWithOut();
|
||||
setTheme(appStore.theme);
|
||||
return {
|
||||
locale,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#app {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.fade-slide-leave-active,
|
||||
.fade-slide-enter-active {
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.fade-slide-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
}
|
||||
|
||||
.fade-slide-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
||||
</style>
|
31
webapp/packages/renderer/src/components/Alas.vue
Normal file
31
webapp/packages/renderer/src/components/Alas.vue
Normal file
@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<iframe
|
||||
class="alas"
|
||||
:src="webuiUrl"
|
||||
></iframe>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent} from 'vue';
|
||||
import {useAppStoreWithOut} from '/@/store/modules/app';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'AlasPage',
|
||||
setup() {
|
||||
const appStore = useAppStoreWithOut();
|
||||
const webuiUrl = computed(() => appStore.webuiUrl);
|
||||
return {
|
||||
webuiUrl,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.alas {
|
||||
border-width: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
15
webapp/packages/renderer/src/components/AlasTitle.vue
Normal file
15
webapp/packages/renderer/src/components/AlasTitle.vue
Normal file
@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<section class="text-6xl font-extralight tracking-wide">
|
||||
<span class="text-7xl text-primary">A</span>zur<span class="text-7xl text-primary">L</span
|
||||
>ane<span class="text-7xl text-primary">A</span>uto<span class="text-7xl text-primary">S</span
|
||||
>cript
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent} from 'vue';
|
||||
export default defineComponent({
|
||||
name: 'AlasTitle',
|
||||
components: {},
|
||||
});
|
||||
</script>
|
128
webapp/packages/renderer/src/components/AppHeader.vue
Normal file
128
webapp/packages/renderer/src/components/AppHeader.vue
Normal file
@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<div class="app-header">
|
||||
<div class="header-drag"></div>
|
||||
<div class="header-icon">
|
||||
<ArrowDownOutlined
|
||||
class="icon"
|
||||
@click="trayWin"
|
||||
></ArrowDownOutlined>
|
||||
<MinusOutlined
|
||||
class="icon"
|
||||
@click="minimizeWin"
|
||||
></MinusOutlined>
|
||||
<BorderOutlined
|
||||
class="icon"
|
||||
@click="maximizeWin"
|
||||
></BorderOutlined>
|
||||
<CloseOutlined
|
||||
class="icon"
|
||||
@click="closeWin"
|
||||
></CloseOutlined>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, h} from 'vue';
|
||||
import {
|
||||
BorderOutlined,
|
||||
CloseOutlined,
|
||||
MinusOutlined,
|
||||
ArrowDownOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import useIpcRenderer from '/@/hooks/useIpcRenderer';
|
||||
import {Modal} from '@arco-design/web-vue';
|
||||
import {useI18n} from '/@/hooks/useI18n';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'AppHeader',
|
||||
components: {
|
||||
ArrowDownOutlined,
|
||||
MinusOutlined,
|
||||
BorderOutlined,
|
||||
CloseOutlined,
|
||||
},
|
||||
setup() {
|
||||
const {t} = useI18n();
|
||||
|
||||
const {ipcRendererSend} = useIpcRenderer();
|
||||
const trayWin = () => {
|
||||
ipcRendererSend('window-tray');
|
||||
};
|
||||
const minimizeWin = () => {
|
||||
ipcRendererSend('window-minimize');
|
||||
};
|
||||
const maximizeWin = () => {
|
||||
ipcRendererSend('window-maximize');
|
||||
};
|
||||
const closeWin = () => {
|
||||
Modal.confirm({
|
||||
title: () =>
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
class: 'flex justify-center items-center font-bold',
|
||||
},
|
||||
t('modal.closeTipTitle'),
|
||||
),
|
||||
content: () =>
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
class: 'flex justify-center items-center',
|
||||
},
|
||||
t('modal.closeTipContent'),
|
||||
),
|
||||
cancelText: t('modal.cancelText'),
|
||||
okText: t('modal.okText'),
|
||||
titleAlign: 'center',
|
||||
okButtonProps: {
|
||||
size: 'medium',
|
||||
},
|
||||
onOk() {
|
||||
ipcRendererSend('window-close');
|
||||
},
|
||||
});
|
||||
};
|
||||
return {
|
||||
trayWin,
|
||||
minimizeWin,
|
||||
maximizeWin,
|
||||
closeWin,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-header {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 51px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
-webkit-app-region: drag;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.header-drag {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
-webkit-app-region: no-drag;
|
||||
text-align: right;
|
||||
font-size: 20px;
|
||||
color: #7c7c7c;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.icon {
|
||||
padding: 10px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
</style>
|
109
webapp/packages/renderer/src/components/CountTo.vue
Normal file
109
webapp/packages/renderer/src/components/CountTo.vue
Normal file
@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<span
|
||||
:class="{prefixClass}"
|
||||
:style="{color}"
|
||||
>
|
||||
{{ value }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import type {PropType} from 'vue';
|
||||
import {defineComponent, ref, watchEffect, computed, unref, watch, onMounted} from 'vue';
|
||||
import {useTransition, TransitionPresets} from '@vueuse/core';
|
||||
import {isNumber} from '/@/utils/is';
|
||||
export default defineComponent({
|
||||
name: 'CountTo',
|
||||
props: {
|
||||
startVal: {type: Number, default: 0},
|
||||
endVal: {type: Number, default: 100},
|
||||
duration: {type: Number, default: 1500},
|
||||
autoplay: {type: Boolean, default: false},
|
||||
decimals: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
validator(value: number) {
|
||||
return value >= 0;
|
||||
},
|
||||
},
|
||||
prefix: {type: String, default: ''},
|
||||
suffix: {type: String, default: ''},
|
||||
separator: {type: String, default: ','},
|
||||
decimal: {type: String, default: '.'},
|
||||
prefixClass: {type: String, default: ''},
|
||||
color: {type: String},
|
||||
useEasing: {type: Boolean, default: true},
|
||||
transition: {type: String as PropType<keyof typeof TransitionPresets>, default: 'linear'},
|
||||
},
|
||||
emits: ['onStarted', 'onFinished'],
|
||||
setup(props, {emit}) {
|
||||
const source = ref(props.startVal);
|
||||
const disabled = ref(false);
|
||||
let outputValue = useTransition(source);
|
||||
const value = computed(() => formatNumber(unref(outputValue)));
|
||||
|
||||
watchEffect(() => {
|
||||
source.value = props.startVal;
|
||||
});
|
||||
|
||||
function formatNumber(num: number | string) {
|
||||
if (!num && num !== 0) {
|
||||
return '';
|
||||
}
|
||||
const {decimals, decimal, separator, suffix, prefix} = props;
|
||||
num = Number(num).toFixed(decimals);
|
||||
num += '';
|
||||
|
||||
const x = num.split('.');
|
||||
let x1 = x[0];
|
||||
const x2 = x.length > 1 ? decimal + x[1] : '';
|
||||
|
||||
const rgx = /(\d+)(\d{3})/;
|
||||
if (separator && !isNumber(separator)) {
|
||||
while (rgx.test(x1)) {
|
||||
x1 = x1.replace(rgx, '$1' + separator + '$2');
|
||||
}
|
||||
}
|
||||
return prefix + x1 + x2 + suffix;
|
||||
}
|
||||
|
||||
watch([() => props.startVal, () => props.endVal], () => {
|
||||
if (props.autoplay) {
|
||||
start();
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
props.autoplay && start();
|
||||
});
|
||||
|
||||
function start() {
|
||||
run();
|
||||
source.value = props.endVal;
|
||||
}
|
||||
|
||||
function reset() {
|
||||
source.value = props.startVal;
|
||||
run();
|
||||
}
|
||||
|
||||
function run() {
|
||||
outputValue = useTransition(source, {
|
||||
disabled,
|
||||
duration: props.duration,
|
||||
onFinished: () => emit('onFinished'),
|
||||
onStarted: () => emit('onStarted'),
|
||||
...(props.useEasing ? {transition: TransitionPresets[props.transition]} : {}),
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
value,
|
||||
start,
|
||||
reset,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
31
webapp/packages/renderer/src/components/ProgressBar.vue
Normal file
31
webapp/packages/renderer/src/components/ProgressBar.vue
Normal file
@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<section class="w-full">
|
||||
<CountTo
|
||||
:class="'text-primary flex text-2xl ml-5 mb-3'"
|
||||
:start-val="progressValue"
|
||||
suffix="%"
|
||||
/><div
|
||||
class="h-3 bg-primary ease-in-out duration-500"
|
||||
:style="getWrapStyle"
|
||||
></div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import VueTypes from 'vue-types';
|
||||
import type {CSSProperties} from 'vue';
|
||||
import {computed, unref} from 'vue';
|
||||
import CountTo from './CountTo.vue';
|
||||
|
||||
const props = defineProps({
|
||||
progressValue: VueTypes.number.def(0),
|
||||
});
|
||||
|
||||
const getWrapStyle = computed((): CSSProperties => {
|
||||
return {
|
||||
width: `${unref(isNaN(props.progressValue) ? 0 : props.progressValue)}%`,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
9
webapp/packages/renderer/src/components/Spin.vue
Normal file
9
webapp/packages/renderer/src/components/Spin.vue
Normal file
@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<div> </div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps({});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
111
webapp/packages/renderer/src/design/color.less
Normal file
111
webapp/packages/renderer/src/design/color.less
Normal file
@ -0,0 +1,111 @@
|
||||
html {
|
||||
|
||||
}
|
||||
|
||||
@white: #fff;
|
||||
|
||||
@content-bg: #f4f7f9;
|
||||
|
||||
@iconify-bg-color: #5551;
|
||||
|
||||
// =================================
|
||||
// ==============border-color=======
|
||||
// =================================
|
||||
|
||||
// Dark-dark
|
||||
@border-color-dark: #b6b7b9;
|
||||
|
||||
// Dark-light
|
||||
@border-color-shallow-dark: #cececd;
|
||||
|
||||
// Light-dark
|
||||
@border-color-light: @border-color-base;
|
||||
|
||||
// =================================
|
||||
// ==============message==============
|
||||
// =================================
|
||||
|
||||
// success-bg-color
|
||||
@success-background-color: #f1f9ec;
|
||||
// info-bg-color
|
||||
@info-background-color: #e8eff8;
|
||||
// warn-bg-color
|
||||
@warning-background-color: #fdf6ed;
|
||||
// danger-bg-color
|
||||
@danger-background-color: #fef0f0;
|
||||
|
||||
// top-menu
|
||||
@top-menu-active-bg-color: var(--header-active-menu-bg-color);
|
||||
|
||||
// =================================
|
||||
// ==============Menu============
|
||||
// =================================
|
||||
|
||||
// trigger
|
||||
@trigger-dark-hover-bg-color: rgba(255, 255, 255, 0.2);
|
||||
@trigger-dark-bg-color: rgba(255, 255, 255, 0.1);
|
||||
|
||||
// =================================
|
||||
// ==============tree============
|
||||
// =================================
|
||||
// tree item hover background
|
||||
@tree-hover-background-color: #f5f7fa;
|
||||
// tree item hover font color
|
||||
@tree-hover-font-color: #f5f7fa;
|
||||
|
||||
// =================================
|
||||
// ==============link============
|
||||
// =================================
|
||||
@link-hover-color: @primary-color;
|
||||
@link-active-color: darken(@primary-color, 10%);
|
||||
|
||||
// =================================
|
||||
// ==============Text color-=============
|
||||
// =================================
|
||||
|
||||
// Main text color
|
||||
@text-color-base: @text-color;
|
||||
|
||||
// Label color
|
||||
@text-color-call-out: #606266;
|
||||
|
||||
// Auxiliary information color-dark
|
||||
@text-color-help-dark: #909399;
|
||||
|
||||
// =================================
|
||||
// ==============breadcrumb=========
|
||||
// =================================
|
||||
@breadcrumb-item-normal-color: #999;
|
||||
// =================================
|
||||
// ==============button=============
|
||||
// =================================
|
||||
|
||||
@button-primary-color: @primary-color;
|
||||
@button-primary-hover-color: lighten(@primary-color, 5%);
|
||||
@button-primary-active-color: darken(@primary-color, 5%);
|
||||
|
||||
@button-ghost-color: @white;
|
||||
@button-ghost-hover-color: lighten(@white, 10%);
|
||||
@button-ghost-hover-bg-color: #e1ebf6;
|
||||
@button-ghost-active-color: darken(@white, 10%);
|
||||
|
||||
@button-success-color: @success-color;
|
||||
@button-success-hover-color: lighten(@success-color, 10%);
|
||||
@button-success-active-color: darken(@success-color, 10%);
|
||||
|
||||
@button-warn-color: @warning-color;
|
||||
@button-warn-hover-color: lighten(@warning-color, 10%);
|
||||
@button-warn-active-color: darken(@warning-color, 10%);
|
||||
|
||||
@button-error-color: @error-color;
|
||||
@button-error-hover-color: lighten(@error-color, 10%);
|
||||
@button-error-active-color: darken(@error-color, 10%);
|
||||
|
||||
@button-cancel-color: @text-color-call-out;
|
||||
@button-cancel-bg-color: @white;
|
||||
@button-cancel-border-color: @border-color-shallow-dark;
|
||||
|
||||
// Mouse over
|
||||
@button-cancel-hover-color: @primary-color;
|
||||
@button-cancel-hover-bg-color: @white;
|
||||
@button-cancel-hover-border-color: @primary-color;
|
2
webapp/packages/renderer/src/design/config.less
Normal file
2
webapp/packages/renderer/src/design/config.less
Normal file
@ -0,0 +1,2 @@
|
||||
@import (reference) 'color.less';
|
||||
@import (reference) 'var/index.less';
|
41
webapp/packages/renderer/src/design/index.less
Normal file
41
webapp/packages/renderer/src/design/index.less
Normal file
@ -0,0 +1,41 @@
|
||||
@import "./theme.less";
|
||||
@import 'var/index.less';
|
||||
|
||||
input:-webkit-autofill {
|
||||
box-shadow: 0 0 0 1000px white inset !important;
|
||||
}
|
||||
|
||||
:-webkit-autofill {
|
||||
transition: background-color 5000s ease-in-out 0s !important;
|
||||
}
|
||||
|
||||
html {
|
||||
overflow: hidden;
|
||||
text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: visible !important;
|
||||
overflow-x: hidden !important;
|
||||
|
||||
&.color-weak {
|
||||
filter: invert(80%);
|
||||
}
|
||||
|
||||
&.gray-mode {
|
||||
filter: grayscale(100%);
|
||||
filter: progid:dximagetransform.microsoft.basicimage(grayscale=1);
|
||||
}
|
||||
}
|
||||
|
||||
a:focus,
|
||||
a:active,
|
||||
button,
|
||||
div,
|
||||
svg,
|
||||
span {
|
||||
outline: none !important;
|
||||
}
|
9
webapp/packages/renderer/src/design/theme.less
Normal file
9
webapp/packages/renderer/src/design/theme.less
Normal file
@ -0,0 +1,9 @@
|
||||
html[arco-theme='light'] {
|
||||
|
||||
}
|
||||
|
||||
[arco-theme='dark'] {
|
||||
|
||||
|
||||
|
||||
}
|
46
webapp/packages/renderer/src/hooks/useI18n.ts
Normal file
46
webapp/packages/renderer/src/hooks/useI18n.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import {i18n} from '/@/locales/setupI18n';
|
||||
|
||||
export interface I18nGlobalTranslation {
|
||||
(key: string): string;
|
||||
(key: string, locale?: string): string;
|
||||
(key: string, locale?: string, list?: unknown[]): string;
|
||||
(key: string, locale?: string, named?: Record<string, unknown>): string;
|
||||
(key: string, list?: unknown[]): string;
|
||||
(key: string, named?: Record<string, unknown>): string;
|
||||
}
|
||||
|
||||
type I18nTranslationRestParameters = [string, any];
|
||||
|
||||
function getKey(namespace: string | undefined, key: string): string {
|
||||
if (!namespace) {
|
||||
return key;
|
||||
}
|
||||
if (key.startsWith(namespace)) {
|
||||
return key;
|
||||
}
|
||||
return `${namespace}.${key}`;
|
||||
}
|
||||
|
||||
export function useI18n(namespace?: string) {
|
||||
const normalFn = {
|
||||
t: (key: string) => {
|
||||
return getKey(namespace, key);
|
||||
},
|
||||
};
|
||||
if (!i18n) {
|
||||
return normalFn;
|
||||
}
|
||||
|
||||
const {t, ...methods} = i18n.global;
|
||||
|
||||
const tFn: I18nGlobalTranslation = (key: string, ...arg: any[]) => {
|
||||
if (!key) return '';
|
||||
if (!key.includes('.') && !namespace) return key;
|
||||
return t(getKey(namespace, key), ...(arg as I18nTranslationRestParameters));
|
||||
};
|
||||
|
||||
return {
|
||||
...methods,
|
||||
t: tFn,
|
||||
};
|
||||
}
|
8
webapp/packages/renderer/src/hooks/useIpcRenderer.ts
Normal file
8
webapp/packages/renderer/src/hooks/useIpcRenderer.ts
Normal file
@ -0,0 +1,8 @@
|
||||
const useIpcRenderer = () => {
|
||||
return {
|
||||
ipcRendererSend: window.__electron_preload__ipcRendererSend,
|
||||
ipcRendererOn: window.__electron_preload__ipcRendererOn,
|
||||
};
|
||||
};
|
||||
|
||||
export default useIpcRenderer;
|
3
webapp/packages/renderer/src/index.less
Normal file
3
webapp/packages/renderer/src/index.less
Normal file
@ -0,0 +1,3 @@
|
||||
@import '@unocss/reset/tailwind.css';
|
||||
@import '@unocss/reset/tailwind-compat.css';
|
||||
|
33
webapp/packages/renderer/src/index.ts
Normal file
33
webapp/packages/renderer/src/index.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import {createApp} from 'vue';
|
||||
import App from '/@/App.vue';
|
||||
import router from '/@/router';
|
||||
import {setupI18n} from '/@/locales/setupI18n';
|
||||
import {setupThemeSetting} from '/@/settings/themeSetting';
|
||||
import {setupStore} from '/@/store';
|
||||
import {initAppConfigStore} from '/@/logics/initAppConfigStore';
|
||||
import './index.less';
|
||||
import 'uno.css';
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
/**
|
||||
* Ensure that the style at development time is consistent with the style after packaging
|
||||
*/
|
||||
import('@arco-design/web-vue/dist/arco.less');
|
||||
}
|
||||
|
||||
async function bootstrap() {
|
||||
const app = createApp(App);
|
||||
setupStore(app);
|
||||
|
||||
await initAppConfigStore();
|
||||
|
||||
await setupI18n(app);
|
||||
|
||||
app.use(router);
|
||||
|
||||
setupThemeSetting();
|
||||
|
||||
app.mount('#app');
|
||||
}
|
||||
|
||||
await bootstrap();
|
11
webapp/packages/renderer/src/locales/helper.ts
Normal file
11
webapp/packages/renderer/src/locales/helper.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import type {LocaleType} from '/#/config';
|
||||
|
||||
export const loadLocalePool: LocaleType[] = [];
|
||||
|
||||
export function setHtmlPageLang(locale: LocaleType) {
|
||||
document.querySelector('html')?.setAttribute('lang', locale);
|
||||
}
|
||||
|
||||
export function setLoadLocalePool(cb: (loadLocalePool: LocaleType[]) => void) {
|
||||
cb(loadLocalePool);
|
||||
}
|
37
webapp/packages/renderer/src/locales/lang/en-US.json
Normal file
37
webapp/packages/renderer/src/locales/lang/en-US.json
Normal file
@ -0,0 +1,37 @@
|
||||
{
|
||||
"common": {
|
||||
"theme": "Theme",
|
||||
"light": "Light",
|
||||
"dark": "Dark",
|
||||
"language": "Language",
|
||||
"update": "Update",
|
||||
"global": "Global",
|
||||
"china": "China",
|
||||
"install": "Install",
|
||||
"installTips": "Used ALAS before? Import your configs here"
|
||||
},
|
||||
"import": {
|
||||
"title": "Import user configs from old ALAS",
|
||||
"tips": "Import configs from old ALAS",
|
||||
"error": "Import failed, please check your configs",
|
||||
"step1": "Select old config files",
|
||||
"step2": "Confirm import",
|
||||
"step3": "Import succeed",
|
||||
"filePathTips": " under Present profile:",
|
||||
"btnOk": "Confirm",
|
||||
"btnImport": "Import",
|
||||
"btnGoBack": "Back",
|
||||
"btnReimport": "Reimport",
|
||||
"fileName": "File Name",
|
||||
"lastModify": "Last Modify Time",
|
||||
"file": {
|
||||
"choose": "Choose files"
|
||||
}
|
||||
},
|
||||
"modal": {
|
||||
"closeTipTitle": "Sure to close ALAS ?",
|
||||
"closeTipContent": "All running instances will be terminated",
|
||||
"cancelText": "Cancel",
|
||||
"okText": "Confirm"
|
||||
}
|
||||
}
|
37
webapp/packages/renderer/src/locales/lang/ja-JP.json
Normal file
37
webapp/packages/renderer/src/locales/lang/ja-JP.json
Normal file
@ -0,0 +1,37 @@
|
||||
{
|
||||
"common": {
|
||||
"theme": "テーマ",
|
||||
"light": "ライトテーマ",
|
||||
"dark": "ダークテーマ",
|
||||
"language": "言語",
|
||||
"update": "アップデート",
|
||||
"global": "グローバル",
|
||||
"china": "中国",
|
||||
"install": "インストール",
|
||||
"installTips": "以前に Alas を使用したことがありますか?ここで設定をインポートすることができます"
|
||||
},
|
||||
"import": {
|
||||
"title": "既存の Alas からユーザー設定をインポートする",
|
||||
"tips": "Alas の設定ファイルをインポートすると、以前の設定をすべて復元することができます",
|
||||
"error": "ファイルの読み込みに失敗しました",
|
||||
"step1": "インポートするファイルを選択",
|
||||
"step2": "インポート中",
|
||||
"step3": "インポート完了",
|
||||
"filePathTips": " の中で以下の設定ファイルがあります:",
|
||||
"btnOk": "確認",
|
||||
"btnImport": "インポート",
|
||||
"btnGoBack": "戻る",
|
||||
"btnReimport": "再インポート",
|
||||
"fileName": "ファイル名",
|
||||
"lastModify": "最終更新日時",
|
||||
"file": {
|
||||
"choose": "ファイルを選択"
|
||||
}
|
||||
},
|
||||
"modal": {
|
||||
"closeTipTitle": "Alas を閉じますか?",
|
||||
"closeTipContent": "稼働中のすべてのインスタンスも停止します",
|
||||
"cancelText": "キャンセル",
|
||||
"okText": "確認"
|
||||
}
|
||||
}
|
37
webapp/packages/renderer/src/locales/lang/zh-CN.json
Normal file
37
webapp/packages/renderer/src/locales/lang/zh-CN.json
Normal file
@ -0,0 +1,37 @@
|
||||
{
|
||||
"common": {
|
||||
"theme": "主题",
|
||||
"light": "亮色主题",
|
||||
"dark": "黑暗主题",
|
||||
"language": "语言",
|
||||
"update": "更新",
|
||||
"global": "全球",
|
||||
"china": "中国大陆",
|
||||
"install": "安装",
|
||||
"installTips": "以前使用过 Alas?在这里导入你的设置"
|
||||
},
|
||||
"import": {
|
||||
"title": "从已有Alas中导入用户设置",
|
||||
"tips": "请将你的 Alas 设置文件拖拽到这里",
|
||||
"error": "导入失败,请检查文件是否正确",
|
||||
"step1": "选择旧的 Alas",
|
||||
"step2": "确认配置文件",
|
||||
"step3": "导入完成",
|
||||
"filePathTips": " 下存在的配置文件:",
|
||||
"btnOk": "确认",
|
||||
"btnImport": "导入",
|
||||
"btnGoBack": "返回",
|
||||
"btnReimport": "重新导入",
|
||||
"fileName": "文件名",
|
||||
"lastModify": "最后修改时间",
|
||||
"file": {
|
||||
"choose": "点击此处选择文件"
|
||||
}
|
||||
},
|
||||
"modal": {
|
||||
"closeTipTitle": "确定要关闭Alas吗?",
|
||||
"closeTipContent": "所有正在运行的Alas实例都将被终止",
|
||||
"cancelText": "取消",
|
||||
"okText": "确定"
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user