Add: [ALAS] webapp

This commit is contained in:
LmeSzinc 2023-09-10 00:22:01 +08:00
parent 0067e343b2
commit badbe52b11
140 changed files with 17389 additions and 0 deletions

18
webapp/.editorconfig Normal file
View 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

View 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
View File

55
webapp/.eslintrc.json Normal file
View 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
View 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
View File

@ -0,0 +1,3 @@
# These are supported funding model platforms
custom: ["https://www.buymeacoffee.com/kozack/", "https://send.monobank.ua/6SmojkkR9i"]

View 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.

View 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.

View 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.

View 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'

View 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
View 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
View 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
View 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
View 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
View 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'

View 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

View 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
View 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
View 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
View File

@ -0,0 +1 @@
auto-install-peers=true

10
webapp/.prettierignore Normal file
View 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
View 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
}

View File

@ -0,0 +1,3 @@
{
"pre-commit": "npx nano-staged"
}

13
webapp/.vscode/launch.json vendored Normal file
View 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
View 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
View 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
View 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/

View File

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

34
webapp/contributing.md Normal file
View 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 were 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.

View 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;

View File

@ -0,0 +1,4 @@
{
"chrome": "94",
"node": "16"
}

10990
webapp/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

87
webapp/package.json Normal file
View 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"
}
}

View 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',
};

View 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';

View 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',
};

View 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}`,
),
);
}

View 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);
}
}
}

View File

@ -0,0 +1,3 @@
export const isWindows = process.platform === 'win32';
export const isMacintosh = process.platform === 'darwin';
export const isLinux = process.platform === 'linux';

View 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;

View 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};

View 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);
}
}

View 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;
}

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

View 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);
});
};

View 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;

View 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() {
/**
*
*/
}
}

View 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,
};
};

View 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;
};

View 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();
}
});

View 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);
},
};

View 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();
}
}

View 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;
}
}

View 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;

View 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;
});
});

View 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;
};

View 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;
};

View File

@ -0,0 +1,4 @@
import {createInstaller} from './createInstaller';
import {createAlas} from './createAlas';
export {createInstaller, createAlas};

View 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);
});

View 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"]
}

View 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;

View 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;
}

View 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);
}

View 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';

View 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);
}

View File

@ -0,0 +1,5 @@
import {type BinaryLike, createHash} from 'node:crypto';
export function sha256sum(data: BinaryLike) {
return createHash('sha256').update(data).digest('hex');
}

View File

@ -0,0 +1 @@
export {versions} from 'node:process';

View 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');
});

View 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);
});

View 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"]
}

View File

@ -0,0 +1,8 @@
interface ElectronApi {
readonly versions: Readonly<NodeJS.ProcessVersions>;
}
declare interface Window {
electron: Readonly<ElectronApi>;
electronRequire?: NodeRequire;
}

View 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;

View 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"
}
}

View File

@ -0,0 +1,8 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-auto-import
export {}
declare global {
}

View 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']
}
}

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@ -0,0 +1,9 @@
<template>
<div> </div>
</template>
<script lang="ts" setup>
defineProps({});
</script>
<style scoped></style>

View 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;

View File

@ -0,0 +1,2 @@
@import (reference) 'color.less';
@import (reference) 'var/index.less';

View 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;
}

View File

@ -0,0 +1,9 @@
html[arco-theme='light'] {
}
[arco-theme='dark'] {
}

View 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,
};
}

View File

@ -0,0 +1,8 @@
const useIpcRenderer = () => {
return {
ipcRendererSend: window.__electron_preload__ipcRendererSend,
ipcRendererOn: window.__electron_preload__ipcRendererOn,
};
};
export default useIpcRenderer;

View File

@ -0,0 +1,3 @@
@import '@unocss/reset/tailwind.css';
@import '@unocss/reset/tailwind-compat.css';

View 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();

View 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);
}

View 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"
}
}

View 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": "確認"
}
}

View 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