(!breaking)Del:打扫干净屋子再请客
@@ -1,9 +0,0 @@
|
|||||||
root = true
|
|
||||||
|
|
||||||
[*]
|
|
||||||
charset = utf-8
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 2
|
|
||||||
end_of_line = lf
|
|
||||||
insert_final_newline = true
|
|
||||||
trim_trailing_whitespace = true
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
* text=auto eol=lf
|
|
||||||
@@ -1,233 +0,0 @@
|
|||||||
name: build
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
version:
|
|
||||||
description: '版本号(例如 1.2.3 或 v1.2.3)'
|
|
||||||
required: false
|
|
||||||
build:
|
|
||||||
description: '构建命令标记(build:win|build:mac|build:linux|build:unpack|build:all)'
|
|
||||||
required: false
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
parse:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
outputs:
|
|
||||||
version: ${{ steps.parse.outputs.version }}
|
|
||||||
tag: ${{ steps.parse.outputs.tag }}
|
|
||||||
build_script: ${{ steps.parse.outputs.build_script }}
|
|
||||||
run_win: ${{ steps.parse.outputs.run_win }}
|
|
||||||
run_mac: ${{ steps.parse.outputs.run_mac }}
|
|
||||||
run_linux: ${{ steps.parse.outputs.run_linux }}
|
|
||||||
run_unpack: ${{ steps.parse.outputs.run_unpack }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: 读取提交信息
|
|
||||||
id: msg
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
|
||||||
{
|
|
||||||
echo "message<<__EOF__"
|
|
||||||
echo "${{ github.event.inputs.version }} ${{ github.event.inputs.build }}"
|
|
||||||
echo "__EOF__"
|
|
||||||
} >> "$GITHUB_OUTPUT"
|
|
||||||
else
|
|
||||||
{
|
|
||||||
echo "message<<__EOF__"
|
|
||||||
echo "${{ github.event.head_commit.message }}"
|
|
||||||
echo "__EOF__"
|
|
||||||
} >> "$GITHUB_OUTPUT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 22
|
|
||||||
|
|
||||||
- name: 解析版本与构建命令
|
|
||||||
id: parse
|
|
||||||
env:
|
|
||||||
RELEASE_MESSAGE: ${{ steps.msg.outputs.message }}
|
|
||||||
run: node scripts/ci/parse-commit.mjs
|
|
||||||
|
|
||||||
build_win:
|
|
||||||
needs: parse
|
|
||||||
if: ${{ needs.parse.outputs.run_win == 'true' }}
|
|
||||||
runs-on: windows-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 22
|
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4
|
|
||||||
with:
|
|
||||||
version: 10
|
|
||||||
|
|
||||||
- name: 应用版本号
|
|
||||||
run: node scripts/ci/apply-version.mjs ${{ needs.parse.outputs.version }}
|
|
||||||
|
|
||||||
- name: 安装依赖
|
|
||||||
run: pnpm install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: 构建(Windows)
|
|
||||||
run: pnpm build:win
|
|
||||||
|
|
||||||
- name: 清理 unpacked 目录
|
|
||||||
shell: pwsh
|
|
||||||
run: |
|
|
||||||
if (Test-Path dist) {
|
|
||||||
Get-ChildItem -Path dist -Directory -ErrorAction SilentlyContinue |
|
|
||||||
Where-Object { $_.Name -like '*unpacked*' } |
|
|
||||||
ForEach-Object { Remove-Item -Recurse -Force $_.FullName }
|
|
||||||
}
|
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: dist-win
|
|
||||||
path: dist/**
|
|
||||||
|
|
||||||
build_mac:
|
|
||||||
needs: parse
|
|
||||||
if: ${{ needs.parse.outputs.run_mac == 'true' }}
|
|
||||||
runs-on: macos-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 22
|
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4
|
|
||||||
with:
|
|
||||||
version: 10
|
|
||||||
|
|
||||||
- name: 应用版本号
|
|
||||||
run: node scripts/ci/apply-version.mjs ${{ needs.parse.outputs.version }}
|
|
||||||
|
|
||||||
- name: 安装依赖
|
|
||||||
run: pnpm install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: 构建(macOS)
|
|
||||||
run: pnpm -s build:mac
|
|
||||||
|
|
||||||
- name: 清理 unpacked 目录
|
|
||||||
run: rm -rf dist/*unpacked*
|
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: dist-mac
|
|
||||||
path: dist/**
|
|
||||||
|
|
||||||
build_linux:
|
|
||||||
needs: parse
|
|
||||||
if: ${{ needs.parse.outputs.run_linux == 'true' }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 22
|
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4
|
|
||||||
with:
|
|
||||||
version: 10
|
|
||||||
|
|
||||||
- name: 应用版本号
|
|
||||||
run: node scripts/ci/apply-version.mjs ${{ needs.parse.outputs.version }}
|
|
||||||
|
|
||||||
- name: 安装依赖
|
|
||||||
run: pnpm install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: 构建(Linux)
|
|
||||||
run: pnpm -s build:linux
|
|
||||||
|
|
||||||
- name: 清理 unpacked 目录
|
|
||||||
run: rm -rf dist/*unpacked*
|
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: dist-linux
|
|
||||||
path: dist/**
|
|
||||||
|
|
||||||
build_unpack:
|
|
||||||
needs: parse
|
|
||||||
if: ${{ needs.parse.outputs.run_unpack == 'true' }}
|
|
||||||
runs-on: windows-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 22
|
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4
|
|
||||||
with:
|
|
||||||
version: 10
|
|
||||||
|
|
||||||
- name: 应用版本号
|
|
||||||
run: node scripts/ci/apply-version.mjs ${{ needs.parse.outputs.version }}
|
|
||||||
|
|
||||||
- name: 安装依赖
|
|
||||||
run: pnpm install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: 构建(dir)
|
|
||||||
run: pnpm -s build:unpack
|
|
||||||
|
|
||||||
- name: 打包并移除 unpacked 目录
|
|
||||||
shell: pwsh
|
|
||||||
run: |
|
|
||||||
if (Test-Path dist) {
|
|
||||||
$dirs = Get-ChildItem -Path dist -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -like '*unpacked*' }
|
|
||||||
foreach ($d in $dirs) {
|
|
||||||
$zipBase = ($d.Name -replace 'unpacked', 'dir')
|
|
||||||
$zipPath = Join-Path dist "$zipBase.zip"
|
|
||||||
if (Test-Path $zipPath) { Remove-Item -Force $zipPath }
|
|
||||||
Compress-Archive -Path (Join-Path $d.FullName '*') -DestinationPath $zipPath -Force
|
|
||||||
Remove-Item -Recurse -Force $d.FullName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: dist-unpack
|
|
||||||
path: dist/**
|
|
||||||
|
|
||||||
release:
|
|
||||||
needs: [parse, build_win, build_mac, build_linux, build_unpack]
|
|
||||||
if: ${{ always() && needs.parse.outputs.tag != '' }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
path: artifacts
|
|
||||||
|
|
||||||
- name: 清理 unpacked 目录
|
|
||||||
run: |
|
|
||||||
if [ -d artifacts ]; then
|
|
||||||
find artifacts -type d -name '*unpacked*' -prune -exec rm -rf {} +
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: 发布 Release
|
|
||||||
uses: softprops/action-gh-release@v2
|
|
||||||
with:
|
|
||||||
tag_name: ${{ needs.parse.outputs.tag }}
|
|
||||||
name: SecScore ${{ needs.parse.outputs.tag }}
|
|
||||||
draft: True
|
|
||||||
prerelease: ${{ contains(needs.parse.outputs.version, '-') }}
|
|
||||||
files: |
|
|
||||||
artifacts/**/*.zip
|
|
||||||
artifacts/**/*.dmg
|
|
||||||
artifacts/**/*.AppImage
|
|
||||||
artifacts/**/*.deb
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
node_modules
|
|
||||||
dist
|
|
||||||
out
|
|
||||||
.DS_Store
|
|
||||||
.eslintcache
|
|
||||||
*.log*
|
|
||||||
logs
|
|
||||||
docs
|
|
||||||
configs
|
|
||||||
|
|
||||||
build/*
|
|
||||||
!build/entitlements.mac.plist
|
|
||||||
db.sqlite
|
|
||||||
*.local
|
|
||||||
.env.local
|
|
||||||
.env.*.local
|
|
||||||
.vscode
|
|
||||||
!.vscode/extensions.json
|
|
||||||
!.vscode/launch.json
|
|
||||||
!.vscode/settings.json
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
electron_mirror=https://npmmirror.com/mirrors/electron/
|
|
||||||
electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/
|
|
||||||
shamefully-hoist=true
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
out
|
|
||||||
dist
|
|
||||||
pnpm-lock.yaml
|
|
||||||
LICENSE.md
|
|
||||||
tsconfig.json
|
|
||||||
tsconfig.*.json
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
singleQuote: true
|
|
||||||
semi: false
|
|
||||||
printWidth: 100
|
|
||||||
trailingComma: none
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
---
|
|
||||||
alwaysApply: false
|
|
||||||
description: 任务完成前的检查
|
|
||||||
---
|
|
||||||
|
|
||||||
1.每次完成任务之前必须跑一遍测试(比如说npm系的typecheck/lint)
|
|
||||||
|
|
||||||
2.每次完成任务之前必须跑一遍格式化(比如说npm系的prettier)
|
|
||||||
|
|
||||||
3.任务完成时详细输出你所做的事情和需要用户确认的问题(如有)
|
|
||||||
|
|
||||||
4.每次规划任务必须按照步骤来,每个步骤结束后遵循第三条规定。
|
|
||||||
@@ -1,674 +0,0 @@
|
|||||||
GNU GENERAL PUBLIC LICENSE
|
|
||||||
Version 3, 29 June 2007
|
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
|
||||||
of this license document, but changing it is not allowed.
|
|
||||||
|
|
||||||
Preamble
|
|
||||||
|
|
||||||
The GNU General Public License is a free, copyleft license for
|
|
||||||
software and other kinds of works.
|
|
||||||
|
|
||||||
The licenses for most software and other practical works are designed
|
|
||||||
to take away your freedom to share and change the works. By contrast,
|
|
||||||
the GNU General Public License is intended to guarantee your freedom to
|
|
||||||
share and change all versions of a program--to make sure it remains free
|
|
||||||
software for all its users. We, the Free Software Foundation, use the
|
|
||||||
GNU General Public License for most of our software; it applies also to
|
|
||||||
any other work released this way by its authors. You can apply it to
|
|
||||||
your programs, too.
|
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom, not
|
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
|
||||||
have the freedom to distribute copies of free software (and charge for
|
|
||||||
them if you wish), that you receive source code or can get it if you
|
|
||||||
want it, that you can change the software or use pieces of it in new
|
|
||||||
free programs, and that you know you can do these things.
|
|
||||||
|
|
||||||
To protect your rights, we need to prevent others from denying you
|
|
||||||
these rights or asking you to surrender the rights. Therefore, you have
|
|
||||||
certain responsibilities if you distribute copies of the software, or if
|
|
||||||
you modify it: responsibilities to respect the freedom of others.
|
|
||||||
|
|
||||||
For example, if you distribute copies of such a program, whether
|
|
||||||
gratis or for a fee, you must pass on to the recipients the same
|
|
||||||
freedoms that you received. You must make sure that they, too, receive
|
|
||||||
or can get the source code. And you must show them these terms so they
|
|
||||||
know their rights.
|
|
||||||
|
|
||||||
Developers that use the GNU GPL protect your rights with two steps:
|
|
||||||
(1) assert copyright on the software, and (2) offer you this License
|
|
||||||
giving you legal permission to copy, distribute and/or modify it.
|
|
||||||
|
|
||||||
For the developers' and authors' protection, the GPL clearly explains
|
|
||||||
that there is no warranty for this free software. For both users' and
|
|
||||||
authors' sake, the GPL requires that modified versions be marked as
|
|
||||||
changed, so that their problems will not be attributed erroneously to
|
|
||||||
authors of previous versions.
|
|
||||||
|
|
||||||
Some devices are designed to deny users access to install or run
|
|
||||||
modified versions of the software inside them, although the manufacturer
|
|
||||||
can do so. This is fundamentally incompatible with the aim of
|
|
||||||
protecting users' freedom to change the software. The systematic
|
|
||||||
pattern of such abuse occurs in the area of products for individuals to
|
|
||||||
use, which is precisely where it is most unacceptable. Therefore, we
|
|
||||||
have designed this version of the GPL to prohibit the practice for those
|
|
||||||
products. If such problems arise substantially in other domains, we
|
|
||||||
stand ready to extend this provision to those domains in future versions
|
|
||||||
of the GPL, as needed to protect the freedom of users.
|
|
||||||
|
|
||||||
Finally, every program is threatened constantly by software patents.
|
|
||||||
States should not allow patents to restrict development and use of
|
|
||||||
software on general-purpose computers, but in those that do, we wish to
|
|
||||||
avoid the special danger that patents applied to a free program could
|
|
||||||
make it effectively proprietary. To prevent this, the GPL assures that
|
|
||||||
patents cannot be used to render the program non-free.
|
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
|
||||||
modification follow.
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
0. Definitions.
|
|
||||||
|
|
||||||
"This License" refers to version 3 of the GNU General Public License.
|
|
||||||
|
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
|
||||||
works, such as semiconductor masks.
|
|
||||||
|
|
||||||
"The Program" refers to any copyrightable work licensed under this
|
|
||||||
License. Each licensee is addressed as "you". "Licensees" and
|
|
||||||
"recipients" may be individuals or organizations.
|
|
||||||
|
|
||||||
To "modify" a work means to copy from or adapt all or part of the work
|
|
||||||
in a fashion requiring copyright permission, other than the making of an
|
|
||||||
exact copy. The resulting work is called a "modified version" of the
|
|
||||||
earlier work or a work "based on" the earlier work.
|
|
||||||
|
|
||||||
A "covered work" means either the unmodified Program or a work based
|
|
||||||
on the Program.
|
|
||||||
|
|
||||||
To "propagate" a work means to do anything with it that, without
|
|
||||||
permission, would make you directly or secondarily liable for
|
|
||||||
infringement under applicable copyright law, except executing it on a
|
|
||||||
computer or modifying a private copy. Propagation includes copying,
|
|
||||||
distribution (with or without modification), making available to the
|
|
||||||
public, and in some countries other activities as well.
|
|
||||||
|
|
||||||
To "convey" a work means any kind of propagation that enables other
|
|
||||||
parties to make or receive copies. Mere interaction with a user through
|
|
||||||
a computer network, with no transfer of a copy, is not conveying.
|
|
||||||
|
|
||||||
An interactive user interface displays "Appropriate Legal Notices"
|
|
||||||
to the extent that it includes a convenient and prominently visible
|
|
||||||
feature that (1) displays an appropriate copyright notice, and (2)
|
|
||||||
tells the user that there is no warranty for the work (except to the
|
|
||||||
extent that warranties are provided), that licensees may convey the
|
|
||||||
work under this License, and how to view a copy of this License. If
|
|
||||||
the interface presents a list of user commands or options, such as a
|
|
||||||
menu, a prominent item in the list meets this criterion.
|
|
||||||
|
|
||||||
1. Source Code.
|
|
||||||
|
|
||||||
The "source code" for a work means the preferred form of the work
|
|
||||||
for making modifications to it. "Object code" means any non-source
|
|
||||||
form of a work.
|
|
||||||
|
|
||||||
A "Standard Interface" means an interface that either is an official
|
|
||||||
standard defined by a recognized standards body, or, in the case of
|
|
||||||
interfaces specified for a particular programming language, one that
|
|
||||||
is widely used among developers working in that language.
|
|
||||||
|
|
||||||
The "System Libraries" of an executable work include anything, other
|
|
||||||
than the work as a whole, that (a) is included in the normal form of
|
|
||||||
packaging a Major Component, but which is not part of that Major
|
|
||||||
Component, and (b) serves only to enable use of the work with that
|
|
||||||
Major Component, or to implement a Standard Interface for which an
|
|
||||||
implementation is available to the public in source code form. A
|
|
||||||
"Major Component", in this context, means a major essential component
|
|
||||||
(kernel, window system, and so on) of the specific operating system
|
|
||||||
(if any) on which the executable work runs, or a compiler used to
|
|
||||||
produce the work, or an object code interpreter used to run it.
|
|
||||||
|
|
||||||
The "Corresponding Source" for a work in object code form means all
|
|
||||||
the source code needed to generate, install, and (for an executable
|
|
||||||
work) run the object code and to modify the work, including scripts to
|
|
||||||
control those activities. However, it does not include the work's
|
|
||||||
System Libraries, or general-purpose tools or generally available free
|
|
||||||
programs which are used unmodified in performing those activities but
|
|
||||||
which are not part of the work. For example, Corresponding Source
|
|
||||||
includes interface definition files associated with source files for
|
|
||||||
the work, and the source code for shared libraries and dynamically
|
|
||||||
linked subprograms that the work is specifically designed to require,
|
|
||||||
such as by intimate data communication or control flow between those
|
|
||||||
subprograms and other parts of the work.
|
|
||||||
|
|
||||||
The Corresponding Source need not include anything that users
|
|
||||||
can regenerate automatically from other parts of the Corresponding
|
|
||||||
Source.
|
|
||||||
|
|
||||||
The Corresponding Source for a work in source code form is that
|
|
||||||
same work.
|
|
||||||
|
|
||||||
2. Basic Permissions.
|
|
||||||
|
|
||||||
All rights granted under this License are granted for the term of
|
|
||||||
copyright on the Program, and are irrevocable provided the stated
|
|
||||||
conditions are met. This License explicitly affirms your unlimited
|
|
||||||
permission to run the unmodified Program. The output from running a
|
|
||||||
covered work is covered by this License only if the output, given its
|
|
||||||
content, constitutes a covered work. This License acknowledges your
|
|
||||||
rights of fair use or other equivalent, as provided by copyright law.
|
|
||||||
|
|
||||||
You may make, run and propagate covered works that you do not
|
|
||||||
convey, without conditions so long as your license otherwise remains
|
|
||||||
in force. You may convey covered works to others for the sole purpose
|
|
||||||
of having them make modifications exclusively for you, or provide you
|
|
||||||
with facilities for running those works, provided that you comply with
|
|
||||||
the terms of this License in conveying all material for which you do
|
|
||||||
not control copyright. Those thus making or running the covered works
|
|
||||||
for you must do so exclusively on your behalf, under your direction
|
|
||||||
and control, on terms that prohibit them from making any copies of
|
|
||||||
your copyrighted material outside their relationship with you.
|
|
||||||
|
|
||||||
Conveying under any other circumstances is permitted solely under
|
|
||||||
the conditions stated below. Sublicensing is not allowed; section 10
|
|
||||||
makes it unnecessary.
|
|
||||||
|
|
||||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
|
||||||
|
|
||||||
No covered work shall be deemed part of an effective technological
|
|
||||||
measure under any applicable law fulfilling obligations under article
|
|
||||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
|
||||||
similar laws prohibiting or restricting circumvention of such
|
|
||||||
measures.
|
|
||||||
|
|
||||||
When you convey a covered work, you waive any legal power to forbid
|
|
||||||
circumvention of technological measures to the extent such circumvention
|
|
||||||
is effected by exercising rights under this License with respect to
|
|
||||||
the covered work, and you disclaim any intention to limit operation or
|
|
||||||
modification of the work as a means of enforcing, against the work's
|
|
||||||
users, your or third parties' legal rights to forbid circumvention of
|
|
||||||
technological measures.
|
|
||||||
|
|
||||||
4. Conveying Verbatim Copies.
|
|
||||||
|
|
||||||
You may convey verbatim copies of the Program's source code as you
|
|
||||||
receive it, in any medium, provided that you conspicuously and
|
|
||||||
appropriately publish on each copy an appropriate copyright notice;
|
|
||||||
keep intact all notices stating that this License and any
|
|
||||||
non-permissive terms added in accord with section 7 apply to the code;
|
|
||||||
keep intact all notices of the absence of any warranty; and give all
|
|
||||||
recipients a copy of this License along with the Program.
|
|
||||||
|
|
||||||
You may charge any price or no price for each copy that you convey,
|
|
||||||
and you may offer support or warranty protection for a fee.
|
|
||||||
|
|
||||||
5. Conveying Modified Source Versions.
|
|
||||||
|
|
||||||
You may convey a work based on the Program, or the modifications to
|
|
||||||
produce it from the Program, in the form of source code under the
|
|
||||||
terms of section 4, provided that you also meet all of these conditions:
|
|
||||||
|
|
||||||
a) The work must carry prominent notices stating that you modified
|
|
||||||
it, and giving a relevant date.
|
|
||||||
|
|
||||||
b) The work must carry prominent notices stating that it is
|
|
||||||
released under this License and any conditions added under section
|
|
||||||
7. This requirement modifies the requirement in section 4 to
|
|
||||||
"keep intact all notices".
|
|
||||||
|
|
||||||
c) You must license the entire work, as a whole, under this
|
|
||||||
License to anyone who comes into possession of a copy. This
|
|
||||||
License will therefore apply, along with any applicable section 7
|
|
||||||
additional terms, to the whole of the work, and all its parts,
|
|
||||||
regardless of how they are packaged. This License gives no
|
|
||||||
permission to license the work in any other way, but it does not
|
|
||||||
invalidate such permission if you have separately received it.
|
|
||||||
|
|
||||||
d) If the work has interactive user interfaces, each must display
|
|
||||||
Appropriate Legal Notices; however, if the Program has interactive
|
|
||||||
interfaces that do not display Appropriate Legal Notices, your
|
|
||||||
work need not make them do so.
|
|
||||||
|
|
||||||
A compilation of a covered work with other separate and independent
|
|
||||||
works, which are not by their nature extensions of the covered work,
|
|
||||||
and which are not combined with it such as to form a larger program,
|
|
||||||
in or on a volume of a storage or distribution medium, is called an
|
|
||||||
"aggregate" if the compilation and its resulting copyright are not
|
|
||||||
used to limit the access or legal rights of the compilation's users
|
|
||||||
beyond what the individual works permit. Inclusion of a covered work
|
|
||||||
in an aggregate does not cause this License to apply to the other
|
|
||||||
parts of the aggregate.
|
|
||||||
|
|
||||||
6. Conveying Non-Source Forms.
|
|
||||||
|
|
||||||
You may convey a covered work in object code form under the terms
|
|
||||||
of sections 4 and 5, provided that you also convey the
|
|
||||||
machine-readable Corresponding Source under the terms of this License,
|
|
||||||
in one of these ways:
|
|
||||||
|
|
||||||
a) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by the
|
|
||||||
Corresponding Source fixed on a durable physical medium
|
|
||||||
customarily used for software interchange.
|
|
||||||
|
|
||||||
b) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by a
|
|
||||||
written offer, valid for at least three years and valid for as
|
|
||||||
long as you offer spare parts or customer support for that product
|
|
||||||
model, to give anyone who possesses the object code either (1) a
|
|
||||||
copy of the Corresponding Source for all the software in the
|
|
||||||
product that is covered by this License, on a durable physical
|
|
||||||
medium customarily used for software interchange, for a price no
|
|
||||||
more than your reasonable cost of physically performing this
|
|
||||||
conveying of source, or (2) access to copy the
|
|
||||||
Corresponding Source from a network server at no charge.
|
|
||||||
|
|
||||||
c) Convey individual copies of the object code with a copy of the
|
|
||||||
written offer to provide the Corresponding Source. This
|
|
||||||
alternative is allowed only occasionally and noncommercially, and
|
|
||||||
only if you received the object code with such an offer, in accord
|
|
||||||
with subsection 6b.
|
|
||||||
|
|
||||||
d) Convey the object code by offering access from a designated
|
|
||||||
place (gratis or for a charge), and offer equivalent access to the
|
|
||||||
Corresponding Source in the same way through the same place at no
|
|
||||||
further charge. You need not require recipients to copy the
|
|
||||||
Corresponding Source along with the object code. If the place to
|
|
||||||
copy the object code is a network server, the Corresponding Source
|
|
||||||
may be on a different server (operated by you or a third party)
|
|
||||||
that supports equivalent copying facilities, provided you maintain
|
|
||||||
clear directions next to the object code saying where to find the
|
|
||||||
Corresponding Source. Regardless of what server hosts the
|
|
||||||
Corresponding Source, you remain obligated to ensure that it is
|
|
||||||
available for as long as needed to satisfy these requirements.
|
|
||||||
|
|
||||||
e) Convey the object code using peer-to-peer transmission, provided
|
|
||||||
you inform other peers where the object code and Corresponding
|
|
||||||
Source of the work are being offered to the general public at no
|
|
||||||
charge under subsection 6d.
|
|
||||||
|
|
||||||
A separable portion of the object code, whose source code is excluded
|
|
||||||
from the Corresponding Source as a System Library, need not be
|
|
||||||
included in conveying the object code work.
|
|
||||||
|
|
||||||
A "User Product" is either (1) a "consumer product", which means any
|
|
||||||
tangible personal property which is normally used for personal, family,
|
|
||||||
or household purposes, or (2) anything designed or sold for incorporation
|
|
||||||
into a dwelling. In determining whether a product is a consumer product,
|
|
||||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
|
||||||
product received by a particular user, "normally used" refers to a
|
|
||||||
typical or common use of that class of product, regardless of the status
|
|
||||||
of the particular user or of the way in which the particular user
|
|
||||||
actually uses, or expects or is expected to use, the product. A product
|
|
||||||
is a consumer product regardless of whether the product has substantial
|
|
||||||
commercial, industrial or non-consumer uses, unless such uses represent
|
|
||||||
the only significant mode of use of the product.
|
|
||||||
|
|
||||||
"Installation Information" for a User Product means any methods,
|
|
||||||
procedures, authorization keys, or other information required to install
|
|
||||||
and execute modified versions of a covered work in that User Product from
|
|
||||||
a modified version of its Corresponding Source. The information must
|
|
||||||
suffice to ensure that the continued functioning of the modified object
|
|
||||||
code is in no case prevented or interfered with solely because
|
|
||||||
modification has been made.
|
|
||||||
|
|
||||||
If you convey an object code work under this section in, or with, or
|
|
||||||
specifically for use in, a User Product, and the conveying occurs as
|
|
||||||
part of a transaction in which the right of possession and use of the
|
|
||||||
User Product is transferred to the recipient in perpetuity or for a
|
|
||||||
fixed term (regardless of how the transaction is characterized), the
|
|
||||||
Corresponding Source conveyed under this section must be accompanied
|
|
||||||
by the Installation Information. But this requirement does not apply
|
|
||||||
if neither you nor any third party retains the ability to install
|
|
||||||
modified object code on the User Product (for example, the work has
|
|
||||||
been installed in ROM).
|
|
||||||
|
|
||||||
The requirement to provide Installation Information does not include a
|
|
||||||
requirement to continue to provide support service, warranty, or updates
|
|
||||||
for a work that has been modified or installed by the recipient, or for
|
|
||||||
the User Product in which it has been modified or installed. Access to a
|
|
||||||
network may be denied when the modification itself materially and
|
|
||||||
adversely affects the operation of the network or violates the rules and
|
|
||||||
protocols for communication across the network.
|
|
||||||
|
|
||||||
Corresponding Source conveyed, and Installation Information provided,
|
|
||||||
in accord with this section must be in a format that is publicly
|
|
||||||
documented (and with an implementation available to the public in
|
|
||||||
source code form), and must require no special password or key for
|
|
||||||
unpacking, reading or copying.
|
|
||||||
|
|
||||||
7. Additional Terms.
|
|
||||||
|
|
||||||
"Additional permissions" are terms that supplement the terms of this
|
|
||||||
License by making exceptions from one or more of its conditions.
|
|
||||||
Additional permissions that are applicable to the entire Program shall
|
|
||||||
be treated as though they were included in this License, to the extent
|
|
||||||
that they are valid under applicable law. If additional permissions
|
|
||||||
apply only to part of the Program, that part may be used separately
|
|
||||||
under those permissions, but the entire Program remains governed by
|
|
||||||
this License without regard to the additional permissions.
|
|
||||||
|
|
||||||
When you convey a copy of a covered work, you may at your option
|
|
||||||
remove any additional permissions from that copy, or from any part of
|
|
||||||
it. (Additional permissions may be written to require their own
|
|
||||||
removal in certain cases when you modify the work.) You may place
|
|
||||||
additional permissions on material, added by you to a covered work,
|
|
||||||
for which you have or can give appropriate copyright permission.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, for material you
|
|
||||||
add to a covered work, you may (if authorized by the copyright holders of
|
|
||||||
that material) supplement the terms of this License with terms:
|
|
||||||
|
|
||||||
a) Disclaiming warranty or limiting liability differently from the
|
|
||||||
terms of sections 15 and 16 of this License; or
|
|
||||||
|
|
||||||
b) Requiring preservation of specified reasonable legal notices or
|
|
||||||
author attributions in that material or in the Appropriate Legal
|
|
||||||
Notices displayed by works containing it; or
|
|
||||||
|
|
||||||
c) Prohibiting misrepresentation of the origin of that material, or
|
|
||||||
requiring that modified versions of such material be marked in
|
|
||||||
reasonable ways as different from the original version; or
|
|
||||||
|
|
||||||
d) Limiting the use for publicity purposes of names of licensors or
|
|
||||||
authors of the material; or
|
|
||||||
|
|
||||||
e) Declining to grant rights under trademark law for use of some
|
|
||||||
trade names, trademarks, or service marks; or
|
|
||||||
|
|
||||||
f) Requiring indemnification of licensors and authors of that
|
|
||||||
material by anyone who conveys the material (or modified versions of
|
|
||||||
it) with contractual assumptions of liability to the recipient, for
|
|
||||||
any liability that these contractual assumptions directly impose on
|
|
||||||
those licensors and authors.
|
|
||||||
|
|
||||||
All other non-permissive additional terms are considered "further
|
|
||||||
restrictions" within the meaning of section 10. If the Program as you
|
|
||||||
received it, or any part of it, contains a notice stating that it is
|
|
||||||
governed by this License along with a term that is a further
|
|
||||||
restriction, you may remove that term. If a license document contains
|
|
||||||
a further restriction but permits relicensing or conveying under this
|
|
||||||
License, you may add to a covered work material governed by the terms
|
|
||||||
of that license document, provided that the further restriction does
|
|
||||||
not survive such relicensing or conveying.
|
|
||||||
|
|
||||||
If you add terms to a covered work in accord with this section, you
|
|
||||||
must place, in the relevant source files, a statement of the
|
|
||||||
additional terms that apply to those files, or a notice indicating
|
|
||||||
where to find the applicable terms.
|
|
||||||
|
|
||||||
Additional terms, permissive or non-permissive, may be stated in the
|
|
||||||
form of a separately written license, or stated as exceptions;
|
|
||||||
the above requirements apply either way.
|
|
||||||
|
|
||||||
8. Termination.
|
|
||||||
|
|
||||||
You may not propagate or modify a covered work except as expressly
|
|
||||||
provided under this License. Any attempt otherwise to propagate or
|
|
||||||
modify it is void, and will automatically terminate your rights under
|
|
||||||
this License (including any patent licenses granted under the third
|
|
||||||
paragraph of section 11).
|
|
||||||
|
|
||||||
However, if you cease all violation of this License, then your
|
|
||||||
license from a particular copyright holder is reinstated (a)
|
|
||||||
provisionally, unless and until the copyright holder explicitly and
|
|
||||||
finally terminates your license, and (b) permanently, if the copyright
|
|
||||||
holder fails to notify you of the violation by some reasonable means
|
|
||||||
prior to 60 days after the cessation.
|
|
||||||
|
|
||||||
Moreover, your license from a particular copyright holder is
|
|
||||||
reinstated permanently if the copyright holder notifies you of the
|
|
||||||
violation by some reasonable means, this is the first time you have
|
|
||||||
received notice of violation of this License (for any work) from that
|
|
||||||
copyright holder, and you cure the violation prior to 30 days after
|
|
||||||
your receipt of the notice.
|
|
||||||
|
|
||||||
Termination of your rights under this section does not terminate the
|
|
||||||
licenses of parties who have received copies or rights from you under
|
|
||||||
this License. If your rights have been terminated and not permanently
|
|
||||||
reinstated, you do not qualify to receive new licenses for the same
|
|
||||||
material under section 10.
|
|
||||||
|
|
||||||
9. Acceptance Not Required for Having Copies.
|
|
||||||
|
|
||||||
You are not required to accept this License in order to receive or
|
|
||||||
run a copy of the Program. Ancillary propagation of a covered work
|
|
||||||
occurring solely as a consequence of using peer-to-peer transmission
|
|
||||||
to receive a copy likewise does not require acceptance. However,
|
|
||||||
nothing other than this License grants you permission to propagate or
|
|
||||||
modify any covered work. These actions infringe copyright if you do
|
|
||||||
not accept this License. Therefore, by modifying or propagating a
|
|
||||||
covered work, you indicate your acceptance of this License to do so.
|
|
||||||
|
|
||||||
10. Automatic Licensing of Downstream Recipients.
|
|
||||||
|
|
||||||
Each time you convey a covered work, the recipient automatically
|
|
||||||
receives a license from the original licensors, to run, modify and
|
|
||||||
propagate that work, subject to this License. You are not responsible
|
|
||||||
for enforcing compliance by third parties with this License.
|
|
||||||
|
|
||||||
An "entity transaction" is a transaction transferring control of an
|
|
||||||
organization, or substantially all assets of one, or subdividing an
|
|
||||||
organization, or merging organizations. If propagation of a covered
|
|
||||||
work results from an entity transaction, each party to that
|
|
||||||
transaction who receives a copy of the work also receives whatever
|
|
||||||
licenses to the work the party's predecessor in interest had or could
|
|
||||||
give under the previous paragraph, plus a right to possession of the
|
|
||||||
Corresponding Source of the work from the predecessor in interest, if
|
|
||||||
the predecessor has it or can get it with reasonable efforts.
|
|
||||||
|
|
||||||
You may not impose any further restrictions on the exercise of the
|
|
||||||
rights granted or affirmed under this License. For example, you may
|
|
||||||
not impose a license fee, royalty, or other charge for exercise of
|
|
||||||
rights granted under this License, and you may not initiate litigation
|
|
||||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
|
||||||
any patent claim is infringed by making, using, selling, offering for
|
|
||||||
sale, or importing the Program or any portion of it.
|
|
||||||
|
|
||||||
11. Patents.
|
|
||||||
|
|
||||||
A "contributor" is a copyright holder who authorizes use under this
|
|
||||||
License of the Program or a work on which the Program is based. The
|
|
||||||
work thus licensed is called the contributor's "contributor version".
|
|
||||||
|
|
||||||
A contributor's "essential patent claims" are all patent claims
|
|
||||||
owned or controlled by the contributor, whether already acquired or
|
|
||||||
hereafter acquired, that would be infringed by some manner, permitted
|
|
||||||
by this License, of making, using, or selling its contributor version,
|
|
||||||
but do not include claims that would be infringed only as a
|
|
||||||
consequence of further modification of the contributor version. For
|
|
||||||
purposes of this definition, "control" includes the right to grant
|
|
||||||
patent sublicenses in a manner consistent with the requirements of
|
|
||||||
this License.
|
|
||||||
|
|
||||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
|
||||||
patent license under the contributor's essential patent claims, to
|
|
||||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
|
||||||
propagate the contents of its contributor version.
|
|
||||||
|
|
||||||
In the following three paragraphs, a "patent license" is any express
|
|
||||||
agreement or commitment, however denominated, not to enforce a patent
|
|
||||||
(such as an express permission to practice a patent or covenant not to
|
|
||||||
sue for patent infringement). To "grant" such a patent license to a
|
|
||||||
party means to make such an agreement or commitment not to enforce a
|
|
||||||
patent against the party.
|
|
||||||
|
|
||||||
If you convey a covered work, knowingly relying on a patent license,
|
|
||||||
and the Corresponding Source of the work is not available for anyone
|
|
||||||
to copy, free of charge and under the terms of this License, through a
|
|
||||||
publicly available network server or other readily accessible means,
|
|
||||||
then you must either (1) cause the Corresponding Source to be so
|
|
||||||
available, or (2) arrange to deprive yourself of the benefit of the
|
|
||||||
patent license for this particular work, or (3) arrange, in a manner
|
|
||||||
consistent with the requirements of this License, to extend the patent
|
|
||||||
license to downstream recipients. "Knowingly relying" means you have
|
|
||||||
actual knowledge that, but for the patent license, your conveying the
|
|
||||||
covered work in a country, or your recipient's use of the covered work
|
|
||||||
in a country, would infringe one or more identifiable patents in that
|
|
||||||
country that you have reason to believe are valid.
|
|
||||||
|
|
||||||
If, pursuant to or in connection with a single transaction or
|
|
||||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
|
||||||
covered work, and grant a patent license to some of the parties
|
|
||||||
receiving the covered work authorizing them to use, propagate, modify
|
|
||||||
or convey a specific copy of the covered work, then the patent license
|
|
||||||
you grant is automatically extended to all recipients of the covered
|
|
||||||
work and works based on it.
|
|
||||||
|
|
||||||
A patent license is "discriminatory" if it does not include within
|
|
||||||
the scope of its coverage, prohibits the exercise of, or is
|
|
||||||
conditioned on the non-exercise of one or more of the rights that are
|
|
||||||
specifically granted under this License. You may not convey a covered
|
|
||||||
work if you are a party to an arrangement with a third party that is
|
|
||||||
in the business of distributing software, under which you make payment
|
|
||||||
to the third party based on the extent of your activity of conveying
|
|
||||||
the work, and under which the third party grants, to any of the
|
|
||||||
parties who would receive the covered work from you, a discriminatory
|
|
||||||
patent license (a) in connection with copies of the covered work
|
|
||||||
conveyed by you (or copies made from those copies), or (b) primarily
|
|
||||||
for and in connection with specific products or compilations that
|
|
||||||
contain the covered work, unless you entered into that arrangement,
|
|
||||||
or that patent license was granted, prior to 28 March 2007.
|
|
||||||
|
|
||||||
Nothing in this License shall be construed as excluding or limiting
|
|
||||||
any implied license or other defenses to infringement that may
|
|
||||||
otherwise be available to you under applicable patent law.
|
|
||||||
|
|
||||||
12. No Surrender of Others' Freedom.
|
|
||||||
|
|
||||||
If conditions are imposed on you (whether by court order, agreement or
|
|
||||||
otherwise) that contradict the conditions of this License, they do not
|
|
||||||
excuse you from the conditions of this License. If you cannot convey a
|
|
||||||
covered work so as to satisfy simultaneously your obligations under this
|
|
||||||
License and any other pertinent obligations, then as a consequence you may
|
|
||||||
not convey it at all. For example, if you agree to terms that obligate you
|
|
||||||
to collect a royalty for further conveying from those to whom you convey
|
|
||||||
the Program, the only way you could satisfy both those terms and this
|
|
||||||
License would be to refrain entirely from conveying the Program.
|
|
||||||
|
|
||||||
13. Use with the GNU Affero General Public License.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, you have
|
|
||||||
permission to link or combine any covered work with a work licensed
|
|
||||||
under version 3 of the GNU Affero General Public License into a single
|
|
||||||
combined work, and to convey the resulting work. The terms of this
|
|
||||||
License will continue to apply to the part which is the covered work,
|
|
||||||
but the special requirements of the GNU Affero General Public License,
|
|
||||||
section 13, concerning interaction through a network will apply to the
|
|
||||||
combination as such.
|
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of
|
|
||||||
the GNU General Public License from time to time. Such new versions will
|
|
||||||
be similar in spirit to the present version, but may differ in detail to
|
|
||||||
address new problems or concerns.
|
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the
|
|
||||||
Program specifies that a certain numbered version of the GNU General
|
|
||||||
Public License "or any later version" applies to it, you have the
|
|
||||||
option of following the terms and conditions either of that numbered
|
|
||||||
version or of any later version published by the Free Software
|
|
||||||
Foundation. If the Program does not specify a version number of the
|
|
||||||
GNU General Public License, you may choose any version ever published
|
|
||||||
by the Free Software Foundation.
|
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future
|
|
||||||
versions of the GNU General Public License can be used, that proxy's
|
|
||||||
public statement of acceptance of a version permanently authorizes you
|
|
||||||
to choose that version for the Program.
|
|
||||||
|
|
||||||
Later license versions may give you additional or different
|
|
||||||
permissions. However, no additional obligations are imposed on any
|
|
||||||
author or copyright holder as a result of your choosing to follow a
|
|
||||||
later version.
|
|
||||||
|
|
||||||
15. Disclaimer of Warranty.
|
|
||||||
|
|
||||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
|
||||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
|
||||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
|
||||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
|
||||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
||||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
|
||||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
|
||||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
|
||||||
|
|
||||||
16. Limitation of Liability.
|
|
||||||
|
|
||||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
|
||||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
|
||||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
|
||||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
|
||||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
|
||||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
|
||||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
|
||||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
|
||||||
SUCH DAMAGES.
|
|
||||||
|
|
||||||
17. Interpretation of Sections 15 and 16.
|
|
||||||
|
|
||||||
If the disclaimer of warranty and limitation of liability provided
|
|
||||||
above cannot be given local legal effect according to their terms,
|
|
||||||
reviewing courts shall apply local law that most closely approximates
|
|
||||||
an absolute waiver of all civil liability in connection with the
|
|
||||||
Program, unless a warranty or assumption of liability accompanies a
|
|
||||||
copy of the Program in return for a fee.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
How to Apply These Terms to Your New Programs
|
|
||||||
|
|
||||||
If you develop a new program, and you want it to be of the greatest
|
|
||||||
possible use to the public, the best way to achieve this is to make it
|
|
||||||
free software which everyone can redistribute and change under these terms.
|
|
||||||
|
|
||||||
To do so, attach the following notices to the program. It is safest
|
|
||||||
to attach them to the start of each source file to most effectively
|
|
||||||
state the exclusion of warranty; and each file should have at least
|
|
||||||
the "copyright" line and a pointer to where the full notice is found.
|
|
||||||
|
|
||||||
<one line to give the program's name and a brief idea of what it does.>
|
|
||||||
Copyright (C) <year> <name of author>
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
|
||||||
|
|
||||||
If the program does terminal interaction, make it output a short
|
|
||||||
notice like this when it starts in an interactive mode:
|
|
||||||
|
|
||||||
<program> Copyright (C) <year> <name of author>
|
|
||||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
|
||||||
This is free software, and you are welcome to redistribute it
|
|
||||||
under certain conditions; type `show c' for details.
|
|
||||||
|
|
||||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
|
||||||
parts of the General Public License. Of course, your program's commands
|
|
||||||
might be different; for a GUI interface, you would use an "about box".
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
|
||||||
For more information on this, and how to apply and follow the GNU GPL, see
|
|
||||||
<https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
The GNU General Public License does not permit incorporating your program
|
|
||||||
into proprietary programs. If your program is a subroutine library, you
|
|
||||||
may consider it more useful to permit linking proprietary applications with
|
|
||||||
the library. If this is what you want to do, use the GNU Lesser General
|
|
||||||
Public License instead of this License. But first, please read
|
|
||||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
# SecScore
|
|
||||||
|
|
||||||
<!-- GitHub 徽章 -->
|
|
||||||
<p align="center">
|
|
||||||
<a href="https://github.com/SECTL/SecScore/releases"><img alt="GitHub release (latest by date)" src="https://img.shields.io/github/v/release/SECTL/SecScore?style=flat-square"></a>
|
|
||||||
<a href="https://github.com/SECTL/SecScore/releases"><img alt="Downloads" src="https://img.shields.io/github/downloads/SECTL/SecScore/total?style=flat-square"></a>
|
|
||||||
<a href="https://github.com/SECTL/SecScore/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/github/license/SECTL/SecScore?style=flat-square"></a>
|
|
||||||
<a href="https://github.com/SECTL/SecScore/issues"><img alt="GitHub issues" src="https://img.shields.io/github/issues/SECTL/SecScore?style=flat-square"></a>
|
|
||||||
<a href="https://github.com/SECTL/SecScore/pulls"><img alt="GitHub pull requests" src="https://img.shields.io/github/issues-pr/SECTL/SecScore?style=flat-square"></a>
|
|
||||||
<img alt="Electron" src="https://img.shields.io/badge/Electron-39+-47848F?style=flat-square&logo=electron">
|
|
||||||
<img alt="React" src="https://img.shields.io/badge/React-19+-61DAFB?style=flat-square&logo=react">
|
|
||||||
<img alt="TypeScript" src="https://img.shields.io/badge/TypeScript-5+-3178C6?style=flat-square&logo=typescript">
|
|
||||||
<img alt="Vite" src="https://img.shields.io/badge/Vite-7+-646CFF?style=flat-square&logo=vite">
|
|
||||||
</p>
|
|
||||||
|
|
||||||
SecScore 是一款教育积分管理软件,基于 Rust + Tauri 开发,用于管理学生名单、记录加/扣分、查看排行榜与结算历史,并提供权限保护与数据备份。
|
|
||||||
|
|
||||||
## 主要功能
|
|
||||||
|
|
||||||
- 学生管理
|
|
||||||
- 添加/删除学生
|
|
||||||
- 通过 xlsx 批量导入名单(支持导入前预览与选择“姓名列”)
|
|
||||||
- 积分管理
|
|
||||||
- 选择学生并提交加分/扣分
|
|
||||||
- 支持“预设理由”一键填充理由与分值
|
|
||||||
- 支持撤销最近的积分记录(撤销后学生积分会回滚)
|
|
||||||
- 理由管理
|
|
||||||
- 维护“预设理由”(分类、理由内容、预设分值)
|
|
||||||
- 排行榜
|
|
||||||
- 支持按“今天 / 本周 / 本月”查看积分变化
|
|
||||||
- 支持导出排行榜为 XLSX
|
|
||||||
- 支持查看单个学生的操作记录(文本列表)
|
|
||||||
- 结算与历史
|
|
||||||
- “结算并重新开始”:把当前未结算记录归档为一个阶段,并将所有学生当前积分清零
|
|
||||||
- 在“结算历史”查看每个阶段的排行榜
|
|
||||||
- 系统设置
|
|
||||||
- 主题切换
|
|
||||||
- 日志查看/导出/清空
|
|
||||||
- 数据导入/导出(JSON)
|
|
||||||
- 密码保护(管理密码 / 积分密码)与找回字符串
|
|
||||||
|
|
||||||
## 使用方法
|
|
||||||
|
|
||||||
### 1. 权限与解锁
|
|
||||||
|
|
||||||
- 右上角会显示当前权限:管理权限 / 积分权限 / 只读
|
|
||||||
- 若设置了密码,可通过右上角“输入密码”进行解锁
|
|
||||||
- 管理密码:全功能(学生管理、理由管理、结算、数据管理等)
|
|
||||||
- 积分密码:仅允许积分相关操作
|
|
||||||
- 点击“锁定”可切回只读状态
|
|
||||||
- 无密码时默认视为管理权限
|
|
||||||
|
|
||||||
### 2. 学生管理(导入名单)
|
|
||||||
|
|
||||||
入口:左侧菜单 → 学生管理 → 导入名单
|
|
||||||
|
|
||||||
- 通过 xlsx 导入
|
|
||||||
1. 选择一个 `.xlsx` 文件(默认读取第一个工作表)
|
|
||||||
2. 在预览表格里点击表头,选择“姓名列”
|
|
||||||
3. 点击“导入”
|
|
||||||
4. 导入完成后会提示“新增 / 跳过”数量
|
|
||||||
|
|
||||||
建议:姓名列尽量只包含纯姓名文本;同名学生会被视为重复并跳过。
|
|
||||||
|
|
||||||
### 3. 积分管理(加分/扣分)
|
|
||||||
|
|
||||||
入口:左侧菜单 → 积分管理
|
|
||||||
|
|
||||||
1. 在“姓名”选择一个学生(支持搜索)
|
|
||||||
2. 选择“加分/扣分”,并输入分值
|
|
||||||
3. 在“理由内容”填写原因(可手动输入)
|
|
||||||
4. 点击“确认提交”
|
|
||||||
|
|
||||||
快捷理由:
|
|
||||||
|
|
||||||
- 可在“快捷理由”下拉框中选择预设理由,一键填充理由内容/分值(优先尊重你当前是否已手动输入分值)
|
|
||||||
|
|
||||||
撤销最近记录:
|
|
||||||
|
|
||||||
- “最近记录”默认折叠,展开后可对记录点击“撤销”
|
|
||||||
- 撤销会回滚该条记录对学生积分的影响
|
|
||||||
|
|
||||||
### 4. 排行榜与导出
|
|
||||||
|
|
||||||
入口:左侧菜单 → 排行榜
|
|
||||||
|
|
||||||
- 可切换统计范围:今天 / 本周 / 本月
|
|
||||||
- 点击“导出 XLSX”可导出当前排行榜
|
|
||||||
- 点击某个学生的“查看”可打开该学生的操作记录
|
|
||||||
|
|
||||||
### 5. 结算与数据备份
|
|
||||||
|
|
||||||
入口:左侧菜单 → 系统设置 → 数据管理
|
|
||||||
|
|
||||||
- 结算并重新开始
|
|
||||||
- 会把当前未结算的积分记录归档为一个阶段
|
|
||||||
- 会将所有学生当前积分清零
|
|
||||||
- 学生名单不变;结算后的历史在“结算历史”查看
|
|
||||||
- 导出 JSON(强烈建议定期备份)
|
|
||||||
- 导入会覆盖现有学生/理由/积分记录/设置
|
|
||||||
- 安全相关设置(密码等)不会随导入写入
|
|
||||||
|
|
||||||
## 开发与运行(面向贡献者)
|
|
||||||
|
|
||||||
### 环境要求
|
|
||||||
|
|
||||||
- Node.js(建议使用 LTS 版本)
|
|
||||||
- pnpm
|
|
||||||
|
|
||||||
### 安装依赖
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm install
|
|
||||||
```
|
|
||||||
|
|
||||||
### 开发模式
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### 构建
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm build
|
|
||||||
```
|
|
||||||
|
|
||||||
(可选)打包:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm build:win
|
|
||||||
pnpm build:unpack
|
|
||||||
```
|
|
||||||
|
|
||||||
### 常用检查
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm lint
|
|
||||||
pnpm typecheck
|
|
||||||
```
|
|
||||||
@@ -1,229 +0,0 @@
|
|||||||
# SecScore URL 协议说明
|
|
||||||
|
|
||||||
## 1. 概述
|
|
||||||
|
|
||||||
SecScore 提供自定义 URL 协议,用于从浏览器、命令行或其他应用中直接调用软件功能。
|
|
||||||
|
|
||||||
- 协议名(scheme):`secscore`
|
|
||||||
- URL 基本格式:
|
|
||||||
|
|
||||||
```text
|
|
||||||
secscore://<command>[/<subcommand>]
|
|
||||||
```
|
|
||||||
|
|
||||||
- 大小写约定:
|
|
||||||
- 协议名、命令部分大小写不敏感(`SecScore://settings` 与 `secscore://SETTINGS` 等效)
|
|
||||||
- 建议实际使用中统一采用小写
|
|
||||||
|
|
||||||
- 行为约定:
|
|
||||||
- 若 SecScore **未运行**:启动应用,并根据 URL 执行对应操作
|
|
||||||
- 若 SecScore **已运行**:不会启动第二个实例,而是把 URL 转发给现有实例执行
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 页面导航类 URL
|
|
||||||
|
|
||||||
用于打开主窗口并跳转到指定功能页面。
|
|
||||||
|
|
||||||
### 2.1 首页
|
|
||||||
|
|
||||||
- URL:
|
|
||||||
|
|
||||||
```text
|
|
||||||
secscore://
|
|
||||||
secscore://home
|
|
||||||
```
|
|
||||||
|
|
||||||
- 行为:
|
|
||||||
- 打开主窗口,跳转到 `/`(首页)
|
|
||||||
|
|
||||||
### 2.2 学生管理
|
|
||||||
|
|
||||||
- URL:
|
|
||||||
|
|
||||||
```text
|
|
||||||
secscore://students
|
|
||||||
```
|
|
||||||
|
|
||||||
- 行为:
|
|
||||||
- 打开主窗口,跳转到 `/students`
|
|
||||||
|
|
||||||
### 2.3 积分管理
|
|
||||||
|
|
||||||
- URL:
|
|
||||||
|
|
||||||
```text
|
|
||||||
secscore://score
|
|
||||||
```
|
|
||||||
|
|
||||||
- 行为:
|
|
||||||
- 打开主窗口,跳转到 `/score`
|
|
||||||
|
|
||||||
### 2.4 排行榜
|
|
||||||
|
|
||||||
- URL:
|
|
||||||
|
|
||||||
```text
|
|
||||||
secscore://leaderboard
|
|
||||||
```
|
|
||||||
|
|
||||||
- 行为:
|
|
||||||
- 打开主窗口,跳转到 `/leaderboard`
|
|
||||||
|
|
||||||
### 2.5 结算历史
|
|
||||||
|
|
||||||
- URL:
|
|
||||||
|
|
||||||
```text
|
|
||||||
secscore://settlements
|
|
||||||
```
|
|
||||||
|
|
||||||
- 行为:
|
|
||||||
- 打开主窗口,跳转到 `/settlements`
|
|
||||||
|
|
||||||
### 2.6 理由管理
|
|
||||||
|
|
||||||
- URL:
|
|
||||||
|
|
||||||
```text
|
|
||||||
secscore://reasons
|
|
||||||
```
|
|
||||||
|
|
||||||
- 行为:
|
|
||||||
- 打开主窗口,跳转到 `/reasons`
|
|
||||||
|
|
||||||
### 2.7 系统设置
|
|
||||||
|
|
||||||
- URL:
|
|
||||||
|
|
||||||
```text
|
|
||||||
secscore://settings
|
|
||||||
```
|
|
||||||
|
|
||||||
- 行为:
|
|
||||||
- 打开主窗口,跳转到 `/settings`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 悬浮侧边栏控制类 URL
|
|
||||||
|
|
||||||
用于控制右侧的全局悬浮侧边栏(`global-sidebar` 窗口)。
|
|
||||||
|
|
||||||
### 3.1 显示悬浮侧边栏
|
|
||||||
|
|
||||||
- URL:
|
|
||||||
|
|
||||||
```text
|
|
||||||
secscore://sidebar/show
|
|
||||||
```
|
|
||||||
|
|
||||||
- 行为:
|
|
||||||
- 若悬浮侧边栏已存在:`show()` + `focus()`
|
|
||||||
- 若不存在:创建 `global-sidebar` 窗口并显示
|
|
||||||
|
|
||||||
### 3.2 隐藏悬浮侧边栏
|
|
||||||
|
|
||||||
- URL:
|
|
||||||
|
|
||||||
```text
|
|
||||||
secscore://sidebar/hide
|
|
||||||
```
|
|
||||||
|
|
||||||
- 行为:
|
|
||||||
- 若悬浮侧边栏存在:调用 `hide()`
|
|
||||||
- 若不存在:不做任何操作
|
|
||||||
|
|
||||||
### 3.3 切换显隐(toggle)
|
|
||||||
|
|
||||||
- URL:
|
|
||||||
|
|
||||||
```text
|
|
||||||
secscore://sidebar/toggle
|
|
||||||
secscore://sidebar
|
|
||||||
```
|
|
||||||
|
|
||||||
- 行为:
|
|
||||||
- 若悬浮侧边栏存在:
|
|
||||||
- 当前可见 → 隐藏
|
|
||||||
- 当前不可见 → 显示并 `focus()`
|
|
||||||
- 若不存在:创建并显示 `global-sidebar` 窗口
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. 使用示例(Windows)
|
|
||||||
|
|
||||||
### 4.1 通过“运行”对话框(Win + R)
|
|
||||||
|
|
||||||
在“运行”窗口中输入:
|
|
||||||
|
|
||||||
```text
|
|
||||||
secscore://settings
|
|
||||||
secscore://sidebar/toggle
|
|
||||||
```
|
|
||||||
|
|
||||||
点击“确定”后,SecScore 会执行对应操作。
|
|
||||||
|
|
||||||
### 4.2 通过浏览器地址栏
|
|
||||||
|
|
||||||
在浏览器(如 Edge)地址栏中输入:
|
|
||||||
|
|
||||||
```text
|
|
||||||
secscore://score
|
|
||||||
```
|
|
||||||
|
|
||||||
浏览器会唤起 SecScore 应用并跳转到“积分管理”。
|
|
||||||
|
|
||||||
### 4.3 通过命令行
|
|
||||||
|
|
||||||
在命令行中执行:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
start "" "secscore://leaderboard"
|
|
||||||
```
|
|
||||||
|
|
||||||
会唤起 SecScore 并打开“排行榜”。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. 安装与注册说明(打包版本)
|
|
||||||
|
|
||||||
> 下述内容针对打包后的安装版本,开发模式(`pnpm dev`)下可能不会自动注册协议。
|
|
||||||
|
|
||||||
- 协议注册配置位于 `electron-builder.yml`:
|
|
||||||
- `scheme`: `secscore`
|
|
||||||
- `name`: `SecScore URL Protocol`
|
|
||||||
- 在安装完成后,操作系统会把所有 `secscore://` URL 交给 SecScore 处理。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. 错误与兼容性说明
|
|
||||||
|
|
||||||
- 所有 URL 都为本地调用,不通过网络传输。
|
|
||||||
- 当前 URL 协议只包含页面级操作与悬浮侧边栏控制,不带参数(例如学生 ID)。
|
|
||||||
- 对于未识别的命令,例如:
|
|
||||||
|
|
||||||
```text
|
|
||||||
secscore://unknown
|
|
||||||
```
|
|
||||||
|
|
||||||
主进程会忽略该 URL,不会抛出错误弹窗。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. 后续扩展建议
|
|
||||||
|
|
||||||
如果需要更复杂的 URL 功能,可以在保持现有兼容性的前提下扩展,例如:
|
|
||||||
|
|
||||||
- 按学生 ID 打开积分记录:
|
|
||||||
|
|
||||||
```text
|
|
||||||
secscore://score/12345
|
|
||||||
```
|
|
||||||
|
|
||||||
- 带筛选条件的排行榜视图:
|
|
||||||
|
|
||||||
```text
|
|
||||||
secscore://leaderboard/today
|
|
||||||
```
|
|
||||||
|
|
||||||
此类扩展只需在主进程的 URL 解析函数中增加相应分支逻辑即可。
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore
|
|
||||||
|
|
||||||
# Built application files
|
|
||||||
*.apk
|
|
||||||
*.aar
|
|
||||||
*.ap_
|
|
||||||
*.aab
|
|
||||||
|
|
||||||
# Files for the ART/Dalvik VM
|
|
||||||
*.dex
|
|
||||||
|
|
||||||
# Java class files
|
|
||||||
*.class
|
|
||||||
|
|
||||||
# Generated files
|
|
||||||
bin/
|
|
||||||
gen/
|
|
||||||
out/
|
|
||||||
# Uncomment the following line in case you need and you don't have the release build type files in your app
|
|
||||||
# release/
|
|
||||||
|
|
||||||
# Gradle files
|
|
||||||
.gradle/
|
|
||||||
build/
|
|
||||||
|
|
||||||
# Local configuration file (sdk path, etc)
|
|
||||||
local.properties
|
|
||||||
|
|
||||||
# Proguard folder generated by Eclipse
|
|
||||||
proguard/
|
|
||||||
|
|
||||||
# Log Files
|
|
||||||
*.log
|
|
||||||
|
|
||||||
# Android Studio Navigation editor temp files
|
|
||||||
.navigation/
|
|
||||||
|
|
||||||
# Android Studio captures folder
|
|
||||||
captures/
|
|
||||||
|
|
||||||
# IntelliJ
|
|
||||||
*.iml
|
|
||||||
.idea/workspace.xml
|
|
||||||
.idea/tasks.xml
|
|
||||||
.idea/gradle.xml
|
|
||||||
.idea/assetWizardSettings.xml
|
|
||||||
.idea/dictionaries
|
|
||||||
.idea/libraries
|
|
||||||
# Android Studio 3 in .gitignore file.
|
|
||||||
.idea/caches
|
|
||||||
.idea/modules.xml
|
|
||||||
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
|
|
||||||
.idea/navEditor.xml
|
|
||||||
|
|
||||||
# Keystore files
|
|
||||||
# Uncomment the following lines if you do not want to check your keystore files in.
|
|
||||||
#*.jks
|
|
||||||
#*.keystore
|
|
||||||
|
|
||||||
# External native build folder generated in Android Studio 2.2 and later
|
|
||||||
.externalNativeBuild
|
|
||||||
.cxx/
|
|
||||||
|
|
||||||
# Google Services (e.g. APIs or Firebase)
|
|
||||||
# google-services.json
|
|
||||||
|
|
||||||
# Freeline
|
|
||||||
freeline.py
|
|
||||||
freeline/
|
|
||||||
freeline_project_description.json
|
|
||||||
|
|
||||||
# fastlane
|
|
||||||
fastlane/report.xml
|
|
||||||
fastlane/Preview.html
|
|
||||||
fastlane/screenshots
|
|
||||||
fastlane/test_output
|
|
||||||
fastlane/readme.md
|
|
||||||
|
|
||||||
# Version control
|
|
||||||
vcs.xml
|
|
||||||
|
|
||||||
# lint
|
|
||||||
lint/intermediates/
|
|
||||||
lint/generated/
|
|
||||||
lint/outputs/
|
|
||||||
lint/tmp/
|
|
||||||
# lint/reports/
|
|
||||||
|
|
||||||
# Android Profiling
|
|
||||||
*.hprof
|
|
||||||
|
|
||||||
# Cordova plugins for Capacitor
|
|
||||||
capacitor-cordova-android-plugins
|
|
||||||
|
|
||||||
# Copied web assets
|
|
||||||
app/src/main/assets/public
|
|
||||||
|
|
||||||
# Generated Config files
|
|
||||||
app/src/main/assets/capacitor.config.json
|
|
||||||
app/src/main/assets/capacitor.plugins.json
|
|
||||||
app/src/main/res/xml/config.xml
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
/build/*
|
|
||||||
!/build/.npmkeep
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
apply plugin: 'com.android.application'
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace = "com.sectl.secscore"
|
|
||||||
compileSdk = rootProject.ext.compileSdkVersion
|
|
||||||
defaultConfig {
|
|
||||||
applicationId "com.sectl.secscore"
|
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
|
||||||
versionCode 1
|
|
||||||
versionName "1.0"
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
||||||
aaptOptions {
|
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
|
||||||
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
|
|
||||||
ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
buildTypes {
|
|
||||||
release {
|
|
||||||
minifyEnabled false
|
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
repositories {
|
|
||||||
flatDir{
|
|
||||||
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
|
||||||
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
|
||||||
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
|
|
||||||
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
|
|
||||||
implementation project(':capacitor-android')
|
|
||||||
testImplementation "junit:junit:$junitVersion"
|
|
||||||
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
|
||||||
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
|
||||||
implementation project(':capacitor-cordova-android-plugins')
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: 'capacitor.build.gradle'
|
|
||||||
|
|
||||||
try {
|
|
||||||
def servicesJSON = file('google-services.json')
|
|
||||||
if (servicesJSON.text) {
|
|
||||||
apply plugin: 'com.google.gms.google-services'
|
|
||||||
}
|
|
||||||
} catch(Exception e) {
|
|
||||||
logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
|
|
||||||
|
|
||||||
android {
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility JavaVersion.VERSION_21
|
|
||||||
targetCompatibility JavaVersion.VERSION_21
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
|
||||||
dependencies {
|
|
||||||
implementation project(':capacitor-preferences')
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (hasProperty('postBuildExtras')) {
|
|
||||||
postBuildExtras()
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# Add project specific ProGuard rules here.
|
|
||||||
# You can control the set of applied configuration files using the
|
|
||||||
# proguardFiles setting in build.gradle.
|
|
||||||
#
|
|
||||||
# For more details, see
|
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
|
||||||
|
|
||||||
# If your project uses WebView with JS, uncomment the following
|
|
||||||
# and specify the fully qualified class name to the JavaScript interface
|
|
||||||
# class:
|
|
||||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
|
||||||
# public *;
|
|
||||||
#}
|
|
||||||
|
|
||||||
# Uncomment this to preserve the line number information for
|
|
||||||
# debugging stack traces.
|
|
||||||
#-keepattributes SourceFile,LineNumberTable
|
|
||||||
|
|
||||||
# If you keep the line number information, uncomment this to
|
|
||||||
# hide the original source file name.
|
|
||||||
#-renamesourcefileattribute SourceFile
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package com.getcapacitor.myapp;
|
|
||||||
|
|
||||||
import static org.junit.Assert.*;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry;
|
|
||||||
import org.junit.Test;
|
|
||||||
import org.junit.runner.RunWith;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Instrumented test, which will execute on an Android device.
|
|
||||||
*
|
|
||||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
|
||||||
*/
|
|
||||||
@RunWith(AndroidJUnit4.class)
|
|
||||||
public class ExampleInstrumentedTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void useAppContext() throws Exception {
|
|
||||||
// Context of the app under test.
|
|
||||||
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
|
|
||||||
|
|
||||||
assertEquals("com.getcapacitor.app", appContext.getPackageName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
|
|
||||||
<application
|
|
||||||
android:allowBackup="true"
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
|
||||||
android:label="@string/app_name"
|
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
|
||||||
android:supportsRtl="true"
|
|
||||||
android:theme="@style/AppTheme">
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation|density"
|
|
||||||
android:name=".MainActivity"
|
|
||||||
android:label="@string/title_activity_main"
|
|
||||||
android:theme="@style/AppTheme.NoActionBarLaunch"
|
|
||||||
android:launchMode="singleTask"
|
|
||||||
android:exported="true">
|
|
||||||
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
</activity>
|
|
||||||
|
|
||||||
<provider
|
|
||||||
android:name="androidx.core.content.FileProvider"
|
|
||||||
android:authorities="${applicationId}.fileprovider"
|
|
||||||
android:exported="false"
|
|
||||||
android:grantUriPermissions="true">
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
|
||||||
android:resource="@xml/file_paths"></meta-data>
|
|
||||||
</provider>
|
|
||||||
</application>
|
|
||||||
|
|
||||||
<!-- Permissions -->
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
|
||||||
</manifest>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
package com.sectl.secscore;
|
|
||||||
|
|
||||||
import com.getcapacitor.BridgeActivity;
|
|
||||||
|
|
||||||
public class MainActivity extends BridgeActivity {}
|
|
||||||
|
Before Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 17 KiB |
@@ -1,34 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:aapt="http://schemas.android.com/aapt"
|
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
|
||||||
android:viewportHeight="108"
|
|
||||||
android:viewportWidth="108">
|
|
||||||
<path
|
|
||||||
android:fillType="evenOdd"
|
|
||||||
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
|
|
||||||
android:strokeColor="#00000000"
|
|
||||||
android:strokeWidth="1">
|
|
||||||
<aapt:attr name="android:fillColor">
|
|
||||||
<gradient
|
|
||||||
android:endX="78.5885"
|
|
||||||
android:endY="90.9159"
|
|
||||||
android:startX="48.7653"
|
|
||||||
android:startY="61.0927"
|
|
||||||
android:type="linear">
|
|
||||||
<item
|
|
||||||
android:color="#44000000"
|
|
||||||
android:offset="0.0" />
|
|
||||||
<item
|
|
||||||
android:color="#00000000"
|
|
||||||
android:offset="1.0" />
|
|
||||||
</gradient>
|
|
||||||
</aapt:attr>
|
|
||||||
</path>
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:fillType="nonZero"
|
|
||||||
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
|
|
||||||
android:strokeColor="#00000000"
|
|
||||||
android:strokeWidth="1" />
|
|
||||||
</vector>
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
|
||||||
android:viewportHeight="108"
|
|
||||||
android:viewportWidth="108">
|
|
||||||
<path
|
|
||||||
android:fillColor="#26A69A"
|
|
||||||
android:pathData="M0,0h108v108h-108z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M9,0L9,108"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,0L19,108"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M29,0L29,108"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M39,0L39,108"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M49,0L49,108"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M59,0L59,108"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M69,0L69,108"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M79,0L79,108"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M89,0L89,108"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M99,0L99,108"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,9L108,9"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,19L108,19"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,29L108,29"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,39L108,39"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,49L108,49"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,59L108,59"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,69L108,69"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,79L108,79"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,89L108,89"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,99L108,99"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,29L89,29"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,39L89,39"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,49L89,49"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,59L89,59"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,69L89,69"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,79L89,79"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M29,19L29,89"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M39,19L39,89"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M49,19L49,89"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M59,19L59,89"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M69,19L69,89"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M79,19L79,89"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
</vector>
|
|
||||||
|
Before Width: | Height: | Size: 3.9 KiB |
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
tools:context=".MainActivity">
|
|
||||||
|
|
||||||
<WebView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent" />
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
|
||||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
|
||||||
</adaptive-icon>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
|
||||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
|
||||||
</adaptive-icon>
|
|
||||||
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 16 KiB |
@@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<color name="ic_launcher_background">#FFFFFF</color>
|
|
||||||
</resources>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<?xml version='1.0' encoding='utf-8'?>
|
|
||||||
<resources>
|
|
||||||
<string name="app_name">SecScore</string>
|
|
||||||
<string name="title_activity_main">SecScore</string>
|
|
||||||
<string name="package_name">com.sectl.secscore</string>
|
|
||||||
<string name="custom_url_scheme">com.sectl.secscore</string>
|
|
||||||
</resources>
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
|
|
||||||
<!-- Base application theme. -->
|
|
||||||
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
|
||||||
<!-- Customize your theme here. -->
|
|
||||||
<item name="colorPrimary">@color/colorPrimary</item>
|
|
||||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
|
||||||
<item name="colorAccent">@color/colorAccent</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
|
|
||||||
<item name="windowActionBar">false</item>
|
|
||||||
<item name="windowNoTitle">true</item>
|
|
||||||
<item name="android:background">@null</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
|
|
||||||
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
|
|
||||||
<item name="android:background">@drawable/splash</item>
|
|
||||||
</style>
|
|
||||||
</resources>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<external-path name="my_images" path="." />
|
|
||||||
<cache-path name="my_cache_images" path="." />
|
|
||||||
</paths>
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package com.getcapacitor.myapp;
|
|
||||||
|
|
||||||
import static org.junit.Assert.*;
|
|
||||||
|
|
||||||
import org.junit.Test;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Example local unit test, which will execute on the development machine (host).
|
|
||||||
*
|
|
||||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
|
||||||
*/
|
|
||||||
public class ExampleUnitTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void addition_isCorrect() throws Exception {
|
|
||||||
assertEquals(4, 2 + 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
|
||||||
|
|
||||||
buildscript {
|
|
||||||
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
dependencies {
|
|
||||||
classpath 'com.android.tools.build:gradle:8.13.0'
|
|
||||||
classpath 'com.google.gms:google-services:4.4.4'
|
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
|
||||||
// in the individual module build.gradle files
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "variables.gradle"
|
|
||||||
|
|
||||||
allprojects {
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
task clean(type: Delete) {
|
|
||||||
delete rootProject.buildDir
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
|
|
||||||
include ':capacitor-android'
|
|
||||||
project(':capacitor-android').projectDir = new File('../node_modules/.pnpm/@capacitor+android@8.1.0_@capacitor+core@8.1.0/node_modules/@capacitor/android/capacitor')
|
|
||||||
|
|
||||||
include ':capacitor-preferences'
|
|
||||||
project(':capacitor-preferences').projectDir = new File('../node_modules/.pnpm/@capacitor+preferences@8.0.1_@capacitor+core@8.1.0/node_modules/@capacitor/preferences/android')
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
# Project-wide Gradle settings.
|
|
||||||
|
|
||||||
# IDE (e.g. Android Studio) users:
|
|
||||||
# Gradle settings configured through the IDE *will override*
|
|
||||||
# any settings specified in this file.
|
|
||||||
|
|
||||||
# For more details on how to configure your build environment visit
|
|
||||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
|
||||||
|
|
||||||
# Specifies the JVM arguments used for the daemon process.
|
|
||||||
# The setting is particularly useful for tweaking memory settings.
|
|
||||||
org.gradle.jvmargs=-Xmx1536m
|
|
||||||
|
|
||||||
# When configured, Gradle will run in incubating parallel mode.
|
|
||||||
# This option should only be used with decoupled projects. More details, visit
|
|
||||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
|
||||||
# org.gradle.parallel=true
|
|
||||||
|
|
||||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
|
||||||
# Android operating system, and which are packaged with your app's APK
|
|
||||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
|
||||||
android.useAndroidX=true
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
|
||||||
distributionPath=wrapper/dists
|
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip
|
|
||||||
networkTimeout=10000
|
|
||||||
validateDistributionUrl=true
|
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
|
||||||
zipStorePath=wrapper/dists
|
|
||||||
@@ -1,251 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
#
|
|
||||||
# Copyright © 2015-2021 the original authors.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
#
|
|
||||||
|
|
||||||
##############################################################################
|
|
||||||
#
|
|
||||||
# Gradle start up script for POSIX generated by Gradle.
|
|
||||||
#
|
|
||||||
# Important for running:
|
|
||||||
#
|
|
||||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
|
||||||
# noncompliant, but you have some other compliant shell such as ksh or
|
|
||||||
# bash, then to run this script, type that shell name before the whole
|
|
||||||
# command line, like:
|
|
||||||
#
|
|
||||||
# ksh Gradle
|
|
||||||
#
|
|
||||||
# Busybox and similar reduced shells will NOT work, because this script
|
|
||||||
# requires all of these POSIX shell features:
|
|
||||||
# * functions;
|
|
||||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
|
||||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
|
||||||
# * compound commands having a testable exit status, especially «case»;
|
|
||||||
# * various built-in commands including «command», «set», and «ulimit».
|
|
||||||
#
|
|
||||||
# Important for patching:
|
|
||||||
#
|
|
||||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
|
||||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
|
||||||
#
|
|
||||||
# The "traditional" practice of packing multiple parameters into a
|
|
||||||
# space-separated string is a well documented source of bugs and security
|
|
||||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
|
||||||
# options in "$@", and eventually passing that to Java.
|
|
||||||
#
|
|
||||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
|
||||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
|
||||||
# see the in-line comments for details.
|
|
||||||
#
|
|
||||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
|
||||||
# Darwin, MinGW, and NonStop.
|
|
||||||
#
|
|
||||||
# (3) This script is generated from the Groovy template
|
|
||||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
|
||||||
# within the Gradle project.
|
|
||||||
#
|
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
|
||||||
#
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
# Attempt to set APP_HOME
|
|
||||||
|
|
||||||
# Resolve links: $0 may be a link
|
|
||||||
app_path=$0
|
|
||||||
|
|
||||||
# Need this for daisy-chained symlinks.
|
|
||||||
while
|
|
||||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
|
||||||
[ -h "$app_path" ]
|
|
||||||
do
|
|
||||||
ls=$( ls -ld "$app_path" )
|
|
||||||
link=${ls#*' -> '}
|
|
||||||
case $link in #(
|
|
||||||
/*) app_path=$link ;; #(
|
|
||||||
*) app_path=$APP_HOME$link ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
# This is normally unused
|
|
||||||
# shellcheck disable=SC2034
|
|
||||||
APP_BASE_NAME=${0##*/}
|
|
||||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
|
||||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
|
||||||
MAX_FD=maximum
|
|
||||||
|
|
||||||
warn () {
|
|
||||||
echo "$*"
|
|
||||||
} >&2
|
|
||||||
|
|
||||||
die () {
|
|
||||||
echo
|
|
||||||
echo "$*"
|
|
||||||
echo
|
|
||||||
exit 1
|
|
||||||
} >&2
|
|
||||||
|
|
||||||
# OS specific support (must be 'true' or 'false').
|
|
||||||
cygwin=false
|
|
||||||
msys=false
|
|
||||||
darwin=false
|
|
||||||
nonstop=false
|
|
||||||
case "$( uname )" in #(
|
|
||||||
CYGWIN* ) cygwin=true ;; #(
|
|
||||||
Darwin* ) darwin=true ;; #(
|
|
||||||
MSYS* | MINGW* ) msys=true ;; #(
|
|
||||||
NONSTOP* ) nonstop=true ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
CLASSPATH="\\\"\\\""
|
|
||||||
|
|
||||||
|
|
||||||
# Determine the Java command to use to start the JVM.
|
|
||||||
if [ -n "$JAVA_HOME" ] ; then
|
|
||||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
|
||||||
# IBM's JDK on AIX uses strange locations for the executables
|
|
||||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
|
||||||
else
|
|
||||||
JAVACMD=$JAVA_HOME/bin/java
|
|
||||||
fi
|
|
||||||
if [ ! -x "$JAVACMD" ] ; then
|
|
||||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
|
||||||
|
|
||||||
Please set the JAVA_HOME variable in your environment to match the
|
|
||||||
location of your Java installation."
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
JAVACMD=java
|
|
||||||
if ! command -v java >/dev/null 2>&1
|
|
||||||
then
|
|
||||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
|
||||||
|
|
||||||
Please set the JAVA_HOME variable in your environment to match the
|
|
||||||
location of your Java installation."
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Increase the maximum file descriptors if we can.
|
|
||||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
|
||||||
case $MAX_FD in #(
|
|
||||||
max*)
|
|
||||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
|
||||||
# shellcheck disable=SC2039,SC3045
|
|
||||||
MAX_FD=$( ulimit -H -n ) ||
|
|
||||||
warn "Could not query maximum file descriptor limit"
|
|
||||||
esac
|
|
||||||
case $MAX_FD in #(
|
|
||||||
'' | soft) :;; #(
|
|
||||||
*)
|
|
||||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
|
||||||
# shellcheck disable=SC2039,SC3045
|
|
||||||
ulimit -n "$MAX_FD" ||
|
|
||||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Collect all arguments for the java command, stacking in reverse order:
|
|
||||||
# * args from the command line
|
|
||||||
# * the main class name
|
|
||||||
# * -classpath
|
|
||||||
# * -D...appname settings
|
|
||||||
# * --module-path (only if needed)
|
|
||||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
|
||||||
|
|
||||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
|
||||||
if "$cygwin" || "$msys" ; then
|
|
||||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
|
||||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
|
||||||
|
|
||||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
|
||||||
|
|
||||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
|
||||||
for arg do
|
|
||||||
if
|
|
||||||
case $arg in #(
|
|
||||||
-*) false ;; # don't mess with options #(
|
|
||||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
|
||||||
[ -e "$t" ] ;; #(
|
|
||||||
*) false ;;
|
|
||||||
esac
|
|
||||||
then
|
|
||||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
|
||||||
fi
|
|
||||||
# Roll the args list around exactly as many times as the number of
|
|
||||||
# args, so each arg winds up back in the position where it started, but
|
|
||||||
# possibly modified.
|
|
||||||
#
|
|
||||||
# NB: a `for` loop captures its iteration list before it begins, so
|
|
||||||
# changing the positional parameters here affects neither the number of
|
|
||||||
# iterations, nor the values presented in `arg`.
|
|
||||||
shift # remove old arg
|
|
||||||
set -- "$@" "$arg" # push replacement arg
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
|
||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
|
||||||
|
|
||||||
# Collect all arguments for the java command:
|
|
||||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
|
||||||
# and any embedded shellness will be escaped.
|
|
||||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
|
||||||
# treated as '${Hostname}' itself on the command line.
|
|
||||||
|
|
||||||
set -- \
|
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
|
||||||
-classpath "$CLASSPATH" \
|
|
||||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
|
||||||
"$@"
|
|
||||||
|
|
||||||
# Stop when "xargs" is not available.
|
|
||||||
if ! command -v xargs >/dev/null 2>&1
|
|
||||||
then
|
|
||||||
die "xargs is not available"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Use "xargs" to parse quoted args.
|
|
||||||
#
|
|
||||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
|
||||||
#
|
|
||||||
# In Bash we could simply go:
|
|
||||||
#
|
|
||||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
|
||||||
# set -- "${ARGS[@]}" "$@"
|
|
||||||
#
|
|
||||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
|
||||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
|
||||||
# character that might be a shell metacharacter, then use eval to reverse
|
|
||||||
# that process (while maintaining the separation between arguments), and wrap
|
|
||||||
# the whole thing up as a single "set" statement.
|
|
||||||
#
|
|
||||||
# This will of course break if any of these variables contains a newline or
|
|
||||||
# an unmatched quote.
|
|
||||||
#
|
|
||||||
|
|
||||||
eval "set -- $(
|
|
||||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
|
||||||
xargs -n1 |
|
|
||||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
|
||||||
tr '\n' ' '
|
|
||||||
)" '"$@"'
|
|
||||||
|
|
||||||
exec "$JAVACMD" "$@"
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
@rem
|
|
||||||
@rem Copyright 2015 the original author or authors.
|
|
||||||
@rem
|
|
||||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
@rem you may not use this file except in compliance with the License.
|
|
||||||
@rem You may obtain a copy of the License at
|
|
||||||
@rem
|
|
||||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
@rem
|
|
||||||
@rem Unless required by applicable law or agreed to in writing, software
|
|
||||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
@rem See the License for the specific language governing permissions and
|
|
||||||
@rem limitations under the License.
|
|
||||||
@rem
|
|
||||||
@rem SPDX-License-Identifier: Apache-2.0
|
|
||||||
@rem
|
|
||||||
|
|
||||||
@if "%DEBUG%"=="" @echo off
|
|
||||||
@rem ##########################################################################
|
|
||||||
@rem
|
|
||||||
@rem Gradle startup script for Windows
|
|
||||||
@rem
|
|
||||||
@rem ##########################################################################
|
|
||||||
|
|
||||||
@rem Set local scope for the variables with windows NT shell
|
|
||||||
if "%OS%"=="Windows_NT" setlocal
|
|
||||||
|
|
||||||
set DIRNAME=%~dp0
|
|
||||||
if "%DIRNAME%"=="" set DIRNAME=.
|
|
||||||
@rem This is normally unused
|
|
||||||
set APP_BASE_NAME=%~n0
|
|
||||||
set APP_HOME=%DIRNAME%
|
|
||||||
|
|
||||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
|
||||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
|
||||||
|
|
||||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
|
||||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
|
||||||
|
|
||||||
@rem Find java.exe
|
|
||||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
|
||||||
|
|
||||||
set JAVA_EXE=java.exe
|
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
|
||||||
if %ERRORLEVEL% equ 0 goto execute
|
|
||||||
|
|
||||||
echo. 1>&2
|
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
|
||||||
echo. 1>&2
|
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
|
||||||
echo location of your Java installation. 1>&2
|
|
||||||
|
|
||||||
goto fail
|
|
||||||
|
|
||||||
:findJavaFromJavaHome
|
|
||||||
set JAVA_HOME=%JAVA_HOME:"=%
|
|
||||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
|
||||||
|
|
||||||
if exist "%JAVA_EXE%" goto execute
|
|
||||||
|
|
||||||
echo. 1>&2
|
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
|
||||||
echo. 1>&2
|
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
|
||||||
echo location of your Java installation. 1>&2
|
|
||||||
|
|
||||||
goto fail
|
|
||||||
|
|
||||||
:execute
|
|
||||||
@rem Setup the command line
|
|
||||||
|
|
||||||
set CLASSPATH=
|
|
||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
|
||||||
|
|
||||||
:end
|
|
||||||
@rem End local scope for the variables with windows NT shell
|
|
||||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
|
||||||
|
|
||||||
:fail
|
|
||||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
|
||||||
rem the _cmd.exe /c_ return code!
|
|
||||||
set EXIT_CODE=%ERRORLEVEL%
|
|
||||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
|
||||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
|
||||||
exit /b %EXIT_CODE%
|
|
||||||
|
|
||||||
:mainEnd
|
|
||||||
if "%OS%"=="Windows_NT" endlocal
|
|
||||||
|
|
||||||
:omega
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
include ':app'
|
|
||||||
include ':capacitor-cordova-android-plugins'
|
|
||||||
project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')
|
|
||||||
|
|
||||||
apply from: 'capacitor.settings.gradle'
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
ext {
|
|
||||||
minSdkVersion = 24
|
|
||||||
compileSdkVersion = 36
|
|
||||||
targetSdkVersion = 36
|
|
||||||
androidxActivityVersion = '1.11.0'
|
|
||||||
androidxAppCompatVersion = '1.7.1'
|
|
||||||
androidxCoordinatorLayoutVersion = '1.3.0'
|
|
||||||
androidxCoreVersion = '1.17.0'
|
|
||||||
androidxFragmentVersion = '1.8.9'
|
|
||||||
coreSplashScreenVersion = '1.2.0'
|
|
||||||
androidxWebkitVersion = '1.14.0'
|
|
||||||
junitVersion = '4.13.2'
|
|
||||||
androidxJunitVersion = '1.3.0'
|
|
||||||
androidxEspressoCoreVersion = '3.7.0'
|
|
||||||
cordovaAndroidVersion = '14.0.1'
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>com.apple.security.cs.allow-jit</key>
|
|
||||||
<true/>
|
|
||||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
|
||||||
<true/>
|
|
||||||
<key>com.apple.security.cs.disable-library-validation</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import type { CapacitorConfig } from '@capacitor/cli'
|
|
||||||
|
|
||||||
const config: CapacitorConfig = {
|
|
||||||
appId: 'com.sectl.secscore',
|
|
||||||
appName: 'SecScore',
|
|
||||||
webDir: 'out',
|
|
||||||
server: {
|
|
||||||
androidScheme: 'https'
|
|
||||||
},
|
|
||||||
plugins: {
|
|
||||||
Preferences: {
|
|
||||||
group: 'SecScore'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
android: {
|
|
||||||
buildOptions: {
|
|
||||||
keystorePath: undefined,
|
|
||||||
keystorePassword: undefined,
|
|
||||||
keystoreAlias: undefined,
|
|
||||||
keystoreAliasPassword: undefined,
|
|
||||||
signingType: 'apksigner'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default config
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
appId: com.secscore.app
|
|
||||||
productName: SecScore
|
|
||||||
directories:
|
|
||||||
buildResources: build
|
|
||||||
protocols:
|
|
||||||
- name: SecScore URL Protocol
|
|
||||||
schemes:
|
|
||||||
- secscore
|
|
||||||
role: Viewer
|
|
||||||
files:
|
|
||||||
- '**/*'
|
|
||||||
- '!**/.vscode/*'
|
|
||||||
- '!src/*'
|
|
||||||
- '!electron.vite.config.{js,ts,mjs,cjs}'
|
|
||||||
- '!{.eslintcache,eslint.config.mjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
|
|
||||||
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
|
|
||||||
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
|
|
||||||
- '!**/node_modules/get-intrinsic/**'
|
|
||||||
- '!**/node_modules/dunder-proto/**'
|
|
||||||
- '!**/node_modules/call-bind/**'
|
|
||||||
- '!**/node_modules/define-data-property/**'
|
|
||||||
- '!**/node_modules/has-property-descriptors/**'
|
|
||||||
- '!**/node_modules/gopd/**'
|
|
||||||
asarUnpack:
|
|
||||||
- resources/**
|
|
||||||
extraFiles:
|
|
||||||
- from: themes
|
|
||||||
to: themes
|
|
||||||
- from: node_modules/get-intrinsic
|
|
||||||
to: node_modules/get-intrinsic
|
|
||||||
- from: node_modules/dunder-proto
|
|
||||||
to: node_modules/dunder-proto
|
|
||||||
- from: node_modules/call-bind
|
|
||||||
to: node_modules/call-bind
|
|
||||||
- from: node_modules/define-data-property
|
|
||||||
to: node_modules/define-data-property
|
|
||||||
- from: node_modules/has-property-descriptors
|
|
||||||
to: node_modules/has-property-descriptors
|
|
||||||
- from: node_modules/gopd
|
|
||||||
to: node_modules/gopd
|
|
||||||
extraResources:
|
|
||||||
- from: resources
|
|
||||||
to: assets
|
|
||||||
win:
|
|
||||||
target:
|
|
||||||
- zip
|
|
||||||
executableName: SecScore
|
|
||||||
icon: resources/SecScore_logo.ico
|
|
||||||
nsis:
|
|
||||||
artifactName: ${name}-${version}-setup.${ext}
|
|
||||||
shortcutName: ${productName}
|
|
||||||
uninstallDisplayName: ${productName}
|
|
||||||
createDesktopShortcut: always
|
|
||||||
allowToChangeInstallationDirectory: true
|
|
||||||
mac:
|
|
||||||
entitlementsInherit: build/entitlements.mac.plist
|
|
||||||
extendInfo:
|
|
||||||
- NSCameraUsageDescription: Application requests access to the device's camera.
|
|
||||||
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
|
||||||
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
|
|
||||||
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
|
|
||||||
notarize: false
|
|
||||||
dmg:
|
|
||||||
artifactName: ${name}-${version}.${ext}
|
|
||||||
linux:
|
|
||||||
target:
|
|
||||||
- AppImage
|
|
||||||
- deb
|
|
||||||
maintainer: electronjs.org
|
|
||||||
category: Utility
|
|
||||||
appImage:
|
|
||||||
artifactName: ${name}-${version}.${ext}
|
|
||||||
npmRebuild: false
|
|
||||||
electronDownload:
|
|
||||||
mirror: https://npmmirror.com/mirrors/electron/
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { resolve } from 'path'
|
|
||||||
import { defineConfig } from 'electron-vite'
|
|
||||||
import react from '@vitejs/plugin-react'
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
main: {
|
|
||||||
build: {
|
|
||||||
rollupOptions: {
|
|
||||||
external: []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
preload: {},
|
|
||||||
renderer: {
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
'@renderer': resolve('src/renderer/src')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
plugins: [react()]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import { defineConfig } from 'eslint/config'
|
|
||||||
import tseslint from '@electron-toolkit/eslint-config-ts'
|
|
||||||
import eslintConfigPrettier from '@electron-toolkit/eslint-config-prettier'
|
|
||||||
import eslintPluginReact from 'eslint-plugin-react'
|
|
||||||
import eslintPluginReactHooks from 'eslint-plugin-react-hooks'
|
|
||||||
import eslintPluginReactRefresh from 'eslint-plugin-react-refresh'
|
|
||||||
|
|
||||||
export default defineConfig(
|
|
||||||
{ ignores: ['**/node_modules', '**/dist', '**/out', 'scripts/**', 'secrandom_ipc_send_url.js'] },
|
|
||||||
tseslint.configs.recommended,
|
|
||||||
eslintPluginReact.configs.flat.recommended,
|
|
||||||
eslintPluginReact.configs.flat['jsx-runtime'],
|
|
||||||
{
|
|
||||||
settings: {
|
|
||||||
react: {
|
|
||||||
version: 'detect'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: ['**/*.{ts,tsx}'],
|
|
||||||
plugins: {
|
|
||||||
'react-hooks': eslintPluginReactHooks,
|
|
||||||
'react-refresh': eslintPluginReactRefresh
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
...eslintPluginReactHooks.configs.recommended.rules,
|
|
||||||
...eslintPluginReactRefresh.configs.vite.rules,
|
|
||||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
|
||||||
'@typescript-eslint/no-explicit-any': 'off',
|
|
||||||
'react-refresh/only-export-components': 'off',
|
|
||||||
'react-hooks/exhaustive-deps': 'warn',
|
|
||||||
'react-hooks/set-state-in-effect': 'off',
|
|
||||||
'@typescript-eslint/no-require-imports': 'off',
|
|
||||||
// we use TypeScript types instead of PropTypes in React components
|
|
||||||
'react/prop-types': 'off'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
eslintConfigPrettier
|
|
||||||
)
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "secscore",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "SecScore – Your Education Points Management Expert",
|
|
||||||
"main": "./out/main/index.js",
|
|
||||||
"author": "example.com",
|
|
||||||
"homepage": "https://example.org",
|
|
||||||
"scripts": {
|
|
||||||
"format": "prettier --write .",
|
|
||||||
"lint": "eslint --cache .",
|
|
||||||
"lint:fix": "eslint --cache . --fix",
|
|
||||||
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
|
|
||||||
"typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
|
|
||||||
"typecheck": "pnpm -s typecheck:node && pnpm -s typecheck:web",
|
|
||||||
"start": "electron-vite preview",
|
|
||||||
"dev": "electron-vite dev",
|
|
||||||
"build": "node scripts/clean-db.mjs && pnpm -s typecheck && electron-vite build",
|
|
||||||
"postinstall": "electron-builder install-app-deps",
|
|
||||||
"build:unpack": "pnpm -s build && electron-builder --dir --publish never",
|
|
||||||
"build:win": "pnpm -s build && electron-builder --win --publish never",
|
|
||||||
"build:mac": "electron-vite build && electron-builder --mac --publish never",
|
|
||||||
"build:linux": "electron-vite build && electron-builder --linux --publish never",
|
|
||||||
"build:mobile": "vite build --config vite.mobile.config.ts",
|
|
||||||
"cap:sync": "npx cap sync",
|
|
||||||
"cap:open:android": "npx cap open android",
|
|
||||||
"mobile:build": "pnpm build:mobile && pnpm cap:sync",
|
|
||||||
"mobile:android": "pnpm mobile:build && npx cap open android"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@ant-design/icons": "^6.1.0",
|
|
||||||
"@capacitor/android": "^8.1.0",
|
|
||||||
"@capacitor/cli": "^8.1.0",
|
|
||||||
"@capacitor/core": "^8.1.0",
|
|
||||||
"@capacitor/preferences": "^8.0.1",
|
|
||||||
"@electron-toolkit/preload": "^3.0.2",
|
|
||||||
"@electron-toolkit/utils": "^4.0.0",
|
|
||||||
"@react-querybuilder/antd": "^8.14.0",
|
|
||||||
"@react-querybuilder/dnd": "^8.14.0",
|
|
||||||
"antd": "^6.3.1",
|
|
||||||
"antd-mobile": "^5.42.3",
|
|
||||||
"better-sqlite3": "^12.6.0",
|
|
||||||
"chokidar": "^5.0.0",
|
|
||||||
"dayjs": "^1.11.20",
|
|
||||||
"es-object-atoms": "^1.1.1",
|
|
||||||
"express": "^5.2.1",
|
|
||||||
"i18next": "^25.8.14",
|
|
||||||
"json-rules-engine": "^7.3.1",
|
|
||||||
"math-intrinsics": "^1.1.0",
|
|
||||||
"mica-electron": "^1.5.16",
|
|
||||||
"os": "^0.1.2",
|
|
||||||
"pg": "^8.19.0",
|
|
||||||
"pinyin-pro": "^3.27.0",
|
|
||||||
"react-dnd": "^16.0.1",
|
|
||||||
"react-dnd-html5-backend": "^16.0.1",
|
|
||||||
"react-dnd-touch-backend": "^16.0.1",
|
|
||||||
"react-i18next": "^16.5.6",
|
|
||||||
"react-querybuilder": "^8.14.0",
|
|
||||||
"react-router-dom": "^6.28.0",
|
|
||||||
"reflect-metadata": "^0.2.2",
|
|
||||||
"typeorm": "^0.3.27",
|
|
||||||
"uuid": "^13.0.0",
|
|
||||||
"winston": "^3.19.0",
|
|
||||||
"winston-daily-rotate-file": "^5.0.0",
|
|
||||||
"xlsx": "^0.18.5"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
|
|
||||||
"@electron-toolkit/eslint-config-ts": "^3.1.0",
|
|
||||||
"@electron-toolkit/tsconfig": "^2.0.0",
|
|
||||||
"@types/better-sqlite3": "^7.6.13",
|
|
||||||
"@types/node": "^22.19.1",
|
|
||||||
"@types/pg": "^8.18.0",
|
|
||||||
"@types/react": "^19.2.7",
|
|
||||||
"@types/react-dom": "^19.2.3",
|
|
||||||
"@types/uuid": "^11.0.0",
|
|
||||||
"@vitejs/plugin-react": "^5.1.1",
|
|
||||||
"electron": "^39.2.6",
|
|
||||||
"electron-builder": "^26.0.12",
|
|
||||||
"electron-vite": "^5.0.0",
|
|
||||||
"eslint": "^9.39.1",
|
|
||||||
"eslint-plugin-react": "^7.37.5",
|
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
|
||||||
"eslint-plugin-react-refresh": "^0.4.24",
|
|
||||||
"prettier": "^3.7.4",
|
|
||||||
"react": "^19.2.1",
|
|
||||||
"react-dom": "^19.2.1",
|
|
||||||
"terser": "^5.46.0",
|
|
||||||
"typescript": "^5.9.3",
|
|
||||||
"vite": "^7.2.6"
|
|
||||||
},
|
|
||||||
"pnpm": {
|
|
||||||
"onlyBuiltDependencies": [
|
|
||||||
"electron",
|
|
||||||
"electron-winstaller",
|
|
||||||
"esbuild"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<!-- Creator: CorelDRAW (OEM 版本) -->
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="57.5732mm" height="57.5732mm" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
|
|
||||||
viewBox="0 0 5757.32 5757.32"
|
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
||||||
xmlns:xodm="http://www.corel.com/coreldraw/odm/2003">
|
|
||||||
<defs>
|
|
||||||
<style type="text/css">
|
|
||||||
<![CDATA[
|
|
||||||
.str0 {stroke:#FEFEFE;stroke-width:20;stroke-miterlimit:22.9256}
|
|
||||||
.str1 {stroke:#2387ED;stroke-width:20;stroke-miterlimit:22.9256}
|
|
||||||
.str2 {stroke:#2789ED;stroke-width:20;stroke-miterlimit:22.9256}
|
|
||||||
.fil0 {fill:#FEFEFE}
|
|
||||||
.fil1 {fill:#2389EC}
|
|
||||||
.fil4 {fill:#75BDF7}
|
|
||||||
.fil2 {fill:#FFDA31}
|
|
||||||
.fil3 {fill:#FFF270}
|
|
||||||
]]>
|
|
||||||
</style>
|
|
||||||
</defs>
|
|
||||||
<g id="圖層_x0020_1">
|
|
||||||
<metadata id="CorelCorpID_0Corel-Layer"/>
|
|
||||||
<g id="_1556526623248">
|
|
||||||
<rect class="fil0 str0" x="27.11" y="26.34" width="5675.33" height="5687.62" rx="1140.46" ry="1142.93"/>
|
|
||||||
<circle class="fil1 str1" cx="3484.74" cy="2200.34" r="920.68"/>
|
|
||||||
<circle class="fil0 str0" cx="3951.4" cy="3983.83" r="1288.52"/>
|
|
||||||
<g>
|
|
||||||
<circle class="fil2" cx="3951.4" cy="3983.83" r="1033.53"/>
|
|
||||||
<path class="fil3" d="M4139.76 3707.08c-14.94,-25.96 -43.54,-77.79 -66.66,-124.1 -23.11,-46.3 -40.75,-87.08 -61.13,-131.88 -20.37,-44.8 -43.48,-93.63 -73.05,-71.06 -29.57,22.56 -65.6,116.49 -95.71,187.21 -30.1,70.71 -54.3,118.2 -66.4,141.95 -12.1,23.74 -12.1,23.74 -12.1,23.74 0,0 0,0 -36.04,5.26 -36.03,5.25 -108.1,15.76 -187.6,26.12 -79.5,10.37 -166.42,20.58 -177.47,52.05 -11.04,31.46 53.79,84.19 113.27,132.93 59.48,48.74 113.6,93.5 140.66,115.88 27.06,22.38 27.06,22.38 27.06,22.38 0,0 0,0 -4.8,29.82 -4.81,29.82 -14.42,89.45 -29.87,166.02 -15.45,76.57 -36.73,170.06 -13.79,197.73 22.93,27.66 90.08,-10.5 157.94,-50.54 67.86,-40.04 136.43,-81.95 170.71,-102.91 34.28,-20.96 34.28,-20.96 34.28,-20.96 0,0 0,0 29.08,17.71 29.07,17.71 87.22,53.13 150.46,92.03 63.23,38.89 131.56,81.25 159.24,64.16 27.68,-17.1 14.73,-93.67 3.9,-169.09 -10.82,-75.42 -19.51,-149.7 -23.86,-186.84 -4.35,-37.13 -4.35,-37.13 58.7,-87.16 63.04,-50.02 189.13,-150.07 193.62,-207.02 4.48,-56.94 -112.64,-70.78 -173.29,-77.95 -60.65,-7.17 -64.83,-7.68 -79.61,-9.01 -14.78,-1.33 -40.17,-3.48 -61.94,-5.33 -21.77,-1.86 -39.91,-3.4 -49.64,-4.23 -9.72,-0.82 -11.01,-0.94 -25.96,-26.91z"/>
|
|
||||||
</g>
|
|
||||||
<ellipse class="fil0" cx="2140.85" cy="2197.59" rx="1194.99" ry="1178.43"/>
|
|
||||||
<circle class="fil4" cx="2124.15" cy="2207.61" r="923.37"/>
|
|
||||||
<path class="fil1 str2" d="M2788.24 3393.56c-11.75,24.43 -34.95,73.29 -54.62,123.82 -19.67,50.52 -35.81,102.71 -53.14,186.31 -17.34,83.6 -35.86,198.63 -29.23,317.47 6.63,118.84 38.41,241.5 66,328.24 27.59,86.75 50.98,137.57 62.68,162.99 11.7,25.41 11.7,25.41 11.7,25.41 0,0 0,0 -278.03,0 -278.03,0 -834.08,0 -1112.11,0 -278.02,0 -278.02,0 -291.55,0.23 -13.53,0.23 -40.59,0.69 -75.21,-7.93 -34.61,-8.62 -76.78,-26.31 -107.12,-64.94 -30.33,-38.64 -48.83,-98.21 -57.28,-137.12 -8.46,-38.92 -6.87,-57.19 -6.07,-82.24 0.79,-25.05 0.79,-56.9 0.79,-80.43 0,-23.54 0,-38.77 1.5,-69.12 1.5,-30.35 4.5,-75.83 9.34,-112.5 4.84,-36.68 11.52,-64.54 44.17,-133.73 32.66,-69.18 91.29,-179.68 171.72,-260.68 80.45,-81 182.69,-132.49 273.98,-164.28 91.29,-31.78 171.63,-43.86 218.71,-49.89 47.09,-6.04 60.94,-6.04 74.09,-6.04 13.15,0 25.61,0 212.92,0 187.31,0 549.47,0 730.87,0 181.4,0 182.04,0 182.71,0 0.67,0 1.37,0 3.04,0 1.68,0 4.31,0 6.31,0 2.01,0 3.37,0 3.98,0 0.62,0 0.48,0 0.72,0 0.24,0 0.86,0 1.02,0 0.16,0 -0.14,0 -11.89,24.43z"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 401 KiB |
@@ -1,10 +0,0 @@
|
|||||||
const fs = require('fs')
|
|
||||||
const path = require('path')
|
|
||||||
|
|
||||||
const afterPack = async (context) => {
|
|
||||||
const appOutDir = context.appOutDir
|
|
||||||
fs.mkdirSync(path.join(appOutDir, 'data'), { recursive: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = afterPack
|
|
||||||
module.exports.default = afterPack
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
import fs from 'fs'
|
|
||||||
import path from 'path'
|
|
||||||
|
|
||||||
const rawVersion = String(process.argv[2] || '')
|
|
||||||
.trim()
|
|
||||||
.replace(/^v/i, '')
|
|
||||||
if (!rawVersion) {
|
|
||||||
process.stderr.write('缺少版本号参数,例如:node scripts/ci/apply-version.mjs 1.2.3\n')
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const semverRe = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+([0-9A-Za-z.-]+))?$/
|
|
||||||
|
|
||||||
function toSemver(version) {
|
|
||||||
if (semverRe.test(version)) {
|
|
||||||
return version
|
|
||||||
}
|
|
||||||
|
|
||||||
const parts = version.split(/[._-]/)
|
|
||||||
const nums = parts.filter((p) => /^\d+$/.test(p))
|
|
||||||
const nonNums = parts.filter((p) => !/^\d+$/.test(p) && p.length > 0)
|
|
||||||
|
|
||||||
let major = '0',
|
|
||||||
minor = '0',
|
|
||||||
patch = '0'
|
|
||||||
let prerelease = ''
|
|
||||||
let buildMeta = ''
|
|
||||||
|
|
||||||
if (nums.length >= 3) {
|
|
||||||
major = nums[0]
|
|
||||||
minor = nums[1]
|
|
||||||
patch = nums[2]
|
|
||||||
if (nums.length > 3) {
|
|
||||||
buildMeta = nums.slice(3).join('.')
|
|
||||||
}
|
|
||||||
} else if (nums.length === 2) {
|
|
||||||
major = nums[0]
|
|
||||||
minor = nums[1]
|
|
||||||
patch = '0'
|
|
||||||
} else if (nums.length === 1) {
|
|
||||||
major = nums[0]
|
|
||||||
minor = '0'
|
|
||||||
patch = '0'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nonNums.length > 0) {
|
|
||||||
if (prerelease) {
|
|
||||||
prerelease += '.' + nonNums.join('.')
|
|
||||||
} else {
|
|
||||||
prerelease = nonNums.join('.')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = `${major}.${minor}.${patch}`
|
|
||||||
if (prerelease) {
|
|
||||||
result += `-${prerelease}`
|
|
||||||
}
|
|
||||||
if (buildMeta) {
|
|
||||||
result += `+${buildMeta}`
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
const version = toSemver(rawVersion)
|
|
||||||
|
|
||||||
process.stdout.write(`版本号转换: "${rawVersion}" -> "${version}"\n`)
|
|
||||||
|
|
||||||
const pkgPath = path.join(process.cwd(), 'package.json')
|
|
||||||
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
|
|
||||||
pkg.version = version
|
|
||||||
fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf-8')
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
import fs from 'fs'
|
|
||||||
import path from 'path'
|
|
||||||
|
|
||||||
const cwd = process.cwd()
|
|
||||||
const pkgPath = path.join(cwd, 'package.json')
|
|
||||||
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
|
|
||||||
const scripts = pkg?.scripts ?? {}
|
|
||||||
|
|
||||||
const rawMessage =
|
|
||||||
process.env.RELEASE_MESSAGE ??
|
|
||||||
process.env.GITHUB_COMMIT_MESSAGE ??
|
|
||||||
process.argv.slice(2).join(' ') ??
|
|
||||||
''
|
|
||||||
const message = String(rawMessage || '').trim()
|
|
||||||
|
|
||||||
// 是否以 release: 开头
|
|
||||||
const isReleasePrefix = /^release:/i.test(message)
|
|
||||||
|
|
||||||
const semverRe = /\bv?(\S+)/
|
|
||||||
const versionMatch = message.match(semverRe)
|
|
||||||
const version = versionMatch ? versionMatch[1] : ''
|
|
||||||
|
|
||||||
const buildToken =
|
|
||||||
message.match(/\b(build:(?:win|mac|linux|unpack|all))\b/i)?.[1]?.toLowerCase() ?? ''
|
|
||||||
let buildScript =
|
|
||||||
buildToken === 'build:all'
|
|
||||||
? 'build:all'
|
|
||||||
: buildToken && buildToken.startsWith('build:')
|
|
||||||
? buildToken
|
|
||||||
: ''
|
|
||||||
|
|
||||||
// 如果是 release: 开头且没有显式指定构建脚本,默认为 build:all
|
|
||||||
if (isReleasePrefix && !buildScript) {
|
|
||||||
buildScript = 'build:all'
|
|
||||||
}
|
|
||||||
|
|
||||||
const supportedBuildScripts = new Set(['build:win', 'build:mac', 'build:linux', 'build:unpack'])
|
|
||||||
const hasBuildScript = buildScript
|
|
||||||
? buildScript === 'build:all'
|
|
||||||
? [...supportedBuildScripts].every((s) => typeof scripts[s] === 'string' && scripts[s])
|
|
||||||
: typeof scripts[buildScript] === 'string' && scripts[buildScript]
|
|
||||||
: false
|
|
||||||
|
|
||||||
const fail = (text) => {
|
|
||||||
process.stderr.write(`${text}\n`)
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasAnySignal = Boolean(isReleasePrefix || version || buildScript)
|
|
||||||
const shouldSkip = !hasAnySignal
|
|
||||||
|
|
||||||
if (!shouldSkip) {
|
|
||||||
if (!version) {
|
|
||||||
fail(
|
|
||||||
[
|
|
||||||
'未在提交信息中检测到版本号。',
|
|
||||||
'示例:release: v1.2.3 build:win 或 release: 1.2.3',
|
|
||||||
'支持格式:v1.2.3 或 1.2.3(可带 -beta.1 等后缀)'
|
|
||||||
].join('\n')
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!buildScript) {
|
|
||||||
fail(
|
|
||||||
[
|
|
||||||
'未在提交信息中检测到构建命令标记。',
|
|
||||||
'示例:release: v1.2.3 build:win',
|
|
||||||
'支持:build:win | build:mac | build:linux | build:unpack | build:all'
|
|
||||||
].join('\n')
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasBuildScript) {
|
|
||||||
fail(`构建脚本不存在或为空:${buildScript}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const runWin = !shouldSkip && (buildScript === 'build:all' || buildScript === 'build:win')
|
|
||||||
const runMac = !shouldSkip && (buildScript === 'build:all' || buildScript === 'build:mac')
|
|
||||||
const runLinux = !shouldSkip && (buildScript === 'build:all' || buildScript === 'build:linux')
|
|
||||||
const runUnpack = !shouldSkip && buildScript === 'build:unpack'
|
|
||||||
|
|
||||||
const outputs = {
|
|
||||||
version: shouldSkip ? '' : version,
|
|
||||||
tag: shouldSkip ? '' : `v${version}`,
|
|
||||||
build_script: shouldSkip ? '' : buildScript,
|
|
||||||
run_win: String(runWin),
|
|
||||||
run_mac: String(runMac),
|
|
||||||
run_linux: String(runLinux),
|
|
||||||
run_unpack: String(runUnpack)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env.GITHUB_OUTPUT) {
|
|
||||||
const lines = Object.entries(outputs).map(([k, v]) => `${k}=${v}`)
|
|
||||||
fs.appendFileSync(process.env.GITHUB_OUTPUT, `${lines.join('\n')}\n`)
|
|
||||||
} else {
|
|
||||||
process.stdout.write(`${JSON.stringify(outputs, null, 2)}\n`)
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import fs from 'fs'
|
|
||||||
import path from 'path'
|
|
||||||
|
|
||||||
const root = process.cwd()
|
|
||||||
const targets = ['db.sqlite']
|
|
||||||
|
|
||||||
for (const name of targets) {
|
|
||||||
const filePath = path.join(root, name)
|
|
||||||
try {
|
|
||||||
if (fs.existsSync(filePath)) {
|
|
||||||
fs.rmSync(filePath, { force: true })
|
|
||||||
console.log(`[clean-db] removed ${filePath}`)
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
if (e.code === 'EPERM' || e.code === 'EACCES') {
|
|
||||||
console.warn(`[clean-db] skipped ${filePath}: file in use or permission denied`)
|
|
||||||
} else {
|
|
||||||
console.error(`[clean-db] failed to remove ${filePath}:`, e?.message || e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
/**
|
|
||||||
* SecRandom IPC URL 发送脚本 (Node.js 版本)
|
|
||||||
*
|
|
||||||
* 用途
|
|
||||||
* - 向本机正在运行的 SecRandom 实例发送 URL(例如 SecRandom://settings)
|
|
||||||
* - 通过“命名 IPC 通道”连接(Windows 命名管道 / Linux socket),无需端口
|
|
||||||
*
|
|
||||||
* 前提
|
|
||||||
* - SecRandom 主程序已启动,并已启动 URL IPC 服务端
|
|
||||||
*
|
|
||||||
* 基本用法(Windows / Linux 通用)
|
|
||||||
* - 发送打开设置页:
|
|
||||||
* node secrandom_ipc_send_url.js "SecRandom://settings"
|
|
||||||
* - 发送切换页面:
|
|
||||||
* node secrandom_ipc_send_url.js "SecRandom://main/lottery"
|
|
||||||
*
|
|
||||||
* 参数说明
|
|
||||||
* - url:要发送的 URL(大小写不敏感)
|
|
||||||
* --ipc-name:目标通道名,默认 SecRandom.secrandom
|
|
||||||
* - Windows 对应:\\.\pipe\SecRandom.secrandom
|
|
||||||
* - Linux 对应:/tmp/SecRandom.secrandom.sock
|
|
||||||
*
|
|
||||||
* 返回与退出码
|
|
||||||
* - 输出:打印服务端响应 JSON
|
|
||||||
* - 退出码:
|
|
||||||
* - 0:success 为 true
|
|
||||||
* - 2:success 为 false(例如命令不支持/被拒绝/服务端返回失败)
|
|
||||||
* - 1:脚本运行异常(例如连接失败、序列化失败等)
|
|
||||||
*/
|
|
||||||
|
|
||||||
const net = require('net')
|
|
||||||
|
|
||||||
function parseArgs(argv) {
|
|
||||||
const out = { url: '', ipcName: 'SecRandom.secrandom', timeout: 5000 }
|
|
||||||
const args = Array.from(argv)
|
|
||||||
for (let i = 0; i < args.length; i++) {
|
|
||||||
const a = String(args[i] ?? '')
|
|
||||||
if (a === '--help' || a === '-h') return { help: true }
|
|
||||||
|
|
||||||
if (a === '--ipc-name') {
|
|
||||||
out.ipcName = String(args[i + 1] ?? '').trim() || out.ipcName
|
|
||||||
i++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (a.startsWith('--ipc-name=')) {
|
|
||||||
out.ipcName = a.slice('--ipc-name='.length).trim() || out.ipcName
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (a === '--timeout') {
|
|
||||||
const n = Number.parseInt(String(args[i + 1] ?? ''), 10)
|
|
||||||
if (Number.isFinite(n) && n > 0) out.timeout = n
|
|
||||||
i++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (a.startsWith('--timeout=')) {
|
|
||||||
const n = Number.parseInt(a.slice('--timeout='.length), 10)
|
|
||||||
if (Number.isFinite(n) && n > 0) out.timeout = n
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!a.startsWith('-') && !out.url) {
|
|
||||||
out.url = a
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsed = parseArgs(process.argv.slice(2))
|
|
||||||
if (parsed.help || !parsed.url) {
|
|
||||||
console.log(
|
|
||||||
'Usage: node secrandom_ipc_send_url.js <url> [--ipc-name <name>] [--timeout <ms>]\n' +
|
|
||||||
'Example: node secrandom_ipc_send_url.js "SecRandom://settings"'
|
|
||||||
)
|
|
||||||
process.exit(parsed.help ? 0 : 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const { url, ipcName, timeout } = parsed
|
|
||||||
|
|
||||||
function getIpcAddress(ipcName) {
|
|
||||||
if (process.platform === 'win32') {
|
|
||||||
return `\\\\.\\pipe\\${ipcName}`
|
|
||||||
}
|
|
||||||
return `/tmp/${ipcName}.sock`
|
|
||||||
}
|
|
||||||
|
|
||||||
const ipcAddress = getIpcAddress(ipcName)
|
|
||||||
const message = JSON.stringify({ type: 'url', payload: { url } })
|
|
||||||
|
|
||||||
const client = net.createConnection(ipcAddress)
|
|
||||||
|
|
||||||
client.setTimeout(parseInt(timeout))
|
|
||||||
|
|
||||||
client.on('connect', () => {
|
|
||||||
client.write(message + '\n')
|
|
||||||
})
|
|
||||||
|
|
||||||
client.on('data', (data) => {
|
|
||||||
try {
|
|
||||||
const response = JSON.parse(data.toString())
|
|
||||||
console.log(JSON.stringify(response, null, 2))
|
|
||||||
process.exit(response.success ? 0 : 2)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to parse response:', error)
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
client.on('timeout', () => {
|
|
||||||
console.error('Connection timeout')
|
|
||||||
client.destroy()
|
|
||||||
process.exit(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
client.on('error', (error) => {
|
|
||||||
if (error.code === 'ENOENT') {
|
|
||||||
console.error(`IPC 通道不存在: ${ipcAddress}. 请确认 SecRandom 已运行且已启动 IPC 服务端。`)
|
|
||||||
} else {
|
|
||||||
console.error('IPC send failed:', error.message)
|
|
||||||
}
|
|
||||||
process.exit(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
client.on('close', () => {
|
|
||||||
if (process.exitCode === undefined) {
|
|
||||||
console.error('Connection closed without response')
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { Context as BaseContext } from '../shared/kernel'
|
|
||||||
import { ipcMain } from 'electron'
|
|
||||||
|
|
||||||
export class MainContext extends BaseContext {
|
|
||||||
public isQuitting = false
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super()
|
|
||||||
}
|
|
||||||
|
|
||||||
handle(
|
|
||||||
channel: string,
|
|
||||||
listener: (event: Electron.IpcMainInvokeEvent, ...args: any[]) => Promise<any> | any
|
|
||||||
) {
|
|
||||||
ipcMain.handle(channel, listener)
|
|
||||||
this.effect(() => ipcMain.removeHandler(channel))
|
|
||||||
}
|
|
||||||
|
|
||||||
ipcOn(channel: string, listener: (event: Electron.IpcMainEvent, ...args: any[]) => void) {
|
|
||||||
ipcMain.on(channel, listener)
|
|
||||||
this.effect(() => ipcMain.removeListener(channel, listener))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,245 +0,0 @@
|
|||||||
import { Context, Service } from '../../shared/kernel'
|
|
||||||
import { DataSource } from 'typeorm'
|
|
||||||
import path from 'path'
|
|
||||||
import fs from 'fs'
|
|
||||||
import { StudentEntity } from './entities/StudentEntity'
|
|
||||||
import { ReasonEntity } from './entities/ReasonEntity'
|
|
||||||
import { ScoreEventEntity } from './entities/ScoreEventEntity'
|
|
||||||
import { SettlementEntity } from './entities/SettlementEntity'
|
|
||||||
import { SettingEntity } from './entities/SettingEntity'
|
|
||||||
import { TagEntity } from './entities/TagEntity'
|
|
||||||
import { StudentTagEntity } from './entities/StudentTagEntity'
|
|
||||||
import { migrations } from './migrations'
|
|
||||||
|
|
||||||
declare module '../../shared/kernel' {
|
|
||||||
interface Context {
|
|
||||||
db: DbManager
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PostgreSQLConfig {
|
|
||||||
host: string
|
|
||||||
port: number
|
|
||||||
username: string
|
|
||||||
password: string
|
|
||||||
database: string
|
|
||||||
ssl?: boolean
|
|
||||||
sslmode?: string
|
|
||||||
channelBinding?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface QueueItem {
|
|
||||||
id: string
|
|
||||||
operation: () => Promise<any>
|
|
||||||
resolve: (value: any) => void
|
|
||||||
reject: (error: Error) => void
|
|
||||||
timestamp: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parsePostgreSQLConnectionString(connectionString: string): PostgreSQLConfig | null {
|
|
||||||
if (!connectionString.startsWith('postgresql://')) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const url = new URL(connectionString)
|
|
||||||
const config: PostgreSQLConfig = {
|
|
||||||
host: url.hostname,
|
|
||||||
port: url.port ? parseInt(url.port, 10) : 5432,
|
|
||||||
username: url.username,
|
|
||||||
password: decodeURIComponent(url.password || ''),
|
|
||||||
database: url.pathname.slice(1),
|
|
||||||
ssl: false
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = new URLSearchParams(url.search)
|
|
||||||
if (params.get('sslmode')) {
|
|
||||||
config.ssl = true
|
|
||||||
config.sslmode = params.get('sslmode') || 'require'
|
|
||||||
}
|
|
||||||
if (params.get('channel_binding')) {
|
|
||||||
config.channelBinding = params.get('channel_binding') || 'require'
|
|
||||||
}
|
|
||||||
|
|
||||||
return config
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to parse PostgreSQL connection string:', e)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DbManager extends Service {
|
|
||||||
private _dataSource: DataSource | null = null
|
|
||||||
private _isPostgreSQL: boolean = false
|
|
||||||
private dbPath: string
|
|
||||||
private operationQueue: QueueItem[] = []
|
|
||||||
private isProcessingQueue: boolean = false
|
|
||||||
private queueIdCounter: number = 0
|
|
||||||
|
|
||||||
constructor(ctx: Context, dbPath: string, pgConnectionString?: string) {
|
|
||||||
super(ctx, 'db')
|
|
||||||
this.dbPath = dbPath
|
|
||||||
this._isPostgreSQL = !!pgConnectionString
|
|
||||||
}
|
|
||||||
|
|
||||||
get dataSource(): DataSource {
|
|
||||||
if (!this._dataSource) {
|
|
||||||
throw new Error('Database not initialized. Call initialize() first.')
|
|
||||||
}
|
|
||||||
return this._dataSource
|
|
||||||
}
|
|
||||||
|
|
||||||
get isPostgreSQL(): boolean {
|
|
||||||
return this._isPostgreSQL
|
|
||||||
}
|
|
||||||
|
|
||||||
private createDataSource(pgConnectionString?: string): DataSource {
|
|
||||||
const pgConfig = pgConnectionString ? parsePostgreSQLConnectionString(pgConnectionString) : null
|
|
||||||
|
|
||||||
if (pgConfig) {
|
|
||||||
this._isPostgreSQL = true
|
|
||||||
return new DataSource({
|
|
||||||
type: 'postgres',
|
|
||||||
host: pgConfig.host,
|
|
||||||
port: pgConfig.port,
|
|
||||||
username: pgConfig.username,
|
|
||||||
password: pgConfig.password,
|
|
||||||
database: pgConfig.database,
|
|
||||||
ssl: pgConfig.ssl ? { rejectUnauthorized: false } : false,
|
|
||||||
extra: {
|
|
||||||
ssl: pgConfig.ssl ? { rejectUnauthorized: false } : undefined,
|
|
||||||
sslmode: pgConfig.sslmode,
|
|
||||||
channelBinding: pgConfig.channelBinding
|
|
||||||
},
|
|
||||||
entities: [
|
|
||||||
StudentEntity,
|
|
||||||
ReasonEntity,
|
|
||||||
ScoreEventEntity,
|
|
||||||
SettlementEntity,
|
|
||||||
SettingEntity,
|
|
||||||
TagEntity,
|
|
||||||
StudentTagEntity
|
|
||||||
],
|
|
||||||
migrations,
|
|
||||||
synchronize: false,
|
|
||||||
logging: false,
|
|
||||||
poolSize: 10,
|
|
||||||
connectTimeoutMS: 30000
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
this._isPostgreSQL = false
|
|
||||||
const dbDir = path.dirname(this.dbPath)
|
|
||||||
if (!fs.existsSync(dbDir)) {
|
|
||||||
fs.mkdirSync(dbDir, { recursive: true })
|
|
||||||
}
|
|
||||||
return new DataSource({
|
|
||||||
type: 'better-sqlite3',
|
|
||||||
database: this.dbPath,
|
|
||||||
entities: [
|
|
||||||
StudentEntity,
|
|
||||||
ReasonEntity,
|
|
||||||
ScoreEventEntity,
|
|
||||||
SettlementEntity,
|
|
||||||
SettingEntity,
|
|
||||||
TagEntity,
|
|
||||||
StudentTagEntity
|
|
||||||
],
|
|
||||||
migrations,
|
|
||||||
synchronize: false,
|
|
||||||
logging: false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async initialize(pgConnectionString?: string) {
|
|
||||||
if (this._dataSource?.isInitialized) {
|
|
||||||
await this.dispose()
|
|
||||||
}
|
|
||||||
|
|
||||||
this._dataSource = this.createDataSource(pgConnectionString)
|
|
||||||
await this._dataSource.initialize()
|
|
||||||
|
|
||||||
if (!this._isPostgreSQL) {
|
|
||||||
await this._dataSource.query('PRAGMA foreign_keys = ON')
|
|
||||||
}
|
|
||||||
|
|
||||||
await this._dataSource.runMigrations()
|
|
||||||
}
|
|
||||||
|
|
||||||
async dispose() {
|
|
||||||
if (!this._dataSource?.isInitialized) return
|
|
||||||
await this._dataSource.destroy()
|
|
||||||
this._dataSource = null
|
|
||||||
}
|
|
||||||
|
|
||||||
async switchConnection(pgConnectionString?: string): Promise<{ type: 'sqlite' | 'postgresql' }> {
|
|
||||||
await this.dispose()
|
|
||||||
await this.initialize(pgConnectionString)
|
|
||||||
return { type: this._isPostgreSQL ? 'postgresql' : 'sqlite' }
|
|
||||||
}
|
|
||||||
|
|
||||||
getDatabaseType(): 'sqlite' | 'postgresql' {
|
|
||||||
return this._isPostgreSQL ? 'postgresql' : 'sqlite'
|
|
||||||
}
|
|
||||||
|
|
||||||
async enqueueOperation<T>(operation: () => Promise<T>): Promise<T> {
|
|
||||||
return new Promise<T>((resolve, reject) => {
|
|
||||||
const item: QueueItem = {
|
|
||||||
id: `op-${++this.queueIdCounter}`,
|
|
||||||
operation,
|
|
||||||
resolve: resolve as (value: any) => void,
|
|
||||||
reject,
|
|
||||||
timestamp: Date.now()
|
|
||||||
}
|
|
||||||
|
|
||||||
this.operationQueue.push(item)
|
|
||||||
this.processQueue()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private async processQueue() {
|
|
||||||
if (this.isProcessingQueue || this.operationQueue.length === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isProcessingQueue = true
|
|
||||||
|
|
||||||
while (this.operationQueue.length > 0) {
|
|
||||||
const item = this.operationQueue.shift()!
|
|
||||||
try {
|
|
||||||
const result = await item.operation()
|
|
||||||
item.resolve(result)
|
|
||||||
} catch (error) {
|
|
||||||
item.reject(error as Error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isProcessingQueue = false
|
|
||||||
}
|
|
||||||
|
|
||||||
async withQueue<T>(operation: () => Promise<T>): Promise<T> {
|
|
||||||
if (this._isPostgreSQL) {
|
|
||||||
return this.enqueueOperation(operation)
|
|
||||||
}
|
|
||||||
return operation()
|
|
||||||
}
|
|
||||||
|
|
||||||
async syncToRemote(): Promise<{ success: boolean; message?: string }> {
|
|
||||||
if (!this._isPostgreSQL) {
|
|
||||||
return { success: false, message: '当前不是远程数据库模式' }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this._dataSource?.isInitialized) {
|
|
||||||
return { success: false, message: '数据库未初始化' }
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await this.enqueueOperation(async () => {
|
|
||||||
await this._dataSource!.query('SELECT 1')
|
|
||||||
return { success: true, message: '同步成功' }
|
|
||||||
})
|
|
||||||
} catch (error: any) {
|
|
||||||
return { success: false, message: error?.message || '同步失败' }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
import type { DataSource } from 'typeorm'
|
|
||||||
import {
|
|
||||||
ReasonEntity,
|
|
||||||
ScoreEventEntity,
|
|
||||||
SettlementEntity,
|
|
||||||
SettingEntity,
|
|
||||||
StudentEntity
|
|
||||||
} from '../entities'
|
|
||||||
|
|
||||||
type exportBundle = {
|
|
||||||
students: StudentEntity[]
|
|
||||||
reasons: ReasonEntity[]
|
|
||||||
events: ScoreEventEntity[]
|
|
||||||
settlements: SettlementEntity[]
|
|
||||||
settings: SettingEntity[]
|
|
||||||
}
|
|
||||||
|
|
||||||
type importResult = { success: true } | { success: false; message: string }
|
|
||||||
|
|
||||||
export class DataBackupRepository {
|
|
||||||
constructor(private readonly dataSource: DataSource) {}
|
|
||||||
|
|
||||||
async exportJson(): Promise<string> {
|
|
||||||
const bundle = await this.exportBundle()
|
|
||||||
return JSON.stringify(bundle, null, 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
private async exportBundle(): Promise<exportBundle> {
|
|
||||||
const students = await this.dataSource.getRepository(StudentEntity).find()
|
|
||||||
const reasons = await this.dataSource.getRepository(ReasonEntity).find()
|
|
||||||
const events = await this.dataSource.getRepository(ScoreEventEntity).find()
|
|
||||||
const settlements = await this.dataSource
|
|
||||||
.getRepository(SettlementEntity)
|
|
||||||
.find({ order: { id: 'ASC' } })
|
|
||||||
const settings = await this.dataSource
|
|
||||||
.getRepository(SettingEntity)
|
|
||||||
.createQueryBuilder('s')
|
|
||||||
.where("s.key NOT LIKE 'security_%'")
|
|
||||||
.getMany()
|
|
||||||
return { students, reasons, events, settlements, settings }
|
|
||||||
}
|
|
||||||
|
|
||||||
async importJson(jsonText: string): Promise<importResult> {
|
|
||||||
let parsed: any
|
|
||||||
try {
|
|
||||||
parsed = JSON.parse(String(jsonText ?? ''))
|
|
||||||
} catch {
|
|
||||||
return { success: false, message: 'Invalid JSON' }
|
|
||||||
}
|
|
||||||
|
|
||||||
const students = Array.isArray(parsed?.students) ? parsed.students : []
|
|
||||||
const reasons = Array.isArray(parsed?.reasons) ? parsed.reasons : []
|
|
||||||
const events = Array.isArray(parsed?.events) ? parsed.events : []
|
|
||||||
const settlements = Array.isArray(parsed?.settlements) ? parsed.settlements : []
|
|
||||||
const settings = Array.isArray(parsed?.settings) ? parsed.settings : []
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.dataSource.transaction(async (manager) => {
|
|
||||||
await manager.clear(ScoreEventEntity)
|
|
||||||
await manager.clear(SettlementEntity)
|
|
||||||
await manager.clear(StudentEntity)
|
|
||||||
await manager.clear(ReasonEntity)
|
|
||||||
await manager
|
|
||||||
.getRepository(SettingEntity)
|
|
||||||
.createQueryBuilder()
|
|
||||||
.delete()
|
|
||||||
.where("key NOT LIKE 'security_%'")
|
|
||||||
.execute()
|
|
||||||
|
|
||||||
const insertStudents: Partial<StudentEntity>[] = []
|
|
||||||
for (const s of students) {
|
|
||||||
const name = String(s?.name ?? '').trim()
|
|
||||||
if (!name) continue
|
|
||||||
const score = Number(s?.score ?? 0)
|
|
||||||
const extraJson = s?.extra_json != null ? String(s.extra_json) : null
|
|
||||||
const createdAt = s?.created_at != null ? String(s.created_at) : new Date().toISOString()
|
|
||||||
const updatedAt = s?.updated_at != null ? String(s.updated_at) : new Date().toISOString()
|
|
||||||
insertStudents.push({
|
|
||||||
name,
|
|
||||||
score: Number.isFinite(score) ? score : 0,
|
|
||||||
extra_json: extraJson,
|
|
||||||
created_at: createdAt,
|
|
||||||
updated_at: updatedAt
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (insertStudents.length) {
|
|
||||||
await manager.getRepository(StudentEntity).insert(insertStudents)
|
|
||||||
}
|
|
||||||
|
|
||||||
const insertReasons: Partial<ReasonEntity>[] = []
|
|
||||||
for (const r of reasons) {
|
|
||||||
const content = String(r?.content ?? '').trim()
|
|
||||||
if (!content) continue
|
|
||||||
const category = String(r?.category ?? '其他')
|
|
||||||
const delta = Number(r?.delta ?? 0)
|
|
||||||
const isSystem = Number(r?.is_system ?? 0) ? 1 : 0
|
|
||||||
const updatedAt = r?.updated_at != null ? String(r.updated_at) : new Date().toISOString()
|
|
||||||
insertReasons.push({
|
|
||||||
content,
|
|
||||||
category,
|
|
||||||
delta: Number.isFinite(delta) ? delta : 0,
|
|
||||||
is_system: isSystem,
|
|
||||||
updated_at: updatedAt
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (insertReasons.length) {
|
|
||||||
await manager.getRepository(ReasonEntity).insert(insertReasons)
|
|
||||||
}
|
|
||||||
|
|
||||||
const insertSettlements: Partial<SettlementEntity>[] = []
|
|
||||||
for (const s of settlements) {
|
|
||||||
const id = Number(s?.id)
|
|
||||||
const startTime = String(s?.start_time ?? '').trim()
|
|
||||||
const endTime = String(s?.end_time ?? '').trim()
|
|
||||||
const createdAt = String(s?.created_at ?? new Date().toISOString())
|
|
||||||
if (!Number.isFinite(id) || !startTime || !endTime) continue
|
|
||||||
insertSettlements.push({
|
|
||||||
id,
|
|
||||||
start_time: startTime,
|
|
||||||
end_time: endTime,
|
|
||||||
created_at: createdAt
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (insertSettlements.length) {
|
|
||||||
await manager.getRepository(SettlementEntity).insert(insertSettlements)
|
|
||||||
}
|
|
||||||
|
|
||||||
const insertEvents: Partial<ScoreEventEntity>[] = []
|
|
||||||
for (const e of events) {
|
|
||||||
const uuid = String(e?.uuid ?? '').trim()
|
|
||||||
const studentName = String(e?.student_name ?? '').trim()
|
|
||||||
const reasonContent = String(e?.reason_content ?? '').trim()
|
|
||||||
if (!uuid || !studentName || !reasonContent) continue
|
|
||||||
const delta = Number(e?.delta ?? 0)
|
|
||||||
const valPrev = Number(e?.val_prev ?? 0)
|
|
||||||
const valCurr = Number(e?.val_curr ?? 0)
|
|
||||||
const eventTime = String(e?.event_time ?? new Date().toISOString())
|
|
||||||
const settlementIdRaw = e?.settlement_id
|
|
||||||
const settlementId =
|
|
||||||
settlementIdRaw === null || settlementIdRaw === undefined
|
|
||||||
? null
|
|
||||||
: Number(settlementIdRaw)
|
|
||||||
insertEvents.push({
|
|
||||||
uuid,
|
|
||||||
student_name: studentName,
|
|
||||||
reason_content: reasonContent,
|
|
||||||
delta: Number.isFinite(delta) ? delta : 0,
|
|
||||||
val_prev: Number.isFinite(valPrev) ? valPrev : 0,
|
|
||||||
val_curr: Number.isFinite(valCurr) ? valCurr : 0,
|
|
||||||
event_time: eventTime,
|
|
||||||
settlement_id: Number.isFinite(settlementId as any) ? (settlementId as any) : null
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (insertEvents.length) {
|
|
||||||
await manager.getRepository(ScoreEventEntity).insert(insertEvents)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const it of settings) {
|
|
||||||
const key = String(it?.key ?? '').trim()
|
|
||||||
if (!key || key.startsWith('security_')) continue
|
|
||||||
await manager.getRepository(SettingEntity).save({ key, value: String(it?.value ?? '') })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} catch (e: any) {
|
|
||||||
return { success: false, message: e?.message || 'Import failed' }
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
export { StudentEntity } from './entities/StudentEntity'
|
|
||||||
export { ReasonEntity } from './entities/ReasonEntity'
|
|
||||||
export { ScoreEventEntity } from './entities/ScoreEventEntity'
|
|
||||||
export { SettlementEntity } from './entities/SettlementEntity'
|
|
||||||
export { SettingEntity } from './entities/SettingEntity'
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'
|
|
||||||
|
|
||||||
@Entity({ name: 'reasons' })
|
|
||||||
export class ReasonEntity {
|
|
||||||
@PrimaryGeneratedColumn()
|
|
||||||
id!: number
|
|
||||||
|
|
||||||
@Index({ unique: true })
|
|
||||||
@Column({ type: 'text' })
|
|
||||||
content!: string
|
|
||||||
|
|
||||||
@Column({ type: 'text', default: '其他' })
|
|
||||||
category!: string
|
|
||||||
|
|
||||||
@Column({ type: 'integer' })
|
|
||||||
delta!: number
|
|
||||||
|
|
||||||
@Column({ type: 'integer', default: 0 })
|
|
||||||
is_system!: number
|
|
||||||
|
|
||||||
@Column({ type: 'text', default: () => 'CURRENT_TIMESTAMP' })
|
|
||||||
updated_at!: string
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'
|
|
||||||
|
|
||||||
@Entity({ name: 'score_events' })
|
|
||||||
export class ScoreEventEntity {
|
|
||||||
@PrimaryGeneratedColumn()
|
|
||||||
id!: number
|
|
||||||
|
|
||||||
@Index({ unique: true })
|
|
||||||
@Column({ type: 'text' })
|
|
||||||
uuid!: string
|
|
||||||
|
|
||||||
@Column({ type: 'text' })
|
|
||||||
student_name!: string
|
|
||||||
|
|
||||||
@Column({ type: 'text' })
|
|
||||||
reason_content!: string
|
|
||||||
|
|
||||||
@Column({ type: 'integer' })
|
|
||||||
delta!: number
|
|
||||||
|
|
||||||
@Column({ type: 'integer' })
|
|
||||||
val_prev!: number
|
|
||||||
|
|
||||||
@Column({ type: 'integer' })
|
|
||||||
val_curr!: number
|
|
||||||
|
|
||||||
@Column({ type: 'text', default: () => 'CURRENT_TIMESTAMP' })
|
|
||||||
event_time!: string
|
|
||||||
|
|
||||||
@Index()
|
|
||||||
@Column({ type: 'integer', nullable: true })
|
|
||||||
settlement_id!: number | null
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { Column, Entity, PrimaryColumn } from 'typeorm'
|
|
||||||
|
|
||||||
@Entity({ name: 'settings' })
|
|
||||||
export class SettingEntity {
|
|
||||||
@PrimaryColumn({ type: 'text' })
|
|
||||||
key!: string
|
|
||||||
|
|
||||||
@Column({ type: 'text', nullable: true })
|
|
||||||
value!: string | null
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'
|
|
||||||
|
|
||||||
@Entity({ name: 'settlements' })
|
|
||||||
export class SettlementEntity {
|
|
||||||
@PrimaryGeneratedColumn()
|
|
||||||
id!: number
|
|
||||||
|
|
||||||
@Column({ type: 'text' })
|
|
||||||
start_time!: string
|
|
||||||
|
|
||||||
@Column({ type: 'text' })
|
|
||||||
end_time!: string
|
|
||||||
|
|
||||||
@Column({ type: 'text', default: () => 'CURRENT_TIMESTAMP' })
|
|
||||||
created_at!: string
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { Column, Entity, PrimaryGeneratedColumn, ManyToMany, JoinTable } from 'typeorm'
|
|
||||||
import { TagEntity } from './TagEntity'
|
|
||||||
|
|
||||||
@Entity({ name: 'students' })
|
|
||||||
export class StudentEntity {
|
|
||||||
@PrimaryGeneratedColumn()
|
|
||||||
id!: number
|
|
||||||
|
|
||||||
@Column({ type: 'text' })
|
|
||||||
name!: string
|
|
||||||
|
|
||||||
@Column({ type: 'text', default: '[]' })
|
|
||||||
tags!: string
|
|
||||||
|
|
||||||
@Column({ type: 'integer', default: 0 })
|
|
||||||
score!: number
|
|
||||||
|
|
||||||
@Column({ type: 'text', nullable: true })
|
|
||||||
extra_json!: string | null
|
|
||||||
|
|
||||||
@Column({ type: 'text', default: () => 'CURRENT_TIMESTAMP' })
|
|
||||||
created_at!: string
|
|
||||||
|
|
||||||
@Column({ type: 'text', default: () => 'CURRENT_TIMESTAMP' })
|
|
||||||
updated_at!: string
|
|
||||||
|
|
||||||
@ManyToMany(() => TagEntity, { cascade: true })
|
|
||||||
@JoinTable({
|
|
||||||
name: 'student_tags',
|
|
||||||
joinColumn: { name: 'student_id', referencedColumnName: 'id' },
|
|
||||||
inverseJoinColumn: { name: 'tag_id', referencedColumnName: 'id' }
|
|
||||||
})
|
|
||||||
tagEntities!: TagEntity[]
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import { Column, Entity, PrimaryGeneratedColumn, ManyToOne, JoinColumn, Index } from 'typeorm'
|
|
||||||
import { StudentEntity } from './StudentEntity'
|
|
||||||
import { TagEntity } from './TagEntity'
|
|
||||||
|
|
||||||
@Entity({ name: 'student_tags' })
|
|
||||||
@Index(['student_id', 'tag_id'], { unique: true })
|
|
||||||
export class StudentTagEntity {
|
|
||||||
@PrimaryGeneratedColumn()
|
|
||||||
id!: number
|
|
||||||
|
|
||||||
@Column({ name: 'student_id', type: 'integer' })
|
|
||||||
student_id!: number
|
|
||||||
|
|
||||||
@Column({ name: 'tag_id', type: 'integer' })
|
|
||||||
tag_id!: number
|
|
||||||
|
|
||||||
@ManyToOne(() => StudentEntity, { onDelete: 'CASCADE' })
|
|
||||||
@JoinColumn({ name: 'student_id' })
|
|
||||||
student!: StudentEntity
|
|
||||||
|
|
||||||
@ManyToOne(() => TagEntity, { onDelete: 'CASCADE' })
|
|
||||||
@JoinColumn({ name: 'tag_id' })
|
|
||||||
tag!: TagEntity
|
|
||||||
|
|
||||||
@Column({ type: 'text', default: () => 'CURRENT_TIMESTAMP' })
|
|
||||||
created_at!: string
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'
|
|
||||||
|
|
||||||
@Entity({ name: 'tags' })
|
|
||||||
export class TagEntity {
|
|
||||||
@PrimaryGeneratedColumn()
|
|
||||||
id!: number
|
|
||||||
|
|
||||||
@Column({ type: 'text', unique: true })
|
|
||||||
name!: string
|
|
||||||
|
|
||||||
@Column({ type: 'text', default: () => 'CURRENT_TIMESTAMP' })
|
|
||||||
created_at!: string
|
|
||||||
|
|
||||||
@Column({ type: 'text', default: () => 'CURRENT_TIMESTAMP' })
|
|
||||||
updated_at!: string
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export { StudentEntity } from './StudentEntity'
|
|
||||||
export { ReasonEntity } from './ReasonEntity'
|
|
||||||
export { ScoreEventEntity } from './ScoreEventEntity'
|
|
||||||
export { SettlementEntity } from './SettlementEntity'
|
|
||||||
export { SettingEntity } from './SettingEntity'
|
|
||||||
export { TagEntity } from './TagEntity'
|
|
||||||
export { StudentTagEntity } from './StudentTagEntity'
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
import type { MigrationInterface, QueryRunner } from 'typeorm'
|
|
||||||
|
|
||||||
export class InitSchema2026011800000 implements MigrationInterface {
|
|
||||||
name = 'InitSchema2026011800000'
|
|
||||||
|
|
||||||
private isPostgres(queryRunner: QueryRunner): boolean {
|
|
||||||
return queryRunner.connection.options.type === 'postgres'
|
|
||||||
}
|
|
||||||
|
|
||||||
async up(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
const isPg = this.isPostgres(queryRunner)
|
|
||||||
const pkSerial = isPg ? 'SERIAL PRIMARY KEY' : 'integer PRIMARY KEY AUTOINCREMENT'
|
|
||||||
const now = isPg ? 'CURRENT_TIMESTAMP' : '(CURRENT_TIMESTAMP)'
|
|
||||||
const defaultZero = isPg ? 'DEFAULT 0' : 'DEFAULT (0)'
|
|
||||||
const defaultEmptyArr = isPg ? "DEFAULT '[]'" : "DEFAULT '[]'"
|
|
||||||
|
|
||||||
if (!(await queryRunner.hasTable('students'))) {
|
|
||||||
await queryRunner.query(`
|
|
||||||
CREATE TABLE "students" (
|
|
||||||
"id" ${pkSerial} NOT NULL,
|
|
||||||
"name" text NOT NULL,
|
|
||||||
"score" integer NOT NULL ${defaultZero},
|
|
||||||
"extra_json" text,
|
|
||||||
"tags" text NOT NULL ${defaultEmptyArr},
|
|
||||||
"created_at" text NOT NULL DEFAULT ${now},
|
|
||||||
"updated_at" text NOT NULL DEFAULT ${now}
|
|
||||||
)
|
|
||||||
`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(await queryRunner.hasTable('reasons'))) {
|
|
||||||
await queryRunner.query(`
|
|
||||||
CREATE TABLE "reasons" (
|
|
||||||
"id" ${pkSerial} NOT NULL,
|
|
||||||
"content" text NOT NULL,
|
|
||||||
"category" text NOT NULL DEFAULT '其他',
|
|
||||||
"delta" integer NOT NULL,
|
|
||||||
"is_system" integer NOT NULL ${defaultZero},
|
|
||||||
"updated_at" text NOT NULL DEFAULT ${now},
|
|
||||||
CONSTRAINT "UQ_reasons_content" UNIQUE ("content")
|
|
||||||
)
|
|
||||||
`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(await queryRunner.hasTable('settlements'))) {
|
|
||||||
await queryRunner.query(`
|
|
||||||
CREATE TABLE "settlements" (
|
|
||||||
"id" ${pkSerial} NOT NULL,
|
|
||||||
"start_time" text NOT NULL,
|
|
||||||
"end_time" text NOT NULL,
|
|
||||||
"created_at" text NOT NULL DEFAULT ${now}
|
|
||||||
)
|
|
||||||
`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(await queryRunner.hasTable('score_events'))) {
|
|
||||||
await queryRunner.query(`
|
|
||||||
CREATE TABLE "score_events" (
|
|
||||||
"id" ${pkSerial} NOT NULL,
|
|
||||||
"uuid" text NOT NULL,
|
|
||||||
"student_name" text NOT NULL,
|
|
||||||
"reason_content" text NOT NULL,
|
|
||||||
"delta" integer NOT NULL,
|
|
||||||
"val_prev" integer NOT NULL,
|
|
||||||
"val_curr" integer NOT NULL,
|
|
||||||
"event_time" text NOT NULL DEFAULT ${now},
|
|
||||||
"settlement_id" integer,
|
|
||||||
CONSTRAINT "UQ_score_events_uuid" UNIQUE ("uuid")
|
|
||||||
)
|
|
||||||
`)
|
|
||||||
}
|
|
||||||
await queryRunner.query(
|
|
||||||
`CREATE INDEX IF NOT EXISTS "IDX_score_events_settlement_id" ON "score_events" ("settlement_id")`
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!(await queryRunner.hasTable('settings'))) {
|
|
||||||
await queryRunner.query(`
|
|
||||||
CREATE TABLE "settings" (
|
|
||||||
"key" text PRIMARY KEY NOT NULL,
|
|
||||||
"value" text
|
|
||||||
)
|
|
||||||
`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const reasonValues = `('上课发言','课堂表现',1,1,CURRENT_TIMESTAMP),('作业优秀','作业情况',2,1,CURRENT_TIMESTAMP),('纪律良好','纪律',1,1,CURRENT_TIMESTAMP),('帮助同学','品德',2,1,CURRENT_TIMESTAMP),('迟到','纪律',-1,1,CURRENT_TIMESTAMP),('未交作业','作业情况',-2,1,CURRENT_TIMESTAMP)`
|
|
||||||
|
|
||||||
if (isPg) {
|
|
||||||
await queryRunner.query(`
|
|
||||||
INSERT INTO "reasons" ("content","category","delta","is_system","updated_at") VALUES
|
|
||||||
${reasonValues}
|
|
||||||
ON CONFLICT DO NOTHING
|
|
||||||
`)
|
|
||||||
} else {
|
|
||||||
await queryRunner.query(`
|
|
||||||
INSERT OR IGNORE INTO "reasons" ("content","category","delta","is_system","updated_at") VALUES
|
|
||||||
${reasonValues}
|
|
||||||
`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(await queryRunner.hasTable('tags'))) {
|
|
||||||
await queryRunner.query(`
|
|
||||||
CREATE TABLE "tags" (
|
|
||||||
"id" ${pkSerial} NOT NULL,
|
|
||||||
"name" text NOT NULL UNIQUE,
|
|
||||||
"created_at" text NOT NULL DEFAULT ${now},
|
|
||||||
"updated_at" text NOT NULL DEFAULT ${now}
|
|
||||||
)
|
|
||||||
`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(await queryRunner.hasTable('student_tags'))) {
|
|
||||||
await queryRunner.query(`
|
|
||||||
CREATE TABLE "student_tags" (
|
|
||||||
"id" ${pkSerial} NOT NULL,
|
|
||||||
"student_id" integer NOT NULL,
|
|
||||||
"tag_id" integer NOT NULL,
|
|
||||||
"created_at" text NOT NULL DEFAULT ${now},
|
|
||||||
FOREIGN KEY ("student_id") REFERENCES "students"("id") ON DELETE CASCADE,
|
|
||||||
FOREIGN KEY ("tag_id") REFERENCES "tags"("id") ON DELETE CASCADE,
|
|
||||||
UNIQUE("student_id", "tag_id")
|
|
||||||
)
|
|
||||||
`)
|
|
||||||
}
|
|
||||||
|
|
||||||
await queryRunner.query(
|
|
||||||
`CREATE INDEX IF NOT EXISTS "IDX_student_tags_student_id" ON "student_tags" ("student_id")`
|
|
||||||
)
|
|
||||||
await queryRunner.query(
|
|
||||||
`CREATE INDEX IF NOT EXISTS "IDX_student_tags_tag_id" ON "student_tags" ("tag_id")`
|
|
||||||
)
|
|
||||||
|
|
||||||
const tagValues = `('优秀生'),('班干部'),('勤奋'),('活跃'),('进步快')`
|
|
||||||
|
|
||||||
if (isPg) {
|
|
||||||
await queryRunner.query(`
|
|
||||||
INSERT INTO "tags" ("name") VALUES ${tagValues}
|
|
||||||
ON CONFLICT DO NOTHING
|
|
||||||
`)
|
|
||||||
} else {
|
|
||||||
await queryRunner.query(`
|
|
||||||
INSERT OR IGNORE INTO "tags" ("name") VALUES ${tagValues}
|
|
||||||
`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async down(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
await queryRunner.query(`DROP TABLE IF EXISTS "score_events"`)
|
|
||||||
await queryRunner.query(`DROP TABLE IF EXISTS "settlements"`)
|
|
||||||
await queryRunner.query(`DROP TABLE IF EXISTS "students"`)
|
|
||||||
await queryRunner.query(`DROP TABLE IF EXISTS "reasons"`)
|
|
||||||
await queryRunner.query(`DROP TABLE IF EXISTS "settings"`)
|
|
||||||
await queryRunner.query(`DROP TABLE IF EXISTS "student_tags"`)
|
|
||||||
await queryRunner.query(`DROP TABLE IF EXISTS "tags"`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import { InitSchema2026011800000 } from './InitSchema2026011800000'
|
|
||||||
|
|
||||||
export const migrations = [InitSchema2026011800000]
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
import type {
|
|
||||||
configureHostDelegate,
|
|
||||||
hostApplicationContext,
|
|
||||||
hostBuilderContext,
|
|
||||||
hostedService,
|
|
||||||
middleware,
|
|
||||||
serviceToken
|
|
||||||
} from './types'
|
|
||||||
import { ServiceProvider } from './serviceCollection'
|
|
||||||
|
|
||||||
export class HostApplication {
|
|
||||||
private hostedInstances: hostedService[] = []
|
|
||||||
private started = false
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly context: hostBuilderContext,
|
|
||||||
private readonly provider: ServiceProvider,
|
|
||||||
private readonly configureDelegates: configureHostDelegate[],
|
|
||||||
private readonly middleware: middleware[],
|
|
||||||
private readonly hostedTokens: serviceToken<hostedService>[]
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async start(): Promise<void> {
|
|
||||||
if (this.started) return
|
|
||||||
|
|
||||||
const appCtx = this.createApplicationContext()
|
|
||||||
for (const configure of this.configureDelegates) {
|
|
||||||
await configure(this.context, appCtx)
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.dispatch(0, appCtx, async () => {
|
|
||||||
await this.bootstrapHostedServices()
|
|
||||||
})
|
|
||||||
|
|
||||||
this.started = true
|
|
||||||
await this.context.lifetime.notifyStarted()
|
|
||||||
}
|
|
||||||
|
|
||||||
async stop(): Promise<void> {
|
|
||||||
if (!this.started) return
|
|
||||||
await this.context.lifetime.notifyStopping()
|
|
||||||
await this.disposeHostedServices()
|
|
||||||
await this.context.lifetime.notifyStopped()
|
|
||||||
this.started = false
|
|
||||||
}
|
|
||||||
|
|
||||||
async dispose(): Promise<void> {
|
|
||||||
await this.stop()
|
|
||||||
await this.provider.dispose()
|
|
||||||
}
|
|
||||||
|
|
||||||
get services(): ServiceProvider {
|
|
||||||
return this.provider
|
|
||||||
}
|
|
||||||
|
|
||||||
get hostContext(): hostBuilderContext {
|
|
||||||
return this.context
|
|
||||||
}
|
|
||||||
|
|
||||||
private createApplicationContext(): hostApplicationContext {
|
|
||||||
return { services: this.provider, host: this.context }
|
|
||||||
}
|
|
||||||
|
|
||||||
private async bootstrapHostedServices() {
|
|
||||||
for (const token of this.hostedTokens) {
|
|
||||||
const service = this.provider.get(token)
|
|
||||||
this.hostedInstances.push(service)
|
|
||||||
await service.start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async disposeHostedServices() {
|
|
||||||
while (this.hostedInstances.length) {
|
|
||||||
const service = this.hostedInstances.pop()
|
|
||||||
if (!service) continue
|
|
||||||
await service.stop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async dispatch(
|
|
||||||
index: number,
|
|
||||||
appCtx: hostApplicationContext,
|
|
||||||
terminal: () => Promise<void>
|
|
||||||
) {
|
|
||||||
const middleware = this.middleware[index]
|
|
||||||
if (!middleware) {
|
|
||||||
await terminal()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await middleware(appCtx, () => this.dispatch(index + 1, appCtx, terminal))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
import { ServiceCollection } from './serviceCollection'
|
|
||||||
import { HostApplication } from './hostApplication'
|
|
||||||
import {
|
|
||||||
type appRuntimeContext,
|
|
||||||
type configureHostDelegate,
|
|
||||||
type configureServicesDelegate,
|
|
||||||
type hostBuilderContext,
|
|
||||||
type hostBuilderSettings,
|
|
||||||
type hostedService,
|
|
||||||
type middleware,
|
|
||||||
type serviceToken
|
|
||||||
} from './types'
|
|
||||||
|
|
||||||
export class SecScoreHostBuilder {
|
|
||||||
private readonly serviceCollection: ServiceCollection
|
|
||||||
private readonly configureServicesDelegates: configureServicesDelegate[] = []
|
|
||||||
private readonly configureDelegates: configureHostDelegate[] = []
|
|
||||||
private readonly middleware: middleware[] = []
|
|
||||||
private readonly hostedServices: serviceToken<hostedService>[] = []
|
|
||||||
private readonly builderContext: hostBuilderContext
|
|
||||||
|
|
||||||
constructor(runtimeCtx: appRuntimeContext, settings: hostBuilderSettings = {}) {
|
|
||||||
this.serviceCollection = new ServiceCollection(runtimeCtx)
|
|
||||||
this.builderContext = {
|
|
||||||
ctx: runtimeCtx,
|
|
||||||
environmentName: settings.environment ?? process.env.SECSCORE_ENV ?? 'Production',
|
|
||||||
properties: new Map(Object.entries(settings.properties ?? {})),
|
|
||||||
lifetime: this.serviceCollection.getLifetime()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get context(): hostBuilderContext {
|
|
||||||
return this.builderContext
|
|
||||||
}
|
|
||||||
|
|
||||||
configureServices(callback: configureServicesDelegate): this {
|
|
||||||
this.configureServicesDelegates.push(callback)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
configure(callback: configureHostDelegate): this {
|
|
||||||
this.configureDelegates.push(callback)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
use(middleware: middleware): this {
|
|
||||||
this.middleware.push(middleware)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
addHostedService(token: serviceToken<hostedService>): this {
|
|
||||||
this.hostedServices.push(token)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
async build(): Promise<SecScoreHost> {
|
|
||||||
for (const configure of this.configureServicesDelegates) {
|
|
||||||
await configure(this.builderContext, this.serviceCollection)
|
|
||||||
}
|
|
||||||
|
|
||||||
const provider = this.serviceCollection.buildServiceProvider()
|
|
||||||
const application = new HostApplication(
|
|
||||||
this.builderContext,
|
|
||||||
provider,
|
|
||||||
this.configureDelegates,
|
|
||||||
this.middleware,
|
|
||||||
this.hostedServices
|
|
||||||
)
|
|
||||||
return new SecScoreHost(application)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SecScoreHost {
|
|
||||||
constructor(private readonly app: HostApplication) {}
|
|
||||||
|
|
||||||
get services() {
|
|
||||||
return this.app.services
|
|
||||||
}
|
|
||||||
|
|
||||||
get hostContext() {
|
|
||||||
return this.app.hostContext
|
|
||||||
}
|
|
||||||
|
|
||||||
async start() {
|
|
||||||
await this.app.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
async stop() {
|
|
||||||
await this.app.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
async dispose() {
|
|
||||||
await this.app.dispose()
|
|
||||||
}
|
|
||||||
|
|
||||||
async run() {
|
|
||||||
await this.start()
|
|
||||||
return async () => {
|
|
||||||
await this.dispose()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createHostBuilder(ctx: appRuntimeContext, settings?: hostBuilderSettings) {
|
|
||||||
return new SecScoreHostBuilder(ctx, settings)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Host = {
|
|
||||||
createApplicationBuilder: createHostBuilder
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
export { SecScoreHostBuilder, SecScoreHost, createHostBuilder, Host } from './hostBuilder'
|
|
||||||
export { ServiceCollection, ServiceProvider } from './serviceCollection'
|
|
||||||
export {
|
|
||||||
HostApplicationLifetimeToken,
|
|
||||||
AppConfigToken,
|
|
||||||
LoggerToken,
|
|
||||||
DbManagerToken,
|
|
||||||
SettingsStoreToken,
|
|
||||||
SecurityServiceToken,
|
|
||||||
PermissionServiceToken,
|
|
||||||
StudentRepositoryToken,
|
|
||||||
ReasonRepositoryToken,
|
|
||||||
EventRepositoryToken,
|
|
||||||
SettlementRepositoryToken,
|
|
||||||
TagRepositoryToken,
|
|
||||||
ThemeServiceToken,
|
|
||||||
WindowManagerToken,
|
|
||||||
TrayServiceToken,
|
|
||||||
AutoScoreServiceToken
|
|
||||||
} from './tokens'
|
|
||||||
export type {
|
|
||||||
appRuntimeContext,
|
|
||||||
hostedService,
|
|
||||||
middleware,
|
|
||||||
hostBuilderSettings,
|
|
||||||
hostBuilderContext,
|
|
||||||
configureServicesDelegate,
|
|
||||||
configureHostDelegate,
|
|
||||||
hostApplicationLifetime,
|
|
||||||
hostApplicationContext,
|
|
||||||
serviceToken
|
|
||||||
} from './types'
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import type { awaitable, disposer, hostApplicationLifetime } from './types'
|
|
||||||
|
|
||||||
type loggerLike = { error: (...args: any[]) => void }
|
|
||||||
|
|
||||||
// 默认的应用生命周期实现,管理启动/停止事件
|
|
||||||
export class DefaultHostApplicationLifetime implements hostApplicationLifetime {
|
|
||||||
constructor(private readonly logger: loggerLike = console) {}
|
|
||||||
private readonly started = new Set<() => awaitable<void>>() // 启动事件处理器
|
|
||||||
private readonly stopping = new Set<() => awaitable<void>>() // 停止事件处理器
|
|
||||||
private readonly stopped = new Set<() => awaitable<void>>() // 已停止事件处理器
|
|
||||||
|
|
||||||
// 注册启动事件处理器
|
|
||||||
onStarted(handler: () => awaitable<void>): disposer {
|
|
||||||
this.started.add(handler)
|
|
||||||
return () => {
|
|
||||||
this.started.delete(handler)
|
|
||||||
} // 返回清理函数
|
|
||||||
}
|
|
||||||
|
|
||||||
// 注册停止事件处理器
|
|
||||||
onStopping(handler: () => awaitable<void>): disposer {
|
|
||||||
this.stopping.add(handler)
|
|
||||||
return () => {
|
|
||||||
this.stopping.delete(handler)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 注册已停止事件处理器
|
|
||||||
onStopped(handler: () => awaitable<void>): disposer {
|
|
||||||
this.stopped.add(handler)
|
|
||||||
return () => {
|
|
||||||
this.stopped.delete(handler)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 通知所有启动事件处理器
|
|
||||||
async notifyStarted() {
|
|
||||||
await this.dispatch(this.started)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 通知所有停止事件处理器
|
|
||||||
async notifyStopping() {
|
|
||||||
await this.dispatch(this.stopping)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 通知所有已停止事件处理器
|
|
||||||
async notifyStopped() {
|
|
||||||
await this.dispatch(this.stopped)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 执行事件处理器列表
|
|
||||||
private async dispatch(targets: Set<() => awaitable<void>>) {
|
|
||||||
for (const handler of Array.from(targets)) {
|
|
||||||
try {
|
|
||||||
await handler()
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error('[HostLifetime] handler failed', error as Error) // 记录错误但不中断
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,216 +0,0 @@
|
|||||||
import { HostApplicationLifetimeToken } from './tokens'
|
|
||||||
import { DefaultHostApplicationLifetime } from './lifetime'
|
|
||||||
import type {
|
|
||||||
appRuntimeContext,
|
|
||||||
disposer,
|
|
||||||
injectableClass,
|
|
||||||
serviceDescriptor,
|
|
||||||
serviceFactory,
|
|
||||||
serviceFactoryOrValue,
|
|
||||||
serviceLifetime,
|
|
||||||
serviceToken
|
|
||||||
} from './types'
|
|
||||||
|
|
||||||
// 检查值是否为构造函数
|
|
||||||
function isConstructor<T>(value: serviceFactoryOrValue<T>): value is injectableClass<T> {
|
|
||||||
return (
|
|
||||||
typeof value === 'function' &&
|
|
||||||
!!(value as any).prototype &&
|
|
||||||
(value as any).prototype.constructor === value
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 服务集合类,管理所有注册的服务描述符
|
|
||||||
export class ServiceCollection {
|
|
||||||
private readonly descriptors = new Map<serviceToken, serviceDescriptor>() // 服务描述符映射
|
|
||||||
private readonly hostLifetime: DefaultHostApplicationLifetime // 应用生命周期管理器
|
|
||||||
|
|
||||||
constructor(private readonly ctx: appRuntimeContext) {
|
|
||||||
this.hostLifetime = new DefaultHostApplicationLifetime(this.ctx.logger ?? console)
|
|
||||||
this.addSingleton(HostApplicationLifetimeToken, () => this.hostLifetime)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取生命周期管理器
|
|
||||||
getLifetime() {
|
|
||||||
return this.hostLifetime
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加单例服务
|
|
||||||
addSingleton<T>(token: serviceToken<T>, impl: serviceFactoryOrValue<T>): this {
|
|
||||||
return this.register(token, 'singleton', impl)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加作用域服务
|
|
||||||
addScoped<T>(token: serviceToken<T>, impl: serviceFactoryOrValue<T>): this {
|
|
||||||
return this.register(token, 'scoped', impl)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加瞬时服务
|
|
||||||
addTransient<T>(token: serviceToken<T>, impl: serviceFactoryOrValue<T>): this {
|
|
||||||
return this.register(token, 'transient', impl)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 尝试添加单例服务(如果不存在)
|
|
||||||
tryAddSingleton<T>(token: serviceToken<T>, impl: serviceFactoryOrValue<T>): this {
|
|
||||||
if (!this.descriptors.has(token)) {
|
|
||||||
this.addSingleton(token, impl)
|
|
||||||
}
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查服务是否已注册
|
|
||||||
has(token: serviceToken): boolean {
|
|
||||||
return this.descriptors.has(token)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清空所有服务
|
|
||||||
clear(): void {
|
|
||||||
this.descriptors.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建服务提供者
|
|
||||||
buildServiceProvider(): ServiceProvider {
|
|
||||||
return new ServiceProvider(this.ctx, new Map(this.descriptors))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 注册服务
|
|
||||||
private register<T>(
|
|
||||||
token: serviceToken<T>,
|
|
||||||
lifetime: serviceLifetime,
|
|
||||||
impl: serviceFactoryOrValue<T>
|
|
||||||
): this {
|
|
||||||
const descriptor: serviceDescriptor = {
|
|
||||||
token,
|
|
||||||
lifetime,
|
|
||||||
factory: this.normalizeFactory(impl) // 标准化工厂函数
|
|
||||||
}
|
|
||||||
this.descriptors.set(token, descriptor)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
// 标准化工厂函数
|
|
||||||
private normalizeFactory<T>(impl: serviceFactoryOrValue<T>): serviceFactory<T> {
|
|
||||||
if (isConstructor(impl)) {
|
|
||||||
return (provider) => this.instantiateClass(impl, provider) // 构造函数,实例化类
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof impl === 'function') {
|
|
||||||
return impl as serviceFactory<T> // 已经是工厂函数
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => impl // 直接值,返回常量
|
|
||||||
}
|
|
||||||
|
|
||||||
// 实例化类,注入依赖
|
|
||||||
private instantiateClass<T>(Ctor: injectableClass<T>, provider: ServiceProvider): T {
|
|
||||||
const deps = [...(Ctor.inject ?? [])].map((token) => provider.get(token)) // 获取依赖
|
|
||||||
return new Ctor(...deps) // 构造实例
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 服务提供者类,负责解析和提供服务实例
|
|
||||||
export class ServiceProvider {
|
|
||||||
private readonly singletonCache: Map<serviceToken, unknown> // 单例缓存
|
|
||||||
private readonly scopedCache = new Map<serviceToken, unknown>() // 作用域缓存
|
|
||||||
private readonly singletonCleanup: disposer[] // 单例清理函数
|
|
||||||
private readonly scopedCleanup: disposer[] = [] // 作用域清理函数
|
|
||||||
private readonly root: ServiceProvider | null // 根提供者
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly ctx: appRuntimeContext,
|
|
||||||
private readonly descriptors: Map<serviceToken, serviceDescriptor>,
|
|
||||||
root: ServiceProvider | null = null,
|
|
||||||
private readonly isScope = false // 是否为作用域提供者
|
|
||||||
) {
|
|
||||||
this.root = root
|
|
||||||
this.singletonCache = root ? root.singletonCache : new Map() // 共享单例缓存
|
|
||||||
this.singletonCleanup = root ? root.singletonCleanup : []
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取服务实例
|
|
||||||
get<T>(token: serviceToken<T>): T {
|
|
||||||
const descriptor = this.descriptors.get(token) as serviceDescriptor<T> | undefined
|
|
||||||
if (!descriptor) {
|
|
||||||
throw new Error(`Service not registered for token: ${token.toString?.() ?? String(token)}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (descriptor.lifetime) {
|
|
||||||
case 'singleton':
|
|
||||||
return this.resolveSingleton(descriptor)
|
|
||||||
case 'scoped':
|
|
||||||
return this.resolveScoped(descriptor)
|
|
||||||
case 'transient':
|
|
||||||
default:
|
|
||||||
return this.instantiate(descriptor) // 每次都新实例
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建作用域提供者
|
|
||||||
createScope(): ServiceProvider {
|
|
||||||
return new ServiceProvider(this.ctx, this.descriptors, this.root ?? this, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 释放资源
|
|
||||||
async dispose(): Promise<void> {
|
|
||||||
await this.flushCleanup(this.scopedCleanup) // 先清理作用域
|
|
||||||
if (!this.isScope) {
|
|
||||||
await this.flushCleanup(this.singletonCleanup) // 再清理单例
|
|
||||||
this.singletonCache.clear()
|
|
||||||
}
|
|
||||||
this.scopedCache.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析单例服务
|
|
||||||
private resolveSingleton<T>(descriptor: serviceDescriptor<T>): T {
|
|
||||||
const cacheOwner = this.root ?? this
|
|
||||||
if (cacheOwner.singletonCache.has(descriptor.token)) {
|
|
||||||
return cacheOwner.singletonCache.get(descriptor.token) as T
|
|
||||||
}
|
|
||||||
const instance = this.instantiate(descriptor)
|
|
||||||
cacheOwner.singletonCache.set(descriptor.token, instance)
|
|
||||||
const disposer = this.extractDisposer(instance) // 提取清理函数
|
|
||||||
if (disposer) cacheOwner.singletonCleanup.push(disposer)
|
|
||||||
return instance
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析作用域服务
|
|
||||||
private resolveScoped<T>(descriptor: serviceDescriptor<T>): T {
|
|
||||||
if (this.scopedCache.has(descriptor.token)) {
|
|
||||||
return this.scopedCache.get(descriptor.token) as T
|
|
||||||
}
|
|
||||||
const instance = this.instantiate(descriptor)
|
|
||||||
this.scopedCache.set(descriptor.token, instance)
|
|
||||||
const disposer = this.extractDisposer(instance)
|
|
||||||
if (disposer) this.scopedCleanup.push(disposer)
|
|
||||||
return instance
|
|
||||||
}
|
|
||||||
|
|
||||||
// 实例化服务
|
|
||||||
private instantiate<T>(descriptor: serviceDescriptor<T>): T {
|
|
||||||
return descriptor.factory(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提取实例的清理函数
|
|
||||||
private extractDisposer(instance: unknown): disposer | null {
|
|
||||||
if (!instance) return null
|
|
||||||
if (typeof (instance as any).dispose === 'function') {
|
|
||||||
return () => (instance as any).dispose()
|
|
||||||
}
|
|
||||||
if (typeof (instance as any).destroy === 'function') {
|
|
||||||
return () => (instance as any).destroy()
|
|
||||||
}
|
|
||||||
if (typeof (instance as any).stop === 'function') {
|
|
||||||
return () => (instance as any).stop()
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// 执行清理函数列表
|
|
||||||
private async flushCleanup(cleanup: disposer[]) {
|
|
||||||
while (cleanup.length) {
|
|
||||||
const disposer = cleanup.pop()
|
|
||||||
if (!disposer) continue
|
|
||||||
await disposer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
export const HostApplicationLifetimeToken = Symbol.for('secscore.hosting.lifetime')
|
|
||||||
|
|
||||||
export const AppConfigToken = Symbol.for('secscore.app.config')
|
|
||||||
export const LoggerToken = Symbol.for('secscore.logger')
|
|
||||||
export const DbManagerToken = Symbol.for('secscore.dbManager')
|
|
||||||
export const SettingsStoreToken = Symbol.for('secscore.settingsStore')
|
|
||||||
export const SecurityServiceToken = Symbol.for('secscore.securityService')
|
|
||||||
export const PermissionServiceToken = Symbol.for('secscore.permissionService')
|
|
||||||
export const StudentRepositoryToken = Symbol.for('secscore.studentRepository')
|
|
||||||
export const ReasonRepositoryToken = Symbol.for('secscore.reasonRepository')
|
|
||||||
export const EventRepositoryToken = Symbol.for('secscore.eventRepository')
|
|
||||||
export const SettlementRepositoryToken = Symbol.for('secscore.settlementRepository')
|
|
||||||
export const TagRepositoryToken = Symbol.for('secscore.tagRepository')
|
|
||||||
export const ThemeServiceToken = Symbol.for('secscore.themeService')
|
|
||||||
export const WindowManagerToken = Symbol.for('secscore.windowManager')
|
|
||||||
export const TrayServiceToken = Symbol.for('secscore.trayService')
|
|
||||||
export const AutoScoreServiceToken = Symbol.for('secscore.autoScoreService')
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
import type { ServiceCollection, ServiceProvider } from './serviceCollection'
|
|
||||||
|
|
||||||
export type awaitable<T> = T | Promise<T>
|
|
||||||
export type disposer = () => awaitable<void>
|
|
||||||
|
|
||||||
export type serviceToken<T = unknown> = string | symbol | (new (...args: any[]) => T)
|
|
||||||
|
|
||||||
export interface injectableClass<T = unknown> {
|
|
||||||
new (...args: any[]): T
|
|
||||||
inject?: readonly serviceToken[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export type serviceFactory<T> = (provider: ServiceProvider) => T
|
|
||||||
export type serviceFactoryOrValue<T> = serviceFactory<T> | injectableClass<T> | T
|
|
||||||
|
|
||||||
export type serviceLifetime = 'singleton' | 'scoped' | 'transient'
|
|
||||||
|
|
||||||
export interface serviceDescriptor<T = unknown> {
|
|
||||||
token: serviceToken<T>
|
|
||||||
lifetime: serviceLifetime
|
|
||||||
factory: serviceFactory<T>
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface hostedService {
|
|
||||||
start(): awaitable<void>
|
|
||||||
stop(): awaitable<void>
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface hostBuilderSettings {
|
|
||||||
environment?: string
|
|
||||||
properties?: Record<string, unknown>
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface hostApplicationLifetime {
|
|
||||||
onStarted(handler: () => awaitable<void>): disposer
|
|
||||||
onStopping(handler: () => awaitable<void>): disposer
|
|
||||||
onStopped(handler: () => awaitable<void>): disposer
|
|
||||||
notifyStarted(): Promise<void>
|
|
||||||
notifyStopping(): Promise<void>
|
|
||||||
notifyStopped(): Promise<void>
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface appRuntimeContext {
|
|
||||||
logger?: { error: (...args: any[]) => void }
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface hostBuilderContext {
|
|
||||||
ctx: appRuntimeContext
|
|
||||||
environmentName: string
|
|
||||||
properties: Map<string | symbol, unknown>
|
|
||||||
lifetime: hostApplicationLifetime
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface hostApplicationContext {
|
|
||||||
services: ServiceProvider
|
|
||||||
host: hostBuilderContext
|
|
||||||
}
|
|
||||||
|
|
||||||
export type configureServicesDelegate = (
|
|
||||||
context: hostBuilderContext,
|
|
||||||
services: ServiceCollection
|
|
||||||
) => awaitable<void>
|
|
||||||
|
|
||||||
export type configureHostDelegate = (
|
|
||||||
context: hostBuilderContext,
|
|
||||||
app: hostApplicationContext
|
|
||||||
) => awaitable<void>
|
|
||||||
|
|
||||||
export type middleware = (app: hostApplicationContext, next: () => Promise<void>) => awaitable<void>
|
|
||||||
|
|
||||||
export type { ServiceCollection, ServiceProvider }
|
|
||||||
@@ -1,367 +0,0 @@
|
|||||||
import 'reflect-metadata'
|
|
||||||
import { app } from 'electron'
|
|
||||||
import { join, dirname } from 'path'
|
|
||||||
import fs from 'fs'
|
|
||||||
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
|
|
||||||
import icon from '../../resources/SecScore_logo.ico?asset'
|
|
||||||
import { MainContext } from './context'
|
|
||||||
import { DbManager } from './db/DbManager'
|
|
||||||
import { LoggerService } from './services/LoggerService'
|
|
||||||
import { SettingsService } from './services/SettingsService'
|
|
||||||
import { SecurityService } from './services/SecurityService'
|
|
||||||
import { PermissionService } from './services/PermissionService'
|
|
||||||
import { AuthService } from './services/AuthService'
|
|
||||||
import { DataService } from './services/DataService'
|
|
||||||
import { ThemeService } from './services/ThemeService'
|
|
||||||
import { WindowManager, type windowManagerOptions } from './services/WindowManager'
|
|
||||||
import { TrayService } from './services/TrayService'
|
|
||||||
import { AutoScoreService } from './services/AutoScoreService'
|
|
||||||
import { DbConnectionService } from './services/DbConnectionService'
|
|
||||||
import { StudentRepository } from './repos/StudentRepository'
|
|
||||||
import { ReasonRepository } from './repos/ReasonRepository'
|
|
||||||
import { EventRepository } from './repos/EventRepository'
|
|
||||||
import { SettlementRepository } from './repos/SettlementRepository'
|
|
||||||
import { TagRepository } from './repos/TagRepository'
|
|
||||||
import {
|
|
||||||
AppConfigToken,
|
|
||||||
createHostBuilder,
|
|
||||||
DbManagerToken,
|
|
||||||
EventRepositoryToken,
|
|
||||||
LoggerToken,
|
|
||||||
PermissionServiceToken,
|
|
||||||
ReasonRepositoryToken,
|
|
||||||
SecurityServiceToken,
|
|
||||||
SettlementRepositoryToken,
|
|
||||||
SettingsStoreToken,
|
|
||||||
StudentRepositoryToken,
|
|
||||||
TagRepositoryToken,
|
|
||||||
ThemeServiceToken,
|
|
||||||
WindowManagerToken,
|
|
||||||
TrayServiceToken,
|
|
||||||
AutoScoreServiceToken
|
|
||||||
} from './hosting'
|
|
||||||
|
|
||||||
type mainAppConfig = {
|
|
||||||
isDev: boolean
|
|
||||||
appRoot: string
|
|
||||||
dataRoot: string
|
|
||||||
configDir: string
|
|
||||||
logDir: string
|
|
||||||
dbPath: string
|
|
||||||
pgConnectionString?: string
|
|
||||||
window: windowManagerOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
const PROTOCOL_SCHEME = 'secscore'
|
|
||||||
|
|
||||||
let mainCtxRef: MainContext | null = null
|
|
||||||
let pendingProtocolUrl: string | null = null
|
|
||||||
|
|
||||||
const extractProtocolUrl = (argv: string[]): string | null => {
|
|
||||||
const prefix = `${PROTOCOL_SCHEME}://`
|
|
||||||
const lowerPrefix = prefix.toLowerCase()
|
|
||||||
for (const arg of argv) {
|
|
||||||
if (typeof arg !== 'string') continue
|
|
||||||
const v = arg.trim()
|
|
||||||
if (!v) continue
|
|
||||||
const lower = v.toLowerCase()
|
|
||||||
if (lower.startsWith(lowerPrefix)) return v
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const openMainRoute = (ctx: MainContext, route: string) => {
|
|
||||||
ctx.windows.open({
|
|
||||||
key: 'main',
|
|
||||||
title: 'SecScore',
|
|
||||||
route
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleProtocolUrl = (rawUrl: string, ctx: MainContext) => {
|
|
||||||
if (!rawUrl) return
|
|
||||||
let s = rawUrl.trim()
|
|
||||||
if (!s) return
|
|
||||||
const prefix = `${PROTOCOL_SCHEME}://`
|
|
||||||
if (s.toLowerCase().startsWith(prefix)) {
|
|
||||||
s = s.slice(prefix.length)
|
|
||||||
}
|
|
||||||
s = s.replace(/^\/+/, '')
|
|
||||||
if (!s) {
|
|
||||||
openMainRoute(ctx, '/')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const parts = s.split('/')
|
|
||||||
const head = parts[0]?.toLowerCase() ?? ''
|
|
||||||
if (!head) {
|
|
||||||
openMainRoute(ctx, '/')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (head === 'home') {
|
|
||||||
openMainRoute(ctx, '/')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (head === 'students') {
|
|
||||||
openMainRoute(ctx, '/students')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (head === 'score') {
|
|
||||||
openMainRoute(ctx, '/score')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (head === 'leaderboard') {
|
|
||||||
openMainRoute(ctx, '/leaderboard')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (head === 'settlements') {
|
|
||||||
openMainRoute(ctx, '/settlements')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (head === 'reasons') {
|
|
||||||
openMainRoute(ctx, '/reasons')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (head === 'settings') {
|
|
||||||
openMainRoute(ctx, '/settings')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasSingleInstanceLock = app.requestSingleInstanceLock()
|
|
||||||
|
|
||||||
if (!hasSingleInstanceLock) {
|
|
||||||
app.quit()
|
|
||||||
} else {
|
|
||||||
const initialUrl = extractProtocolUrl(process.argv)
|
|
||||||
if (initialUrl) {
|
|
||||||
pendingProtocolUrl = initialUrl
|
|
||||||
}
|
|
||||||
app.on('second-instance', (event, argv) => {
|
|
||||||
event.preventDefault()
|
|
||||||
const url = extractProtocolUrl(argv)
|
|
||||||
if (!url) return
|
|
||||||
if (mainCtxRef) {
|
|
||||||
handleProtocolUrl(url, mainCtxRef)
|
|
||||||
} else {
|
|
||||||
pendingProtocolUrl = url
|
|
||||||
}
|
|
||||||
})
|
|
||||||
app.on('open-url', (event, url) => {
|
|
||||||
event.preventDefault()
|
|
||||||
if (mainCtxRef) {
|
|
||||||
handleProtocolUrl(url, mainCtxRef)
|
|
||||||
} else {
|
|
||||||
pendingProtocolUrl = url
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
app.whenReady().then(async () => {
|
|
||||||
electronApp.setAppUserModelId('com.electron')
|
|
||||||
|
|
||||||
if (!is.dev) {
|
|
||||||
app.setAsDefaultProtocolClient(PROTOCOL_SCHEME)
|
|
||||||
}
|
|
||||||
|
|
||||||
app.on('browser-window-created', (_, window) => {
|
|
||||||
optimizer.watchWindowShortcuts(window)
|
|
||||||
})
|
|
||||||
|
|
||||||
const appRoot = is.dev ? process.cwd() : dirname(process.execPath)
|
|
||||||
|
|
||||||
const ensureWritableDir = (preferred: string, fallback: string) => {
|
|
||||||
try {
|
|
||||||
if (!fs.existsSync(preferred)) fs.mkdirSync(preferred, { recursive: true })
|
|
||||||
return preferred
|
|
||||||
} catch {
|
|
||||||
if (!fs.existsSync(fallback)) fs.mkdirSync(fallback, { recursive: true })
|
|
||||||
return fallback
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const dataRoot = is.dev
|
|
||||||
? process.cwd()
|
|
||||||
: ensureWritableDir(join(appRoot, 'data'), join(app.getPath('userData'), 'secscore-data'))
|
|
||||||
|
|
||||||
const logDir = is.dev ? join(process.cwd(), 'logs') : join(dataRoot, 'logs')
|
|
||||||
const configDir = is.dev ? join(process.cwd(), 'configs') : join(dataRoot, 'configs')
|
|
||||||
const dbPath = is.dev ? join(process.cwd(), 'db.sqlite') : join(dataRoot, 'db.sqlite')
|
|
||||||
|
|
||||||
const pgConnectionString = process.env['PG_CONNECTION_STRING'] || undefined
|
|
||||||
|
|
||||||
const config: mainAppConfig = {
|
|
||||||
isDev: is.dev,
|
|
||||||
appRoot,
|
|
||||||
dataRoot,
|
|
||||||
logDir,
|
|
||||||
configDir,
|
|
||||||
dbPath,
|
|
||||||
pgConnectionString,
|
|
||||||
window: {
|
|
||||||
icon,
|
|
||||||
preloadPath: join(__dirname, '../preload/index.js'),
|
|
||||||
rendererHtmlPath: join(__dirname, '../renderer/index.html'),
|
|
||||||
getRendererUrl: () => (is.dev ? process.env['ELECTRON_RENDERER_URL'] : undefined)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const builder = createHostBuilder({
|
|
||||||
logger: {
|
|
||||||
error: (...args: any[]) => {
|
|
||||||
try {
|
|
||||||
process.stderr.write(`${args.map((a) => String(a)).join(' ')}\n`)
|
|
||||||
} catch {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.configureServices(async (_builderContext, services) => {
|
|
||||||
services.addSingleton(AppConfigToken, config)
|
|
||||||
|
|
||||||
services.addSingleton(MainContext, () => new MainContext())
|
|
||||||
|
|
||||||
services.addSingleton(
|
|
||||||
LoggerToken,
|
|
||||||
(p) => new LoggerService(p.get(MainContext), config.logDir)
|
|
||||||
)
|
|
||||||
services.addSingleton(
|
|
||||||
DbManagerToken,
|
|
||||||
(p) => new DbManager(p.get(MainContext), config.dbPath, config.pgConnectionString)
|
|
||||||
)
|
|
||||||
services.addSingleton(SettingsStoreToken, (p) => new SettingsService(p.get(MainContext)))
|
|
||||||
services.addSingleton(SecurityServiceToken, (p) => new SecurityService(p.get(MainContext)))
|
|
||||||
services.addSingleton(
|
|
||||||
PermissionServiceToken,
|
|
||||||
(p) => new PermissionService(p.get(MainContext))
|
|
||||||
)
|
|
||||||
services.addSingleton(AuthService, (p) => new AuthService(p.get(MainContext)))
|
|
||||||
services.addSingleton(
|
|
||||||
DataService,
|
|
||||||
(p) => new DataService(p.get(MainContext), p.get(TagRepositoryToken))
|
|
||||||
)
|
|
||||||
|
|
||||||
services.addSingleton(
|
|
||||||
StudentRepositoryToken,
|
|
||||||
(p) => new StudentRepository(p.get(MainContext))
|
|
||||||
)
|
|
||||||
services.addSingleton(ReasonRepositoryToken, (p) => new ReasonRepository(p.get(MainContext)))
|
|
||||||
services.addSingleton(EventRepositoryToken, (p) => new EventRepository(p.get(MainContext)))
|
|
||||||
services.addSingleton(
|
|
||||||
SettlementRepositoryToken,
|
|
||||||
(p) => new SettlementRepository(p.get(MainContext))
|
|
||||||
)
|
|
||||||
services.addSingleton(
|
|
||||||
TagRepositoryToken,
|
|
||||||
(p) => new TagRepository((p.get(DbManagerToken) as DbManager).dataSource)
|
|
||||||
)
|
|
||||||
|
|
||||||
services.addSingleton(ThemeServiceToken, (p) => new ThemeService(p.get(MainContext)))
|
|
||||||
services.addSingleton(
|
|
||||||
WindowManagerToken,
|
|
||||||
(p) => new WindowManager(p.get(MainContext), config.window)
|
|
||||||
)
|
|
||||||
services.addSingleton(
|
|
||||||
TrayServiceToken,
|
|
||||||
(p) => new TrayService(p.get(MainContext), config.window)
|
|
||||||
)
|
|
||||||
services.addSingleton(AutoScoreServiceToken, (p) => new AutoScoreService(p.get(MainContext)))
|
|
||||||
services.addSingleton(DbConnectionService, (p) => new DbConnectionService(p.get(MainContext)))
|
|
||||||
})
|
|
||||||
.configure(async (_builderContext, appCtx) => {
|
|
||||||
const services = appCtx.services
|
|
||||||
services.get(LoggerToken)
|
|
||||||
// 先初始化 db(使用默认 SQLite)
|
|
||||||
const db = services.get(DbManagerToken) as DbManager
|
|
||||||
await db.initialize()
|
|
||||||
// 然后初始化 settings
|
|
||||||
const settings = services.get(SettingsStoreToken) as SettingsService
|
|
||||||
await settings.initialize()
|
|
||||||
// 检查是否需要切换到 PostgreSQL
|
|
||||||
const pgConnectionString = settings.getValue('pg_connection_string')
|
|
||||||
if (pgConnectionString) {
|
|
||||||
try {
|
|
||||||
await db.switchConnection(pgConnectionString)
|
|
||||||
await settings.setValue('pg_connection_status', {
|
|
||||||
connected: true,
|
|
||||||
type: 'postgresql'
|
|
||||||
})
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error('Failed to connect to PostgreSQL:', e)
|
|
||||||
await settings.setValue('pg_connection_status', {
|
|
||||||
connected: false,
|
|
||||||
type: 'postgresql',
|
|
||||||
error: e?.message || '连接失败'
|
|
||||||
})
|
|
||||||
// 切换回 SQLite
|
|
||||||
await db.switchConnection(undefined)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
services.get(SecurityServiceToken)
|
|
||||||
services.get(PermissionServiceToken)
|
|
||||||
services.get(AuthService)
|
|
||||||
services.get(DataService)
|
|
||||||
services.get(StudentRepositoryToken)
|
|
||||||
services.get(ReasonRepositoryToken)
|
|
||||||
services.get(EventRepositoryToken)
|
|
||||||
services.get(SettlementRepositoryToken)
|
|
||||||
const theme = services.get(
|
|
||||||
ThemeServiceToken
|
|
||||||
) as import('./services/ThemeService').ThemeService
|
|
||||||
await theme.init()
|
|
||||||
if (!process.env.HEADLESS) {
|
|
||||||
services.get(WindowManagerToken)
|
|
||||||
const tray = services.get(TrayServiceToken) as TrayService
|
|
||||||
tray.initialize()
|
|
||||||
}
|
|
||||||
const autoScore = services.get(AutoScoreServiceToken) as AutoScoreService
|
|
||||||
await autoScore.initialize?.()
|
|
||||||
services.get(DbConnectionService)
|
|
||||||
})
|
|
||||||
|
|
||||||
const host = await builder.build()
|
|
||||||
const ctx = host.services.get(MainContext) as MainContext
|
|
||||||
mainCtxRef = ctx
|
|
||||||
|
|
||||||
ctx.handle('app:register-url-protocol', async () => {
|
|
||||||
if (is.dev) {
|
|
||||||
return { success: false, message: '仅在打包后的应用中可用' }
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const ok = app.setAsDefaultProtocolClient(PROTOCOL_SCHEME)
|
|
||||||
if (ok) {
|
|
||||||
return { success: true, data: { registered: true } }
|
|
||||||
}
|
|
||||||
return { success: false, data: { registered: false }, message: '系统未接受协议注册' }
|
|
||||||
} catch (error) {
|
|
||||||
const message =
|
|
||||||
error instanceof Error ? error.message : typeof error === 'string' ? error : 'Unknown error'
|
|
||||||
return { success: false, message: `注册失败: ${message}` }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
await host.start()
|
|
||||||
|
|
||||||
if (pendingProtocolUrl) {
|
|
||||||
handleProtocolUrl(pendingProtocolUrl, ctx)
|
|
||||||
pendingProtocolUrl = null
|
|
||||||
} else {
|
|
||||||
openMainRoute(ctx, '/')
|
|
||||||
}
|
|
||||||
|
|
||||||
let disposing = false
|
|
||||||
const beforeQuitHandler = () => {
|
|
||||||
if (disposing) return
|
|
||||||
disposing = true
|
|
||||||
ctx.isQuitting = true
|
|
||||||
app.removeListener('before-quit', beforeQuitHandler)
|
|
||||||
void host.dispose()
|
|
||||||
}
|
|
||||||
app.on('before-quit', beforeQuitHandler)
|
|
||||||
})
|
|
||||||
|
|
||||||
app.on('window-all-closed', () => {
|
|
||||||
if (process.platform !== 'darwin') {
|
|
||||||
app.quit()
|
|
||||||
}
|
|
||||||
})
|
|
||||||