├── .github
└── workflows
│ ├── build.yaml
│ └── deploy.yaml
├── .gitignore
├── .prettierrc.json
├── LICENSE
├── PRIVACY.md
├── README.md
├── SECURITY.md
├── backend
├── .gitignore
├── api.go
├── auth.go
├── config.example.yaml
├── connection.go
├── github.go
├── go.mod
├── go.sum
├── main.go
├── middleware.go
├── storage.go
└── utils.go
├── env.d.ts
├── index.html
├── package.json
├── pnpm-lock.yaml
├── public
├── background.webp
├── background
│ ├── forest.webp
│ ├── hills.webp
│ ├── lake.webp
│ ├── morning.webp
│ ├── mountain.webp
│ ├── ocean.webp
│ ├── snow.webp
│ └── sunshine.webp
├── beian.webp
├── favicon.ico
├── icon.png
└── tool
│ ├── add.svg
│ ├── cloudflare.svg
│ ├── codepen.svg
│ ├── convert.svg
│ ├── github.svg
│ ├── jsdelivr.svg
│ ├── kaggle.svg
│ ├── lightnotes.ico
│ ├── openai.svg
│ ├── readthedocs.svg
│ ├── replit.svg
│ ├── stackoverflow.svg
│ ├── twitter.svg
│ ├── unknown.svg
│ └── vercel.svg
├── screenshot
├── customize.png
├── engine.png
├── i18n.png
├── main.png
├── search.png
└── settings.png
├── src
├── App.vue
├── assets
│ ├── script
│ │ ├── auth.ts
│ │ ├── card
│ │ │ ├── calendar.ts
│ │ │ ├── github.ts
│ │ │ └── weather.ts
│ │ ├── config.ts
│ │ ├── connection.ts
│ │ ├── engine.ts
│ │ ├── openai.ts
│ │ ├── shared.ts
│ │ ├── storage.ts
│ │ ├── tool.ts
│ │ └── utils
│ │ │ ├── base.ts
│ │ │ ├── scroll.ts
│ │ │ ├── service.ts
│ │ │ └── typing.ts
│ └── style
│ │ ├── base.css
│ │ ├── card
│ │ └── weather.css
│ │ └── engine.css
├── components
│ ├── About.vue
│ ├── AutoUpdater.vue
│ ├── Background.vue
│ ├── CardContainer.vue
│ ├── InputBox.vue
│ ├── Quote.vue
│ ├── SettingWindow.vue
│ ├── TimeWidget.vue
│ ├── ToolBox.vue
│ ├── cards
│ │ ├── DateCard.vue
│ │ ├── GithubCard.vue
│ │ └── WeatherCard.vue
│ ├── compositions
│ │ ├── Checkbox.vue
│ │ ├── Cover.vue
│ │ ├── Notification.vue
│ │ ├── Suggestion.vue
│ │ ├── Tool.vue
│ │ └── Window.vue
│ └── icons
│ │ ├── box.vue
│ │ ├── chat.vue
│ │ ├── check.vue
│ │ ├── clock.vue
│ │ ├── close.vue
│ │ ├── cursor.vue
│ │ ├── delete.vue
│ │ ├── edit.vue
│ │ ├── github.vue
│ │ ├── info.vue
│ │ ├── international.vue
│ │ ├── loader.vue
│ │ ├── note.vue
│ │ ├── openai.vue
│ │ ├── qq.vue
│ │ ├── search.vue
│ │ ├── settings.vue
│ │ └── star.vue
├── i18n
│ ├── engine.ts
│ └── index.ts
├── main.ts
└── types
│ ├── calendar.d.ts
│ ├── pwa.d.ts
│ └── vue.d.ts
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
/.github/workflows/build.yaml:
--------------------------------------------------------------------------------
1 | name: Build Test
2 | on:
3 | push:
4 | branches:
5 | - main
6 | jobs:
7 | release:
8 | runs-on: ubuntu-latest
9 | strategy:
10 | matrix:
11 | node-version: [ 18.x ]
12 | steps:
13 | - name: Checkout
14 | uses: actions/checkout@v3
15 |
16 | - name: Use Node.js
17 | uses: actions/setup-node@v3
18 | with:
19 | node-version: ${{ matrix.node-version }}
20 |
21 | - name: Build Frontend
22 | run: |
23 | npm install -g pnpm
24 | pnpm install
25 | pnpm build
26 |
27 | - name: Use Golang
28 | uses: actions/setup-go@v4
29 | with:
30 | go-version: '1.20'
31 |
32 | - name: Build Backend
33 | run: |
34 | cd backend
35 | go build .
36 |
37 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yaml:
--------------------------------------------------------------------------------
1 | name: Deploy
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | permissions:
9 | contents: write
10 |
11 | jobs:
12 | deploy:
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - name: Checkout code
17 | uses: actions/checkout@v2
18 |
19 | - name: Set up Node.js
20 | uses: actions/setup-node@v2
21 | with:
22 | node-version: '18'
23 |
24 | - name: Cache node modules
25 | uses: actions/cache@v2
26 | with:
27 | path: ~/.pnpm-store
28 | key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
29 | restore-keys: |
30 | ${{ runner.os }}-pnpm-
31 |
32 | - name: Install pnpm
33 | run: npm install -g pnpm
34 |
35 | - name: Install dependencies
36 | run: pnpm install
37 |
38 | - name: Build project
39 | run: pnpm build
40 |
41 | - name: Deploy
42 | uses: JamesIves/github-pages-deploy-action@v4
43 | with:
44 | folder: dist
45 | branch: pages
46 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | .DS_Store
12 | dist
13 | dist-ssr
14 | dev-dist
15 | coverage
16 | *.local
17 |
18 | /cypress/videos/
19 | /cypress/screenshots/
20 |
21 | # Editor directories and files
22 | .vscode/*
23 | !.vscode/extensions.json
24 | .idea
25 | *.suo
26 | *.ntvs*
27 | *.njsproj
28 | *.sln
29 | *.sw?
30 |
31 | package-lock.json
32 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/PRIVACY.md:
--------------------------------------------------------------------------------
1 | # Privacy Policy
2 |
3 | Welcome to our fystart. We value your privacy and are committed to protecting your personal information. Please read this privacy policy carefully to understand how we collect, use, and protect your information.
4 |
5 | ## Information Collection
6 |
7 | Our browser start page utilizes a third-party provided by OpenAI, for the search feature. When you use the search functionality, your search queries and related information will be sent to the OpenAI service to provide search results and relevant suggestions. We do not associate this information with any personally identifiable information.
8 |
9 | However, we provide customizable options, and you can choose to disable this feature by adjusting the settings in your browser.
10 |
11 | Please note that OpenAI is an independent third-party service, and its use and protection of personal information are governed by its own privacy policy. We recommend that you read and understand their privacy policy before using the OpenAI service.
12 |
13 | ## Log Data
14 |
15 | We may collect log data about your usage of the browser start page. This log data may include your IP address, access time, browser type and version, operating system information, and more. We use this log data for the improvement, troubleshooting, and security monitoring of the browser start page. It is not associated with any personally identifiable information.
16 |
17 | ## Third-Party Services
18 |
19 | Our browser start page may contain links or content from third-party websites or services. Please note that these third-party sites or services have their own privacy policies, and we are not responsible for their actions and practices. Before accessing these third-party links or using their services, please read and understand their privacy policies.
20 |
21 | ## Security
22 |
23 | We have implemented reasonable security measures to protect the information you provide to us. We use local storage technology to enhance user experience and functionality instead of cookies. Local storage is a method of storing data in your browser that offers higher security compared to cookies.
24 |
25 | Please note that while we take security measures to protect your information, the internet is not an entirely secure environment, and we cannot guarantee the absolute security of information transmitted over the internet.
26 |
27 | ## Changes
28 |
29 | We may update this privacy policy from time to time. The updated privacy policy will be posted on our browser start page and will replace any previous versions. We recommend that you review this privacy policy periodically to understand how we handle information.
30 |
31 | ## Contact Us
32 |
33 | If you have any questions or concerns about this privacy policy or our information handling practices, please contact us.
34 |
35 | Thank you for using our fystart!
36 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | 
4 | # [Fyrrum Start · 极目起始页](https://fystart.com/)
5 |
6 | [](https://fystart.com)
7 | [](https://fystart.com)
8 | [](https://fystart.com)
9 | [](https://fystart.com)
10 |
11 |
12 |
13 | > [!note]
14 | > ## 此项目已停止开发,新起始页:[冰糖桌面 (bingtang.com)](https://bingtang.com)!
15 |
16 |
17 | ## Features | 功能
18 | - 🍏 一言
19 | - 🍏 Quotes
20 | - 🎈 天气
21 | - 🎈 Weather
22 | - 🍊 日历
23 | - 🍊 Calendar
24 | - 🍋 自定义设置 (账号自动同步)
25 | - 🍋 Customizable Settings (Account Auto Sync)
26 | - 🍎 AI 搜索建议 (基于 Chat Nio)
27 | - 🍎 AI Search Suggestions (Powered by Chat Nio)
28 | - 🍉 翻译 / Github 搜索
29 | - 🍉 Translation / GitHub Search
30 | - 🍇 工具箱
31 | - 🍇 Tool Box
32 | - 🍐 搜索引擎建议
33 | - 🍐 Search Engine Suggestions
34 | - 🍑 账号管理
35 | - 🍑 Account Management
36 | - 🎃 PWA 应用
37 | - 🎃 PWA Application
38 | - ✨ 离线访问 (Service Worker)
39 | - ✨ Offline Requests (Service Worker)
40 | - ⚡ 搜索引擎优化
41 | - ⚡ SEO (Search Engine Optimization)
42 | - ❤ 国际化支持
43 | - ❤ i18n (Internationalization) Support
44 | - ✔ 🇨🇳 简体中文 (Simplified Chinese)
45 | - ✔ 🇨🇳 🇹🇼 繁體中文 (Traditional Chinese)
46 | - ✔ 🇺🇸 English (United States)
47 | - ✔ 🇷🇺 Русский (Russian)
48 | - ✔ 🇫🇷 Français (French)
49 | - ✔ 🇯🇵 日本語 (Japanese)
50 |
51 |
52 | > [!warning]
53 | > 由于和风天气插件产品于2024年5月1日不再提供服务, Fystart 已关闭天气组件!
54 |
55 |
56 | ## ScreenShot | 快照
57 | 
58 |
59 | 
60 |
61 | 
62 |
63 | 
64 |
65 | 
66 |
67 | 
68 |
69 |
70 | ### Get Started | 开始
71 | npm (yarn, pnpm)
72 | ```shell
73 | npm install
74 | npm run dev
75 |
76 | cd backend
77 | go run .
78 | ```
79 |
80 | ### Configuration | 配置
81 | /src/assets/script/config.ts
82 | ```ts
83 | export const deploy = true;
84 | export let endpoint = "https://api.fystart.com";
85 | export let openai_endpoint = "wss://api.chatnio.net";
86 | export const qweather = "...";
87 |
88 | if (!deploy) endpoint = "http://localhost:8001";
89 | ```
90 |
91 | /backend/config.yaml
92 | ```yaml
93 | debug: true
94 | github:
95 | endpoint: https://api.github.com
96 | token: "ghp_..."
97 |
98 | redis:
99 | host: "localhost"
100 | port: 6379
101 | password: ""
102 | db: 0
103 | ```
104 |
105 | ### Build | 构建
106 | ```shell
107 | npm run build
108 | cd backend && go build .
109 | ```
110 |
111 | ### License | 开源协议
112 | [MIT](/LICENSE)
113 |
114 | ### Security Policy | 安全政策
115 | [Security Policy](/SECURITY.md)
116 |
117 | ### Privacy Policy | 隐私政策
118 | [Privacy Policy](/PRIVACY.md)
119 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Information Security Protection
4 |
5 | We take the security of your information seriously and are committed to protecting your personal data. This Security Policy outlines the security measures we have implemented to ensure the safety of your information while using the Fystart browser start page.
6 |
7 | ## Data Protection Measures
8 |
9 | We have implemented reasonable technical and organizational measures to protect your personal information from unauthorized access, use, or disclosure. Here are the security measures we have in place:
10 |
11 | 1. Access Control: We limit access to user information only to authorized personnel.
12 | 2. Data Encryption: We employ secure protocols and encryption algorithms to encrypt the transmission and storage of user information.
13 | 3. Secure Development Practices: We follow industry best practices in developing and maintaining the Fystart start page to ensure secure coding and protection against common vulnerabilities.
14 | 4. Vulnerability Management: We regularly monitor for and address any identified security vulnerabilities or threats.
15 | 5. Incident Response: We have established procedures for promptly responding to and mitigating security incidents or breaches.
16 |
17 | Please note that while we have implemented these security measures, it is important to understand that the internet is not completely secure. Therefore, we cannot guarantee the absolute security of your information. You should also take precautions to protect the security of your account and personal information while using the Fystart start page.
18 |
19 | ## Reporting Security Vulnerabilities
20 |
21 | If you discover any security vulnerabilities or potential security threats related to the Fystart browser start page, we appreciate and encourage you to report them to us promptly. Please submit any security concerns or vulnerabilities as issues on our GitHub repository. We will review and address them accordingly.
22 |
23 | To report a security issue, follow these steps:
24 |
25 | 1. Visit our GitHub repository at [fystart](https://github.com/Deeptrain-Community/fystart).
26 | 2. Go to the "Issues" tab and click on "New Issue."
27 | 3. Provide a detailed description of the security concern or vulnerability.
28 | 4. Submit the issue, and we will review and respond to it as soon as possible.
29 |
30 | ## Changes to the Policy
31 |
32 | We reserve the right to make changes to this security policy at any time. Any updates or modifications will be posted on our website or application and will replace any previous versions. We recommend checking our security policy periodically to stay informed about our security measures and any changes to the provisions.
33 |
34 | ## Contact Us
35 |
36 | If you have any questions or concerns regarding this security policy or the information security related to the Fystart browser start page, please create an issue on our GitHub repository.
37 |
38 | Thank you for using the fystart and trusting us to protect your information!
39 |
--------------------------------------------------------------------------------
/backend/.gitignore:
--------------------------------------------------------------------------------
1 | *.exe
2 | fystart
3 | config.yaml
4 |
--------------------------------------------------------------------------------
/backend/api.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "github.com/gin-gonic/gin"
7 | "github.com/go-redis/redis/v8"
8 | "github.com/spf13/viper"
9 | "net/http"
10 | "strings"
11 | "time"
12 | )
13 |
14 | type ChatGPTMessage struct {
15 | Role string `json:"role"`
16 | Content string `json:"content"`
17 | }
18 |
19 | func GetResponse(message string) (string, error) {
20 | res, err := Post("https://api.openai.com/v1/chat/completions", map[string]string{
21 | "Content-Type": "application/json",
22 | "Authorization": "Bearer " + viper.GetString("api_key"),
23 | }, map[string]interface{}{
24 | "model": "gpt-3.5-turbo-16k",
25 | "messages": []ChatGPTMessage{
26 | {
27 | Role: "user",
28 | Content: message,
29 | },
30 | },
31 | "max_tokens": 150,
32 | })
33 | if err != nil {
34 | return "", err
35 | }
36 | data := res.(map[string]interface{})["choices"].([]interface{})[0].(map[string]interface{})["message"].(map[string]interface{})["content"]
37 | return data.(string), nil
38 | }
39 |
40 | func GetResponseWithCache(c context.Context, message string) (string, error) {
41 | res, err := Cache.Get(c, fmt.Sprintf(":chatgpt:%s", message)).Result()
42 | if err != nil || len(res) == 0 {
43 | res, err := GetResponse(message)
44 | if err != nil {
45 | return "There was something wrong...", err
46 | }
47 | Cache.Set(c, fmt.Sprintf(":chatgpt:%s", message), res, time.Hour*6)
48 | return res, nil
49 | }
50 | return res, nil
51 | }
52 |
53 | func ChatGPTAPI(c *gin.Context, message string) {
54 | message = strings.TrimSpace(message)
55 | if len(message) == 0 {
56 | c.JSON(http.StatusOK, gin.H{
57 | "status": false,
58 | "message": "",
59 | "reason": "message is empty",
60 | })
61 | return
62 | }
63 | res, err := GetResponseWithCache(c, message)
64 | if err != nil {
65 | c.JSON(http.StatusOK, gin.H{
66 | "status": false,
67 | "message": res,
68 | "reason": err.Error(),
69 | })
70 | return
71 | }
72 | c.JSON(http.StatusOK, gin.H{
73 | "status": true,
74 | "message": res,
75 | "reason": "",
76 | })
77 | }
78 |
79 | func RegisterChatGPTAPI(app *gin.Engine) {
80 | app.Handle("GET", "/gpt", func(c *gin.Context) {
81 | ChatGPTAPI(c, c.Query("message"))
82 | })
83 | app.Handle("POST", "/gpt", func(c *gin.Context) {
84 | var body RequestBody
85 | if err := c.ShouldBindJSON(&body); err != nil {
86 | c.JSON(http.StatusOK, gin.H{
87 | "status": false,
88 | "message": "",
89 | "reason": "message is empty",
90 | })
91 | return
92 | }
93 | ChatGPTAPI(c, body.Message)
94 | })
95 | }
96 |
97 | func GithubExploreAPI(c *gin.Context) {
98 | resp, err := GetRandomPopularRepoWithCache(c, c.MustGet("cache").(*redis.Client))
99 |
100 | if err != nil {
101 | c.JSON(http.StatusOK, gin.H{
102 | "status": false,
103 | "data": nil,
104 | "reason": err.Error(),
105 | })
106 | return
107 | }
108 |
109 | c.JSON(http.StatusOK, gin.H{
110 | "status": true,
111 | "data": resp,
112 | "reason": "",
113 | })
114 | }
115 |
--------------------------------------------------------------------------------
/backend/auth.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "database/sql"
5 | "encoding/json"
6 | "fmt"
7 | "github.com/dgrijalva/jwt-go"
8 | "github.com/gin-gonic/gin"
9 | "github.com/go-redis/redis/v8"
10 | "github.com/spf13/viper"
11 | "net/http"
12 | "time"
13 | )
14 |
15 | type User struct {
16 | ID int64 `json:"id"`
17 | Username string `json:"username"`
18 | BindID int64 `json:"bind_id"`
19 | Password string `json:"password"`
20 | Token string `json:"token"`
21 | }
22 |
23 | type LoginForm struct {
24 | Token string `form:"token" binding:"required"`
25 | }
26 |
27 | type ValidateUserResponse struct {
28 | Status bool `json:"status" required:"true"`
29 | Username string `json:"username" required:"true"`
30 | ID int `json:"id" required:"true"`
31 | }
32 |
33 | func Validate(token string) *ValidateUserResponse {
34 | res, err := Post("https://api.deeptrain.net/app/validate", map[string]string{
35 | "Content-Type": "application/json",
36 | }, map[string]interface{}{
37 | "password": viper.GetString("auth.access"),
38 | "token": token,
39 | "hash": Sha2Encrypt(token + viper.GetString("auth.salt")),
40 | })
41 |
42 | if err != nil || res == nil || res.(map[string]interface{})["status"] == false {
43 | return nil
44 | }
45 |
46 | converter, _ := json.Marshal(res)
47 | resp, _ := Unmarshal[ValidateUserResponse](converter)
48 | return &resp
49 | }
50 |
51 | func (u *User) Validate(c *gin.Context) bool {
52 | if u.Username == "" || u.Password == "" {
53 | return false
54 | }
55 | cache := c.MustGet("cache").(*redis.Client)
56 |
57 | if password, err := cache.Get(c, fmt.Sprintf("fystart:user:%s", u.Username)).Result(); err == nil && len(password) > 0 {
58 | return u.Password == password
59 | }
60 |
61 | db := c.MustGet("db").(*sql.DB)
62 | var count int
63 | if err := db.QueryRow("SELECT COUNT(*) FROM auth WHERE username = ? AND password = ?", u.Username, u.Password).Scan(&count); err != nil || count == 0 {
64 | return false
65 | }
66 |
67 | cache.Set(c, fmt.Sprintf("fystart:user:%s", u.Username), u.Password, 30*time.Minute)
68 | return true
69 | }
70 |
71 | func (u *User) GenerateToken() string {
72 | instance := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
73 | "username": u.Username,
74 | "password": u.Password,
75 | "exp": time.Now().Add(time.Hour * 24 * 30).Unix(),
76 | })
77 | token, err := instance.SignedString([]byte(viper.GetString("secret")))
78 | if err != nil {
79 | return ""
80 | }
81 | return token
82 | }
83 |
84 | func IsUserExist(db *sql.DB, username string) bool {
85 | var count int
86 | if err := db.QueryRow("SELECT COUNT(*) FROM auth WHERE username = ?", username).Scan(&count); err != nil {
87 | return false
88 | }
89 | return count > 0
90 | }
91 |
92 | func ParseToken(c *gin.Context, token string) *User {
93 | instance, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
94 | return []byte(viper.GetString("secret")), nil
95 | })
96 | if err != nil {
97 | return nil
98 | }
99 | if claims, ok := instance.Claims.(jwt.MapClaims); ok && instance.Valid {
100 | if int64(claims["exp"].(float64)) < time.Now().Unix() {
101 | return nil
102 | }
103 | user := &User{
104 | Username: claims["username"].(string),
105 | Password: claims["password"].(string),
106 | }
107 | if !user.Validate(c) {
108 | return nil
109 | }
110 | return user
111 | }
112 | return nil
113 | }
114 |
115 | func Login(c *gin.Context, token string) (bool, string) {
116 | // DeepTrain Token Validation
117 | user := Validate(token)
118 | if user == nil {
119 | return false, ""
120 | }
121 |
122 | db := c.MustGet("db").(*sql.DB)
123 | if !IsUserExist(db, user.Username) {
124 | // register
125 | password := GenerateChar(64)
126 | _ = db.QueryRow("INSERT INTO auth (bind_id, username, token, password) VALUES (?, ?, ?, ?)",
127 | user.ID, user.Username, token, password)
128 | u := &User{
129 | Username: user.Username,
130 | Password: password,
131 | }
132 | return true, u.GenerateToken()
133 | }
134 |
135 | // login
136 | _ = db.QueryRow("UPDATE auth SET token = ? WHERE username = ?", token, user.Username)
137 | var password string
138 | err := db.QueryRow("SELECT password FROM auth WHERE username = ?", user.Username).Scan(&password)
139 | if err != nil {
140 | return false, ""
141 | }
142 | u := &User{
143 | Username: user.Username,
144 | Password: password,
145 | }
146 | return true, u.GenerateToken()
147 | }
148 |
149 | func LoginAPI(c *gin.Context) {
150 | var form LoginForm
151 | if err := c.ShouldBind(&form); err != nil {
152 | c.JSON(http.StatusOK, gin.H{
153 | "status": false,
154 | "error": "bad request",
155 | })
156 | return
157 | }
158 |
159 | state, token := Login(c, form.Token)
160 | if !state {
161 | c.JSON(http.StatusOK, gin.H{
162 | "status": false,
163 | "error": "user not found",
164 | })
165 | return
166 | }
167 | c.JSON(http.StatusOK, gin.H{
168 | "status": true,
169 | "token": token,
170 | })
171 | }
172 |
173 | func StateAPI(c *gin.Context) {
174 | username := c.MustGet("user").(string)
175 | c.JSON(http.StatusOK, gin.H{
176 | "status": len(username) != 0,
177 | "user": username,
178 | })
179 | }
180 |
--------------------------------------------------------------------------------
/backend/config.example.yaml:
--------------------------------------------------------------------------------
1 | debug: true
2 |
3 | github:
4 | endpoint: https://api.github.com
5 | token: "ghp_..."
6 |
7 | redis:
8 | host: "localhost"
9 | port: 6379
10 | password: ""
11 | db: 0
12 |
13 | mysql:
14 | host: "localhost"
15 | port: 3306
16 | user: root
17 | password: ...
18 | db: "fystart"
19 |
20 | secret: ...
21 | auth:
22 | access: ...
23 | salt: ...
24 |
--------------------------------------------------------------------------------
/backend/connection.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "database/sql"
6 | "fmt"
7 | "github.com/go-redis/redis/v8"
8 | _ "github.com/go-sql-driver/mysql"
9 | "github.com/spf13/viper"
10 | "log"
11 | )
12 |
13 | var _ *sql.DB
14 | var Cache *redis.Client
15 |
16 | func ConnectRedis() *redis.Client {
17 | // connect to redis
18 | Cache = redis.NewClient(&redis.Options{
19 | Addr: fmt.Sprintf("%s:%d", viper.GetString("redis.host"), viper.GetInt("redis.port")),
20 | Password: viper.GetString("redis.password"),
21 | DB: viper.GetInt("redis.db"),
22 | })
23 | _, err := Cache.Ping(context.Background()).Result()
24 |
25 | if err != nil {
26 | log.Fatalln("Failed to connect to Redis server: ", err)
27 | } else {
28 | log.Println("Connected to Redis server successfully")
29 | }
30 |
31 | if viper.GetBool("debug") {
32 | Cache.FlushAll(context.Background())
33 | log.Println("Flushed all cache")
34 | }
35 |
36 | return Cache
37 | }
38 |
39 | func ConnectMySQL() *sql.DB {
40 | // connect to MySQL
41 | Database, err := sql.Open("mysql", fmt.Sprintf(
42 | "%s:%s@tcp(%s:%d)/%s",
43 | viper.GetString("mysql.user"),
44 | viper.GetString("mysql.password"),
45 | viper.GetString("mysql.host"),
46 | viper.GetInt("mysql.port"),
47 | viper.GetString("mysql.db"),
48 | ))
49 | if err != nil {
50 | log.Fatalln("Failed to connect to MySQL server: ", err)
51 | } else {
52 | log.Println("Connected to MySQL server successfully")
53 | }
54 |
55 | CreateUserTable(Database)
56 | CreateStorageTable(Database)
57 | return Database
58 | }
59 |
60 | func CreateUserTable(db *sql.DB) {
61 | _, err := db.Exec(`
62 | CREATE TABLE IF NOT EXISTS auth (
63 | id INT PRIMARY KEY AUTO_INCREMENT,
64 | bind_id INT UNIQUE,
65 | username VARCHAR(24) UNIQUE,
66 | token VARCHAR(255) NOT NULL,
67 | password VARCHAR(64) NOT NULL
68 | );
69 | `)
70 | if err != nil {
71 | log.Fatal(err)
72 | }
73 | }
74 |
75 | func CreateStorageTable(db *sql.DB) {
76 | _, err := db.Exec(`
77 | CREATE TABLE IF NOT EXISTS storage (
78 | id INT PRIMARY KEY AUTO_INCREMENT,
79 | bind_id INT UNIQUE,
80 | data TEXT(65535) NOT NULL
81 | );
82 | `)
83 | if err != nil {
84 | log.Fatal(err)
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/backend/github.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "github.com/gin-gonic/gin"
7 | "github.com/go-redis/redis/v8"
8 | "github.com/spf13/viper"
9 | "math/rand"
10 | "strconv"
11 | "time"
12 | )
13 |
14 | // generate at https://github.com/ozh/github-colors/blob/master/colors.json
15 | var colors = map[string]string{"1C Enterprise": "#814CCC", "2-Dimensional Array": "#38761D", "4D": "#004289", "ABAP": "#E8274B", "ABAP CDS": "#555e25", "ActionScript": "#882B0F", "Ada": "#02f88c", "Adblock Filter List": "#800000", "Adobe Font Metrics": "#fa0f00", "Agda": "#315665", "AGS Script": "#B9D9FF", "AIDL": "#34EB6B", "AL": "#3AA2B5", "Alloy": "#64C800", "Alpine Abuild": "#0D597F", "Altium Designer": "#A89663", "AMPL": "#E6EFBB", "AngelScript": "#C7D7DC", "Ant Build System": "#A9157E", "Antlers": "#ff269e", "ANTLR": "#9DC3FF", "ApacheConf": "#d12127", "Apex": "#1797c0", "API Blueprint": "#2ACCA8", "APL": "#5A8164", "Apollo Guidance Computer": "#0B3D91", "AppleScript": "#101F1F", "Arc": "#aa2afe", "AsciiDoc": "#73a0c5", "ASL": "#d2cece", "ASP.NET": "#9400ff", "AspectJ": "#a957b0", "Assembly": "#6E4C13", "Astro": "#ff5a03", "Asymptote": "#ff0000", "ATS": "#1ac620", "Augeas": "#9CC134", "AutoHotkey": "#6594b9", "AutoIt": "#1C3552", "Avro IDL": "#0040FF", "Awk": "#c30e9b", "Ballerina": "#FF5000", "BASIC": "#ff0000", "Batchfile": "#C1F12E", "Beef": "#a52f4e", "Befunge": "#d2cece", "Berry": "#15A13C", "BibTeX": "#778899", "Bicep": "#519aba", "Bikeshed": "#5562ac", "Bison": "#6A463F", "BitBake": "#00bce4", "Blade": "#f7523f", "BlitzBasic": "#00FFAE", "BlitzMax": "#cd6400", "Bluespec": "#12223c", "Boo": "#d4bec1", "Boogie": "#c80fa0", "Brainfuck": "#2F2530", "BrighterScript": "#66AABB", "Brightscript": "#662D91", "Browserslist": "#ffd539", "C": "#555555", "C#": "#178600", "C++": "#f34b7d", "C2hs Haskell": "#d2cece", "Cabal Config": "#483465", "Cadence": "#00ef8b", "Cairo": "#ff4a48", "CameLIGO": "#3be133", "CAP CDS": "#0092d1", "Cap'n Proto": "#c42727", "CartoCSS": "#d2cece", "Ceylon": "#dfa535", "Chapel": "#8dc63f", "Charity": "#d2cece", "ChucK": "#3f8000", "Circom": "#707575", "Cirru": "#ccccff", "Clarion": "#db901e", "Clarity": "#5546ff", "Classic ASP": "#6a40fd", "Clean": "#3F85AF", "Click": "#E4E6F3", "CLIPS": "#00A300", "Clojure": "#db5855", "Closure Templates": "#0d948f", "Cloud Firestore Security Rules": "#FFA000", "CMake": "#DA3434", "COBOL": "#d2cece", "CodeQL": "#140f46", "CoffeeScript": "#244776", "ColdFusion": "#ed2cd6", "ColdFusion CFC": "#ed2cd6", "COLLADA": "#F1A42B", "Common Lisp": "#3fb68b", "Common Workflow Language": "#B5314C", "Component Pascal": "#B0CE4E", "Cool": "#d2cece", "Coq": "#d0b68c", "Crystal": "#000100", "CSON": "#244776", "Csound": "#1a1a1a", "Csound Document": "#1a1a1a", "Csound Score": "#1a1a1a", "CSS": "#563d7c", "CSV": "#237346", "Cuda": "#3A4E3A", "CUE": "#5886E1", "Curry": "#531242", "CWeb": "#00007a", "Cycript": "#d2cece", "Cypher": "#34c0eb", "Cython": "#fedf5b", "D": "#ba595e", "Dafny": "#FFEC25", "Darcs Patch": "#8eff23", "Dart": "#00B4AB", "DataWeave": "#003a52", "Debian Package Control File": "#D70751", "DenizenScript": "#FBEE96", "Dhall": "#dfafff", "DIGITAL Command Language": "#d2cece", "DirectX 3D File": "#aace60", "DM": "#447265", "Dockerfile": "#384d54", "Dogescript": "#cca760", "Dotenv": "#e5d559", "DTrace": "#d2cece", "Dylan": "#6c616e", "E": "#ccce35", "Earthly": "#2af0ff", "Easybuild": "#069406", "eC": "#913960", "Ecere Projects": "#913960", "ECL": "#8a1267", "ECLiPSe": "#001d9d", "Ecmarkup": "#eb8131", "EditorConfig": "#fff1f2", "Eiffel": "#4d6977", "EJS": "#a91e50", "Elixir": "#6e4a7e", "Elm": "#60B5CC", "Elvish": "#55BB55", "Elvish Transcript": "#55BB55", "Emacs Lisp": "#c065db", "EmberScript": "#FFF4F3", "EQ": "#a78649", "Erlang": "#B83998", "Euphoria": "#FF790B", "F#": "#b845fc", "F*": "#572e30", "Factor": "#636746", "Fancy": "#7b9db4", "Fantom": "#14253c", "Faust": "#c37240", "Fennel": "#fff3d7", "FIGlet Font": "#FFDDBB", "Filebench WML": "#F6B900", "Filterscript": "#d2cece", "fish": "#4aae47", "Fluent": "#ffcc33", "FLUX": "#88ccff", "Forth": "#341708", "Fortran": "#4d41b1", "Fortran Free Form": "#4d41b1", "FreeBasic": "#141AC9", "FreeMarker": "#0050b2", "Frege": "#00cafe", "Futhark": "#5f021f", "G-code": "#D08CF2", "Game Maker Language": "#71b417", "GAML": "#FFC766", "GAMS": "#f49a22", "GAP": "#0000cc", "GCC Machine Description": "#FFCFAB", "GDB": "#d2cece", "GDScript": "#355570", "GEDCOM": "#003058", "Gemfile.lock": "#701516", "Gemini": "#ff6900", "Genero": "#63408e", "Genero Forms": "#d8df39", "Genie": "#fb855d", "Genshi": "#951531", "Gentoo Ebuild": "#9400ff", "Gentoo Eclass": "#9400ff", "Gerber Image": "#d20b00", "Gherkin": "#5B2063", "Git Attributes": "#F44D27", "Git Config": "#F44D27", "Git Revision List": "#F44D27", "Gleam": "#ffaff3", "GLSL": "#5686a5", "Glyph": "#c1ac7f", "Gnuplot": "#f0a9f0", "Go": "#00ADD8", "Go Checksums": "#00ADD8", "Go Module": "#00ADD8", "Godot Resource": "#355570", "Golo": "#88562A", "Gosu": "#82937f", "Grace": "#615f8b", "Gradle": "#02303a", "Grammatical Framework": "#ff0000", "GraphQL": "#e10098", "Graphviz (DOT)": "#2596be", "Groovy": "#4298b8", "Groovy Server Pages": "#4298b8", "GSC": "#FF6800", "Hack": "#878787", "Haml": "#ece2a9", "Handlebars": "#f7931e", "HAProxy": "#106da9", "Harbour": "#0e60e3", "Haskell": "#5e5086", "Haxe": "#df7900", "HCL": "#844FBA", "HiveQL": "#dce200", "HLSL": "#aace60", "HOCON": "#9ff8ee", "HolyC": "#ffefaf", "hoon": "#00b171", "HTML": "#e34c26", "HTML+ECR": "#2e1052", "HTML+EEX": "#6e4a7e", "HTML+ERB": "#701516", "HTML+PHP": "#4f5d95", "HTML+Razor": "#512be4", "HTTP": "#005C9C", "HXML": "#f68712", "Hy": "#7790B2", "HyPhy": "#d2cece", "IDL": "#a3522f", "Idris": "#b30000", "Ignore List": "#000000", "IGOR Pro": "#0000cc", "ImageJ Macro": "#99AAFF", "Imba": "#16cec6", "Inform 7": "#d2cece", "INI": "#d1dbe0", "Ink": "#d2cece", "Inno Setup": "#264b99", "Io": "#a9188d", "Ioke": "#078193", "Isabelle": "#FEFE00", "Isabelle ROOT": "#FEFE00", "J": "#9EEDFF", "Janet": "#0886a5", "JAR Manifest": "#b07219", "Jasmin": "#d03600", "Java": "#b07219", "Java Properties": "#2A6277", "Java Server Pages": "#2A6277", "JavaScript": "#f1e05a", "JavaScript+ERB": "#f1e05a", "JCL": "#d90e09", "Jest Snapshot": "#15c213", "JetBrains MPS": "#21D789", "JFlex": "#DBCA00", "Jinja": "#a52a22", "Jison": "#56b3cb", "Jison Lex": "#56b3cb", "Jolie": "#843179", "jq": "#c7254e", "JSON": "#292929", "JSON with Comments": "#292929", "JSON5": "#267CB9", "JSONiq": "#40d47e", "JSONLD": "#0c479c", "Jsonnet": "#0064bd", "Julia": "#a270ba", "Jupyter Notebook": "#DA5B0B", "Just": "#384d54", "Kaitai Struct": "#773b37", "KakouneScript": "#6f8042", "KerboScript": "#41adf0", "KiCad Layout": "#2f4aab", "KiCad Legacy Layout": "#2f4aab", "KiCad Schematic": "#2f4aab", "Kotlin": "#A97BFF", "KRL": "#28430A", "kvlang": "#1da6e0", "LabVIEW": "#fede06", "Lark": "#2980B9", "Lasso": "#999999", "Latte": "#f2a542", "Lean": "#d2cece", "Less": "#1d365d", "Lex": "#DBCA00", "LFE": "#4C3023", "LigoLANG": "#0e74ff", "LilyPond": "#9ccc7c", "Limbo": "#d2cece", "Liquid": "#67b8de", "Literate Agda": "#315665", "Literate CoffeeScript": "#244776", "Literate Haskell": "#5e5086", "LiveScript": "#499886", "LLVM": "#185619", "Logos": "#d2cece", "Logtalk": "#295b9a", "LOLCODE": "#cc9900", "LookML": "#652B81", "LoomScript": "#d2cece", "LSL": "#3d9970", "Lua": "#000080", "M": "#d2cece", "M4": "#d2cece", "M4Sugar": "#d2cece", "Macaulay2": "#d8ffff", "Makefile": "#427819", "Mako": "#7e858d", "Markdown": "#083fa1", "Marko": "#42bff2", "Mask": "#f97732", "Mathematica": "#dd1100", "MATLAB": "#e16737", "Max": "#c4a79c", "MAXScript": "#00a6a6", "mcfunction": "#E22837", "Mercury": "#ff2b2b", "Mermaid": "#ff3670", "Meson": "#007800", "Metal": "#8f14e9", "MiniD": "#d2cece", "MiniYAML": "#ff1111", "Mint": "#02b046", "Mirah": "#c7a938", "mIRC Script": "#3d57c3", "MLIR": "#5EC8DB", "Modelica": "#de1d31", "Modula-2": "#10253f", "Modula-3": "#223388", "Module Management System": "#d2cece", "Monkey": "#d2cece", "Monkey C": "#8D6747", "Moocode": "#d2cece", "MoonScript": "#ff4585", "Motoko": "#fbb03b", "Motorola 68K Assembly": "#005daa", "Move": "#4a137a", "MQL4": "#62A8D6", "MQL5": "#4A76B8", "MTML": "#b7e1f4", "MUF": "#d2cece", "mupad": "#244963", "Mustache": "#724b3b", "Myghty": "#d2cece", "nanorc": "#2d004d", "Nasal": "#1d2c4e", "NASL": "#d2cece", "NCL": "#28431f", "Nearley": "#990000", "Nemerle": "#3d3c6e", "nesC": "#94B0C7", "NetLinx": "#0aa0ff", "NetLinx+ERB": "#747faa", "NetLogo": "#ff6375", "NewLisp": "#87AED7", "Nextflow": "#3ac486", "Nginx": "#009639", "Nim": "#ffc200", "Nit": "#009917", "Nix": "#7e7eff", "NPM Config": "#cb3837", "NSIS": "#d2cece", "Nu": "#c9df40", "NumPy": "#9C8AF9", "Nunjucks": "#3d8137", "NWScript": "#111522", "OASv2-json": "#85ea2d", "OASv2-yaml": "#85ea2d", "OASv3-json": "#85ea2d", "OASv3-yaml": "#85ea2d", "Objective-C": "#438eff", "Objective-C++": "#6866fb", "Objective-J": "#ff0c5a", "ObjectScript": "#424893", "OCaml": "#3be133", "Odin": "#60AFFE", "Omgrofl": "#cabbff", "ooc": "#b0b77e", "Opa": "#d2cece", "Opal": "#f7ede0", "Open Policy Agent": "#7d9199", "OpenAPI Specification v2": "#85ea2d", "OpenAPI Specification v3": "#85ea2d", "OpenCL": "#ed2e2d", "OpenEdge ABL": "#5ce600", "OpenQASM": "#AA70FF", "OpenRC runscript": "#d2cece", "OpenSCAD": "#e5cd45", "Option List": "#476732", "Org": "#77aa99", "Ox": "#d2cece", "Oxygene": "#cdd0e3", "Oz": "#fab738", "P4": "#7055b5", "Pan": "#cc0000", "Papyrus": "#6600cc", "Parrot": "#f3ca0a", "Parrot Assembly": "#d2cece", "Parrot Internal Representation": "#d2cece", "Pascal": "#E3F171", "Pawn": "#dbb284", "PDDL": "#0d00ff", "PEG.js": "#234d6b", "Pep8": "#C76F5B", "Perl": "#0298c3", "PHP": "#4F5D95", "PicoLisp": "#6067af", "PigLatin": "#fcd7de", "Pike": "#005390", "PlantUML": "#fbbd16", "PLpgSQL": "#336790", "PLSQL": "#dad8d8", "PogoScript": "#d80074", "Polar": "#ae81ff", "Pony": "#d2cece", "Portugol": "#f8bd00", "PostCSS": "#dc3a0c", "PostScript": "#da291c", "POV-Ray SDL": "#6bac65", "PowerBuilder": "#8f0f8d", "PowerShell": "#012456", "Prisma": "#0c344b", "Processing": "#0096D8", "Procfile": "#3B2F63", "Prolog": "#74283c", "Promela": "#de0000", "Propeller Spin": "#7fa2a7", "Pug": "#a86454", "Puppet": "#302B6D", "PureBasic": "#5a6986", "PureScript": "#1D222D", "Pyret": "#ee1e10", "Python": "#3572A5", "Python console": "#3572A5", "Python traceback": "#3572A5", "q": "#0040cd", "Q#": "#fed659", "QMake": "#d2cece", "QML": "#44a51c", "Qt Script": "#00b841", "Quake": "#882233", "R": "#198CE7", "Racket": "#3c5caa", "Ragel": "#9d5200", "Raku": "#0000fb", "RAML": "#77d9fb", "Rascal": "#fffaa0", "RDoc": "#701516", "REALbasic": "#d2cece", "Reason": "#ff5847", "ReasonLIGO": "#ff5847", "Rebol": "#358a5b", "Record Jar": "#0673ba", "Red": "#f50000", "Redcode": "#d2cece", "Regular Expression": "#009a00", "Ren'Py": "#ff7f7f", "RenderScript": "#d2cece", "ReScript": "#ed5051", "reStructuredText": "#141414", "REXX": "#d90e09", "Ring": "#2D54CB", "Riot": "#A71E49", "RMarkdown": "#198ce7", "RobotFramework": "#00c0b5", "Roff": "#ecdebe", "Roff Manpage": "#ecdebe", "Rouge": "#cc0088", "RouterOS Script": "#DE3941", "RPC": "#d2cece", "RPGLE": "#2BDE21", "Ruby": "#701516", "RUNOFF": "#665a4e", "Rust": "#dea584", "Sage": "#d2cece", "SaltStack": "#646464", "SAS": "#B34936", "Sass": "#a53b70", "Scala": "#c22d40", "Scaml": "#bd181a", "Scenic": "#fdc700", "Scheme": "#1e4aec", "Scilab": "#ca0f21", "SCSS": "#c6538c", "sed": "#64b970", "Self": "#0579aa", "ShaderLab": "#222c37", "Shell": "#89e051", "ShellCheck Config": "#cecfcb", "ShellSession": "#d2cece", "Shen": "#120F14", "Sieve": "#d2cece", "Simple File Verification": "#C9BFED", "Singularity": "#64E6AD", "Slash": "#007eff", "Slice": "#003fa2", "Slim": "#2b2b2b", "Smali": "#d2cece", "Smalltalk": "#596706", "Smarty": "#f0c040", "Smithy": "#c44536", "SmPL": "#c94949", "SMT": "#d2cece", "Snakemake": "#419179", "Solidity": "#AA6746", "SourcePawn": "#f69e1d", "SPARQL": "#0C4597", "SQF": "#3F3F3F", "SQL": "#e38c00", "SQLPL": "#e38c00", "Squirrel": "#800000", "SRecode Template": "#348a34", "Stan": "#b2011d", "Standard ML": "#dc566d", "Starlark": "#76d275", "Stata": "#1a5f91", "STL": "#373b5e", "StringTemplate": "#3fb34f", "Stylus": "#ff6347", "SubRip Text": "#9e0101", "SugarSS": "#2fcc9f", "SuperCollider": "#46390b", "Svelte": "#ff3e00", "SVG": "#ff9900", "Sway": "#dea584", "Swift": "#F05138", "SWIG": "#d2cece", "SystemVerilog": "#DAE1C2", "Talon": "#333333", "Tcl": "#e4cc98", "Tcsh": "#d2cece", "Terra": "#00004c", "TeX": "#3D6117", "Textile": "#ffe7ac", "TextMate Properties": "#df66e4", "Thrift": "#D12127", "TI Program": "#A0AA87", "TLA": "#4b0079", "TOML": "#9c4221", "TSQL": "#e38c00", "TSV": "#237346", "TSX": "#3178c6", "Turing": "#cf142b", "Twig": "#c1d026", "TXL": "#0178b8", "TypeScript": "#3178c6", "Unified Parallel C": "#4e3617", "Unity3D Asset": "#222c37", "Unix Assembly": "#d2cece", "Uno": "#9933cc", "UnrealScript": "#a54c4d", "UrWeb": "#ccccee", "V": "#4f87c4", "Vala": "#a56de2", "Valve Data Format": "#f26025", "VBA": "#867db1", "VBScript": "#15dcdc", "VCL": "#148AA8", "Velocity Template Language": "#507cff", "Verilog": "#b2b7f8", "VHDL": "#adb2cb", "Vim Help File": "#199f4b", "Vim Script": "#199f4b", "Vim Snippet": "#199f4b", "Visual Basic .NET": "#945db7", "Visual Basic 6.0": "#2c6353", "Volt": "#1F1F1F", "Vue": "#41b883", "Vyper": "#2980b9", "wdl": "#42f1f4", "Web Ontology Language": "#5b70bd", "WebAssembly": "#04133b", "WebIDL": "#d2cece", "Whiley": "#d5c397", "Wikitext": "#fc5757", "Windows Registry Entries": "#52d5ff", "wisp": "#7582D1", "Witcher Script": "#ff0000", "Wollok": "#a23738", "World of Warcraft Addon Data": "#f7e43f", "Wren": "#383838", "X10": "#4B6BEF", "xBase": "#403a40", "XC": "#99DA07", "XML": "#0060ac", "XML Property List": "#0060ac", "Xojo": "#81bd41", "Xonsh": "#285EEF", "XProc": "#d2cece", "XQuery": "#5232e7", "XS": "#d2cece", "XSLT": "#EB8CEB", "Xtend": "#24255d", "Yacc": "#4B6C4B", "YAML": "#cb171e", "YARA": "#220000", "YASnippet": "#32AB90", "Yul": "#794932", "ZAP": "#0d665e", "Zeek": "#d2cece", "ZenScript": "#00BCD1", "Zephir": "#118f9e", "Zig": "#ec915c", "ZIL": "#dc75e5", "Zimpl": "#d67711"}
16 |
17 | const totalPage = 40000
18 |
19 | type GithubRepo struct {
20 | FullName string `json:"full_name"`
21 | Name string `json:"name"`
22 | Url string `json:"html_url"`
23 | Stars int `json:"stargazers_count"`
24 | Description string `json:"description"`
25 | Owner struct {
26 | AvatarUrl string `json:"avatar_url"`
27 | Login string `json:"login"`
28 | } `json:"owner"`
29 | Language string `json:"language"`
30 | }
31 |
32 | type PopularRepo struct {
33 | User string `json:"user"`
34 | Avatar string `json:"avatar"`
35 | Repo string `json:"repo"`
36 | Description string `json:"description"`
37 | Language string `json:"language"`
38 | Color string `json:"color"`
39 | Stars int `json:"stars"`
40 | Url string `json:"url"`
41 | }
42 |
43 | func GetColor(lang any) string {
44 | if lang == nil {
45 | return "#d2cece"
46 | }
47 | val, ok := colors[lang.(string)]
48 | if !ok {
49 | return "#d2cece"
50 | }
51 | return val
52 | }
53 |
54 | func GetPopularRepo(page int) ([]PopularRepo, error) {
55 | if page*100 > totalPage {
56 | page = 1
57 | }
58 |
59 | uri := viper.GetString("github.endpoint") + "/search/repositories?q=stars:%3E1000&per_page=100&page=" + strconv.Itoa(page)
60 | data, err := Get(uri, map[string]string{
61 | "Accept": "application/vnd.github.v3+json",
62 | "Authorization": "Bearer " + viper.GetString("github.token"),
63 | })
64 |
65 | if err != nil {
66 | return nil, err
67 | }
68 |
69 | var res []GithubRepo
70 | items := data.(map[string]interface{})["items"].([]interface{})
71 |
72 | err = MapToStruct(items, &res)
73 | if err != nil {
74 | return nil, err
75 | }
76 |
77 | return HandlePopularRepo(res), nil
78 | }
79 |
80 | func HandlePopularRepo(data []GithubRepo) []PopularRepo {
81 | var res []PopularRepo
82 | for _, v := range data {
83 | res = append(res, PopularRepo{
84 | User: v.Owner.Login,
85 | Avatar: v.Owner.AvatarUrl,
86 | Repo: v.Name,
87 | Description: v.Description,
88 | Language: v.Language,
89 | Stars: v.Stars,
90 | Url: v.Url,
91 | Color: GetColor(v.Language),
92 | })
93 | }
94 |
95 | return res
96 | }
97 |
98 | func GetRandomPagination(data []PopularRepo) []PopularRepo {
99 | page := rand.Intn(25)
100 | return data[page*4 : page*4+4]
101 | }
102 |
103 | func GetRandomPopularRepoWithCache(ctx *gin.Context, cache *redis.Client) ([]PopularRepo, error) {
104 | page := rand.Intn(10)
105 |
106 | if result, err := cache.Get(ctx, fmt.Sprintf("popularepo:%d", page)).Result(); err == nil && result != "" {
107 | var res []PopularRepo
108 | if err = json.Unmarshal([]byte(result), &res); err == nil {
109 | return GetRandomPagination(res), nil
110 | }
111 | }
112 |
113 | res, err := GetPopularRepo(page)
114 | if err != nil {
115 | return nil, err
116 | }
117 |
118 | if buffer, err := json.Marshal(res); err == nil {
119 | cache.Set(ctx, fmt.Sprintf("popularepo:%d", page), buffer, time.Minute*2)
120 | }
121 |
122 | return GetRandomPagination(res), nil
123 | }
124 |
--------------------------------------------------------------------------------
/backend/go.mod:
--------------------------------------------------------------------------------
1 | module fystart
2 |
3 | go 1.20
4 |
5 | require (
6 | github.com/dgrijalva/jwt-go v3.2.0+incompatible
7 | github.com/gin-gonic/gin v1.9.1
8 | github.com/go-redis/redis/v8 v8.11.5
9 | github.com/go-sql-driver/mysql v1.7.1
10 | github.com/spf13/viper v1.16.0
11 | )
12 |
13 | require (
14 | github.com/bytedance/sonic v1.10.0-rc // indirect
15 | github.com/cespare/xxhash/v2 v2.1.2 // indirect
16 | github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
17 | github.com/chenzhuoyu/iasm v0.9.0 // indirect
18 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
19 | github.com/fsnotify/fsnotify v1.6.0 // indirect
20 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect
21 | github.com/gin-contrib/sse v0.1.0 // indirect
22 | github.com/go-playground/locales v0.14.1 // indirect
23 | github.com/go-playground/universal-translator v0.18.1 // indirect
24 | github.com/go-playground/validator/v10 v10.14.1 // indirect
25 | github.com/goccy/go-json v0.10.2 // indirect
26 | github.com/hashicorp/hcl v1.0.0 // indirect
27 | github.com/json-iterator/go v1.1.12 // indirect
28 | github.com/klauspost/cpuid/v2 v2.2.5 // indirect
29 | github.com/leodido/go-urn v1.2.4 // indirect
30 | github.com/magiconair/properties v1.8.7 // indirect
31 | github.com/mattn/go-isatty v0.0.19 // indirect
32 | github.com/mitchellh/mapstructure v1.5.0 // indirect
33 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
34 | github.com/modern-go/reflect2 v1.0.2 // indirect
35 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect
36 | github.com/spf13/afero v1.9.5 // indirect
37 | github.com/spf13/cast v1.5.1 // indirect
38 | github.com/spf13/jwalterweatherman v1.1.0 // indirect
39 | github.com/spf13/pflag v1.0.5 // indirect
40 | github.com/subosito/gotenv v1.4.2 // indirect
41 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
42 | github.com/ugorji/go/codec v1.2.11 // indirect
43 | golang.org/x/arch v0.3.0 // indirect
44 | golang.org/x/crypto v0.10.0 // indirect
45 | golang.org/x/net v0.11.0 // indirect
46 | golang.org/x/sys v0.9.0 // indirect
47 | golang.org/x/text v0.10.0 // indirect
48 | google.golang.org/protobuf v1.31.0 // indirect
49 | gopkg.in/ini.v1 v1.67.0 // indirect
50 | gopkg.in/yaml.v3 v3.0.1 // indirect
51 | )
52 |
--------------------------------------------------------------------------------
/backend/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | "github.com/spf13/viper"
6 | )
7 |
8 | type RequestBody struct {
9 | Message string `json:"message" required:"true"`
10 | }
11 |
12 | func main() {
13 | viper.SetConfigFile("config.yaml")
14 | if err := viper.ReadInConfig(); err != nil {
15 | panic(err)
16 | }
17 | if viper.GetBool("debug") {
18 | gin.SetMode(gin.DebugMode)
19 | } else {
20 | gin.SetMode(gin.ReleaseMode)
21 | }
22 |
23 | app := gin.Default()
24 |
25 | {
26 | app.Use(CORSMiddleware())
27 | app.Use(BuiltinMiddleWare(ConnectMySQL(), ConnectRedis()))
28 | app.Use(ThrottleMiddleware())
29 | app.Use(AuthMiddleware())
30 | }
31 | {
32 | app.POST("/login", LoginAPI)
33 | app.POST("/state", StateAPI)
34 | app.POST("/sync", SyncStorageAPI)
35 | app.GET("/github", GithubExploreAPI)
36 | }
37 |
38 | err := app.Run(":8001")
39 | if err != nil {
40 | panic(err)
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/backend/middleware.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "database/sql"
5 | "fmt"
6 | "github.com/gin-gonic/gin"
7 | "github.com/go-redis/redis/v8"
8 | "net/http"
9 | "strings"
10 | "time"
11 | )
12 |
13 | var allowedOrigins = []string{
14 | "https://fystart.com",
15 | "https://www.fystart.com",
16 | "https://fystart.cn",
17 | "https://www.fystart.cn",
18 | "https://deeptrain.net",
19 | "https://www.deeptrain.net",
20 | "http://localhost",
21 | "http://localhost:5173",
22 | }
23 |
24 | type Limiter struct {
25 | Duration int
26 | Count int64
27 | }
28 |
29 | func (l *Limiter) RateLimit(ctx *gin.Context, rds *redis.Client, ip string, path string) bool {
30 | key := fmt.Sprintf("rate%s:%s", path, ip)
31 | count, err := rds.Incr(ctx, key).Result()
32 | if err != nil {
33 | return true
34 | }
35 | if count == 1 {
36 | rds.Expire(ctx, key, time.Duration(l.Duration)*time.Second)
37 | }
38 | return count > l.Count
39 | }
40 |
41 | var limits = map[string]Limiter{
42 | "/login": {Duration: 10, Count: 5},
43 | "/state": {Duration: 1, Count: 2},
44 | "/github": {Duration: 60, Count: 45},
45 | "/storage/sync": {Duration: 120, Count: 60},
46 | }
47 |
48 | func GetPrefixMap[T comparable](s string, p map[string]T) *T {
49 | for k, v := range p {
50 | if strings.HasPrefix(s, k) {
51 | return &v
52 | }
53 | }
54 | return nil
55 | }
56 |
57 | func ThrottleMiddleware() gin.HandlerFunc {
58 | return func(c *gin.Context) {
59 | ip := c.ClientIP()
60 | path := c.Request.URL.Path
61 | rds := c.MustGet("cache").(*redis.Client)
62 | limiter := GetPrefixMap[Limiter](path, limits)
63 | if limiter != nil && limiter.RateLimit(c, rds, ip, path) {
64 | c.JSON(200, gin.H{"status": false, "reason": "You have sent too many requests. Please try again later."})
65 | c.Abort()
66 | return
67 | }
68 | c.Next()
69 | }
70 | }
71 |
72 | func CORSMiddleware() gin.HandlerFunc {
73 | return func(c *gin.Context) {
74 | origin := c.Request.Header.Get("Origin")
75 | if Contains(origin, allowedOrigins) {
76 | c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
77 | c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
78 | c.Writer.Header().Set("Access-Control-Allow-Headers", "Origin, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
79 |
80 | if c.Request.Method == "OPTIONS" {
81 | c.Writer.Header().Set("Access-Control-Max-Age", "3600")
82 | c.AbortWithStatus(http.StatusOK)
83 | return
84 | }
85 | }
86 |
87 | c.Next()
88 | }
89 | }
90 |
91 | func AuthMiddleware() gin.HandlerFunc {
92 | return func(c *gin.Context) {
93 | token := strings.TrimSpace(c.GetHeader("Authorization"))
94 | if token != "" {
95 | if user := ParseToken(c, token); user != nil {
96 | c.Set("token", token)
97 | c.Set("auth", true)
98 | c.Set("user", user.Username)
99 | c.Next()
100 | return
101 | }
102 | }
103 |
104 | c.Set("token", token)
105 | c.Set("auth", false)
106 | c.Set("user", "")
107 | c.Next()
108 | }
109 | }
110 |
111 | func BuiltinMiddleWare(db *sql.DB, cache *redis.Client) gin.HandlerFunc {
112 | return func(c *gin.Context) {
113 | c.Set("db", db)
114 | c.Set("cache", cache)
115 | c.Next()
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/backend/storage.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "database/sql"
5 | "encoding/json"
6 | "github.com/gin-gonic/gin"
7 | "net/http"
8 | )
9 |
10 | type StorageRequestBody struct {
11 | ChatGPT bool `json:"chatgpt" required:"true"`
12 | Quote bool `json:"quote" required:"true"`
13 | ToolBox bool `json:"toolbox" required:"true"`
14 | About bool `json:"about" required:"true"`
15 | ExactTime bool `json:"exactTime" required:"true"`
16 | OpenAISecret string `json:"openaiSecret"`
17 | FocusInput bool `json:"focusInput" required:"true"`
18 | Language string `json:"language" required:"true"`
19 | Background string `json:"background" required:"true"`
20 | Stamp int64 `json:"stamp" required:"true"`
21 | Tools []interface{} `json:"tools" required:"true"`
22 | }
23 |
24 | func GetStorage(db *sql.DB, id int) *StorageRequestBody {
25 | var text string
26 | if err := db.QueryRow("SELECT data FROM storage WHERE id = ?", id).Scan(&text); err != nil {
27 | return nil
28 | }
29 |
30 | var data StorageRequestBody
31 | if err := json.Unmarshal([]byte(text), &data); err != nil {
32 | return nil
33 | }
34 |
35 | return &data
36 | }
37 |
38 | func SaveStorage(db *sql.DB, id int, data StorageRequestBody) bool {
39 | res, err := json.Marshal(data)
40 | if err != nil {
41 | return false
42 | }
43 |
44 | if _, err := db.Exec("UPDATE storage SET data = ? WHERE id = ?", string(res), id); err != nil {
45 | return false
46 | }
47 | return true
48 | }
49 |
50 | func SyncStorageAPI(c *gin.Context) {
51 | var body StorageRequestBody
52 | if err := c.ShouldBindJSON(&body); err != nil {
53 | c.JSON(http.StatusOK, gin.H{
54 | "status": false,
55 | "message": "Bad Request",
56 | })
57 | return
58 | }
59 |
60 | user := c.MustGet("user").(string)
61 | if user == "" {
62 | c.JSON(http.StatusOK, gin.H{
63 | "status": false,
64 | "message": "Bad Request",
65 | })
66 | return
67 | }
68 |
69 | db := c.MustGet("db").(*sql.DB)
70 |
71 | var id int
72 | if err := db.QueryRow("SELECT id FROM auth WHERE username = ?", user).Scan(&id); err != nil {
73 | c.JSON(http.StatusOK, gin.H{
74 | "status": false,
75 | "message": "Internal Server Error",
76 | "error": err.Error(),
77 | })
78 | return
79 | }
80 |
81 | data := GetStorage(db, id)
82 | if data == nil {
83 | // create new storage
84 | if !SaveStorage(db, id, body) {
85 | c.JSON(http.StatusOK, gin.H{
86 | "status": false,
87 | "message": "Internal Server Error",
88 | "error": "save storage failed",
89 | })
90 | } else {
91 | c.JSON(http.StatusOK, gin.H{
92 | "status": true,
93 | "sync": false,
94 | "data": body,
95 | })
96 | }
97 | return
98 | }
99 |
100 | if body.Stamp < data.Stamp {
101 | // sync new data
102 | c.JSON(http.StatusOK, gin.H{
103 | "status": true,
104 | "sync": true,
105 | "data": data,
106 | })
107 | return
108 | }
109 |
110 | // save storage
111 | if !SaveStorage(db, id, body) {
112 | c.JSON(http.StatusOK, gin.H{
113 | "status": false,
114 | "message": "Internal Server Error",
115 | "error": "save storage failed",
116 | })
117 | return
118 | }
119 |
120 | c.JSON(http.StatusOK, gin.H{
121 | "status": true,
122 | "sync": false,
123 | "data": body,
124 | })
125 | }
126 |
--------------------------------------------------------------------------------
/backend/utils.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "crypto/sha256"
6 | "encoding/hex"
7 | "encoding/json"
8 | "io"
9 | "math/rand"
10 | "net/http"
11 | "net/url"
12 | )
13 |
14 | func Sha2Encrypt(raw string) string {
15 | hash := sha256.Sum256([]byte(raw))
16 | return hex.EncodeToString(hash[:])
17 | }
18 |
19 | func Unmarshal[T interface{}](data []byte) (form T, err error) {
20 | err = json.Unmarshal(data, &form)
21 | return form, err
22 | }
23 |
24 | func GenerateChar(length int) string {
25 | const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
26 | result := make([]byte, length)
27 | for i := 0; i < length; i++ {
28 | result[i] = charset[rand.Intn(len(charset))]
29 | }
30 | return string(result)
31 | }
32 |
33 | func Http(uri string, method string, ptr interface{}, headers map[string]string, body io.Reader) (err error) {
34 | req, err := http.NewRequest(method, uri, body)
35 | if err != nil {
36 | return err
37 | }
38 | for key, value := range headers {
39 | req.Header.Set(key, value)
40 | }
41 |
42 | client := &http.Client{}
43 | resp, err := client.Do(req)
44 | if err != nil {
45 | return err
46 | }
47 |
48 | defer resp.Body.Close()
49 |
50 | if err = json.NewDecoder(resp.Body).Decode(ptr); err != nil {
51 | return err
52 | }
53 | return nil
54 | }
55 |
56 | func Get(uri string, headers map[string]string) (data interface{}, err error) {
57 | err = Http(uri, http.MethodGet, &data, headers, nil)
58 | return data, err
59 | }
60 |
61 | func Post(uri string, headers map[string]string, body interface{}) (data interface{}, err error) {
62 | var form io.Reader
63 | if buffer, err := json.Marshal(body); err == nil {
64 | form = bytes.NewBuffer(buffer)
65 | }
66 | err = Http(uri, http.MethodPost, &data, headers, form)
67 | return data, err
68 | }
69 |
70 | func PostForm(uri string, body map[string]interface{}) (data map[string]interface{}, err error) {
71 | client := &http.Client{}
72 | form := make(url.Values)
73 | for key, value := range body {
74 | form[key] = []string{value.(string)}
75 | }
76 | res, err := client.PostForm(uri, form)
77 | if err != nil {
78 | return nil, err
79 | }
80 | content, err := io.ReadAll(res.Body)
81 | if err != nil {
82 | return nil, err
83 | }
84 |
85 | if err = json.Unmarshal(content, &data); err != nil {
86 | return nil, err
87 | }
88 |
89 | return data, nil
90 | }
91 |
92 | func Contains[T comparable](value T, slice []T) bool {
93 | for _, item := range slice {
94 | if item == value {
95 | return true
96 | }
97 | }
98 | return false
99 | }
100 |
101 | func MapToStruct(m interface{}, s interface{}) error {
102 | b, err := json.Marshal(m)
103 | if err != nil {
104 | return err
105 | }
106 | return json.Unmarshal(b, s)
107 | }
108 |
--------------------------------------------------------------------------------
/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 极目起始页 | Fystart
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fystart",
3 | "version": "1.12.3",
4 | "private": true,
5 | "scripts": {
6 | "dev": "vite",
7 | "build": "vite build",
8 | "preview": "vite preview",
9 | "prettier": "prettier --write \"src/**/*.vue\" \"src/**/*.ts\""
10 | },
11 | "dependencies": {
12 | "axios": "^1.4.0",
13 | "lunar-calendar": "^0.1.4",
14 |
15 | "vue": "^3.2.45",
16 | "vue-i18n": "9.2.2"
17 | },
18 | "devDependencies": {
19 | "prettier": "^3.0.3",
20 | "@intlify/unplugin-vue-i18n": "^0.12.0",
21 | "@types/node": "^18.14.2",
22 | "@vitejs/plugin-vue": "^4.0.0",
23 | "@vue/tsconfig": "^0.1.3",
24 | "npm-run-all": "^4.1.5",
25 | "typescript": "~4.8.4",
26 | "vite": "4.1.5",
27 | "vite-plugin-html": "^3.2.0",
28 | "vite-plugin-pwa": "^0.16.4",
29 | "vue-tsc": "^1.2.0",
30 | "workbox-window": "^7.0.0"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/public/background.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zmh-program/fystart/d3013fc1daede8e84520743a544a619d2ea9ecb8/public/background.webp
--------------------------------------------------------------------------------
/public/background/forest.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zmh-program/fystart/d3013fc1daede8e84520743a544a619d2ea9ecb8/public/background/forest.webp
--------------------------------------------------------------------------------
/public/background/hills.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zmh-program/fystart/d3013fc1daede8e84520743a544a619d2ea9ecb8/public/background/hills.webp
--------------------------------------------------------------------------------
/public/background/lake.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zmh-program/fystart/d3013fc1daede8e84520743a544a619d2ea9ecb8/public/background/lake.webp
--------------------------------------------------------------------------------
/public/background/morning.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zmh-program/fystart/d3013fc1daede8e84520743a544a619d2ea9ecb8/public/background/morning.webp
--------------------------------------------------------------------------------
/public/background/mountain.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zmh-program/fystart/d3013fc1daede8e84520743a544a619d2ea9ecb8/public/background/mountain.webp
--------------------------------------------------------------------------------
/public/background/ocean.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zmh-program/fystart/d3013fc1daede8e84520743a544a619d2ea9ecb8/public/background/ocean.webp
--------------------------------------------------------------------------------
/public/background/snow.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zmh-program/fystart/d3013fc1daede8e84520743a544a619d2ea9ecb8/public/background/snow.webp
--------------------------------------------------------------------------------
/public/background/sunshine.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zmh-program/fystart/d3013fc1daede8e84520743a544a619d2ea9ecb8/public/background/sunshine.webp
--------------------------------------------------------------------------------
/public/beian.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zmh-program/fystart/d3013fc1daede8e84520743a544a619d2ea9ecb8/public/beian.webp
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zmh-program/fystart/d3013fc1daede8e84520743a544a619d2ea9ecb8/public/favicon.ico
--------------------------------------------------------------------------------
/public/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zmh-program/fystart/d3013fc1daede8e84520743a544a619d2ea9ecb8/public/icon.png
--------------------------------------------------------------------------------
/public/tool/add.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/public/tool/cloudflare.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/public/tool/codepen.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/public/tool/convert.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/public/tool/github.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/public/tool/jsdelivr.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/public/tool/kaggle.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/public/tool/lightnotes.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zmh-program/fystart/d3013fc1daede8e84520743a544a619d2ea9ecb8/public/tool/lightnotes.ico
--------------------------------------------------------------------------------
/public/tool/openai.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/public/tool/readthedocs.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/public/tool/replit.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/public/tool/stackoverflow.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/public/tool/twitter.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/public/tool/unknown.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/public/tool/vercel.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/screenshot/customize.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zmh-program/fystart/d3013fc1daede8e84520743a544a619d2ea9ecb8/screenshot/customize.png
--------------------------------------------------------------------------------
/screenshot/engine.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zmh-program/fystart/d3013fc1daede8e84520743a544a619d2ea9ecb8/screenshot/engine.png
--------------------------------------------------------------------------------
/screenshot/i18n.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zmh-program/fystart/d3013fc1daede8e84520743a544a619d2ea9ecb8/screenshot/i18n.png
--------------------------------------------------------------------------------
/screenshot/main.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zmh-program/fystart/d3013fc1daede8e84520743a544a619d2ea9ecb8/screenshot/main.png
--------------------------------------------------------------------------------
/screenshot/search.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zmh-program/fystart/d3013fc1daede8e84520743a544a619d2ea9ecb8/screenshot/search.png
--------------------------------------------------------------------------------
/screenshot/settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zmh-program/fystart/d3013fc1daede8e84520743a544a619d2ea9ecb8/screenshot/settings.png
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
37 |
--------------------------------------------------------------------------------
/src/assets/script/auth.ts:
--------------------------------------------------------------------------------
1 | import { onMounted, ref, watch } from "vue";
2 | import axios from "axios";
3 |
4 | export const auth = ref(undefined);
5 | export const token = ref(localStorage.getItem("token") || "");
6 | export const username = ref("");
7 |
8 | async function update() {
9 | localStorage.setItem("token", token.value);
10 | axios.defaults.headers.common["Authorization"] = token.value;
11 |
12 | if (token.value) {
13 | const resp = await axios.post("/state");
14 | username.value = resp.data.user;
15 | auth.value = resp.data.status;
16 | if (auth.value) {
17 | username.value = resp.data.user;
18 | }
19 | } else {
20 | auth.value = false;
21 | username.value = "";
22 | }
23 | }
24 |
25 | watch(token, update);
26 |
27 | window.addEventListener("load", () => {
28 | const url = new URLSearchParams(window.location.search);
29 | if (url.has("token")) {
30 | window.history.replaceState({}, "", "/");
31 | const client = url.get("token") || "";
32 | if (client)
33 | axios.post("/login", { token: client }).then((resp) => {
34 | token.value = resp.data.token;
35 | });
36 | }
37 | update().then((r) => 0);
38 | });
39 |
--------------------------------------------------------------------------------
/src/assets/script/card/calendar.ts:
--------------------------------------------------------------------------------
1 | import { solarToLunar } from "lunar-calendar";
2 |
3 | const WEEKDAY = ["日", "一", "二", "三", "四", "五", "六"];
4 |
5 | export type Calendar = {
6 | lunar: string;
7 | zodiac: string;
8 | solar: string;
9 | ganzhi: string;
10 | weekday: string;
11 | day: number;
12 | festival?: string;
13 | };
14 |
15 | export function getCalendar(): Calendar {
16 | const date = new Date();
17 | const weekday = WEEKDAY[date.getUTCDay()];
18 | const day = date.getUTCDate();
19 | const week = Math.ceil(day / 7);
20 | const month = date.getUTCMonth();
21 | const year = date.getUTCFullYear();
22 | const calendar = solarToLunar(
23 | date.getFullYear(),
24 | date.getMonth() + 1,
25 | date.getDate(),
26 | );
27 | return {
28 | lunar: calendar.lunarMonthName + calendar.lunarDayName,
29 | zodiac: calendar.zodiac,
30 | solar: `${year}年${month + 1}月`,
31 | ganzhi: `${calendar.GanZhiYear}年${calendar.GanZhiMonth}月${calendar.GanZhiDay}日`,
32 | weekday: `星期${weekday}`,
33 | day,
34 | festival: calendar.solarFestival,
35 | };
36 | }
37 |
--------------------------------------------------------------------------------
/src/assets/script/card/github.ts:
--------------------------------------------------------------------------------
1 | import { ref } from "vue";
2 | import axios from "axios";
3 | import { DecimalConvert } from "@/assets/script/utils/base";
4 |
5 | type GithubRepo = {
6 | user: string;
7 | avatar: string;
8 | repo: string;
9 | description: string;
10 | url: string;
11 | language: string;
12 | stars: string;
13 | color: string;
14 | };
15 |
16 | export const data = ref([]);
17 |
18 | export const loading = ref(false);
19 | export function update() {
20 | if (loading.value) return;
21 | loading.value = true;
22 | axios
23 | .get("/github")
24 | .then((res) => {
25 | data.value = res.data.data;
26 | data.value.forEach((repo: GithubRepo) => {
27 | repo.stars = DecimalConvert(Number(repo.stars));
28 | });
29 |
30 | loading.value = false;
31 | })
32 | .catch((e) => {
33 | console.debug(e);
34 | loading.value = false;
35 | });
36 | }
37 |
--------------------------------------------------------------------------------
/src/assets/script/card/weather.ts:
--------------------------------------------------------------------------------
1 | import { exportScript, insertScript } from "@/assets/script/utils/base";
2 | import { qweather } from "@/assets/script/config";
3 |
4 | export function setupWeatherCard(): void {
5 | exportScript("WIDGET", {
6 | CONFIG: {
7 | layout: "1",
8 | width: "220",
9 | height: "181",
10 | background: "3",
11 | dataColor: "FFFFFF",
12 | borderRadius: "5",
13 | modules: "10",
14 | key: qweather,
15 | },
16 | });
17 | insertScript(
18 | "https://widget.qweather.net/standard/static/js/he-standard-common.js?v=2.0",
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/src/assets/script/config.ts:
--------------------------------------------------------------------------------
1 | export const deploy = true;
2 | export let endpoint = "https://api.fystart.com";
3 | export let openai_endpoint = "wss://api.chatnio.net";
4 | export const qweather = "d25856c083574121a538e53952a2bfff";
5 |
6 | if (!deploy) endpoint = "http://localhost:8001";
7 |
--------------------------------------------------------------------------------
/src/assets/script/connection.ts:
--------------------------------------------------------------------------------
1 | import { openai_endpoint } from "@/assets/script/config";
2 | import { storage } from "@/assets/script/storage";
3 |
4 | export const endpoint = `${openai_endpoint}/chat`;
5 |
6 | export type StreamMessage = {
7 | keyword?: string;
8 | quota?: number;
9 | message: string;
10 | end: boolean;
11 | };
12 |
13 | export type ChatProps = {
14 | type: string;
15 | message: string;
16 | model: string;
17 | web?: boolean;
18 | };
19 |
20 | type StreamCallback = (message: StreamMessage) => void;
21 |
22 | export class Connection {
23 | protected connection?: WebSocket;
24 | protected callback?: StreamCallback;
25 | public id: number;
26 | public state: boolean;
27 |
28 | public constructor(id: number, callback?: StreamCallback) {
29 | this.state = false;
30 | this.id = id;
31 | this.init();
32 | this.callback && this.setCallback(callback);
33 | }
34 |
35 | public init(): void {
36 | this.connection = new WebSocket(endpoint);
37 | this.state = false;
38 | this.connection.onopen = () => {
39 | this.state = true;
40 | this.send({
41 | token: storage.openaiSecret || "anonymous",
42 | id: this.id,
43 | });
44 | };
45 | this.connection.onclose = () => {
46 | this.state = false;
47 | setTimeout(() => {
48 | console.debug(`[connection] reconnecting... (id: ${this.id})`);
49 | this.init();
50 | }, 3000);
51 | };
52 | this.connection.onmessage = (event) => {
53 | const message = JSON.parse(event.data);
54 | this.triggerCallback(message as StreamMessage);
55 | };
56 | }
57 |
58 | public send(data: Record): boolean {
59 | if (!this.state || !this.connection) {
60 | console.debug("[connection] connection not ready, retrying in 500ms...");
61 | return false;
62 | }
63 | this.connection.send(JSON.stringify(data));
64 | return true;
65 | }
66 |
67 | public sendWithRetry(data: ChatProps): void {
68 | try {
69 | if (!this.send(data)) {
70 | setTimeout(() => {
71 | this.sendWithRetry(data);
72 | }, 500);
73 | }
74 | } catch {
75 | this.triggerCallback({
76 | message:
77 | "Request failed, please check your network connection and try again later.",
78 | end: true,
79 | });
80 | }
81 | }
82 |
83 | public close(): void {
84 | if (!this.connection) return;
85 | this.connection.close();
86 | }
87 |
88 | public setCallback(callback?: StreamCallback): void {
89 | this.callback = callback;
90 | }
91 |
92 | protected triggerCallback(message: StreamMessage): void {
93 | this.callback && this.callback(message);
94 | }
95 | }
96 |
97 | export const connection = new Connection(-1);
98 |
--------------------------------------------------------------------------------
/src/assets/script/engine.ts:
--------------------------------------------------------------------------------
1 | import { computed, ref } from "vue";
2 | import type { Ref } from "vue";
3 |
4 | export function wrap(
5 | f: (...arg: any[]) => any,
6 | t?: number,
7 | ): (...arg: any) => any {
8 | /**
9 | * @param {function} f - function
10 | * @param {number} t - timeout
11 | * @return {function}
12 | */
13 | let timeout: number | undefined;
14 | return function (...arg: any[]): void {
15 | clearTimeout(timeout);
16 | timeout = setTimeout(() => f(...arg), t || 400);
17 | };
18 | }
19 |
20 | type Engine = string;
21 | export const engines: Engine[] = ["baidu", "bing", "google"];
22 | export const icons = {
23 | baidu:
24 | '',
25 | bing: '',
26 | google:
27 | '',
28 | };
29 |
30 | export const urls: Record = {
31 | baidu: "https://www.baidu.com/s?word=",
32 | bing: "https://bing.com/search?q=",
33 | google: "https://www.google.com/search?q=",
34 | };
35 |
36 | export const current: Ref = ref(
37 | engines.indexOf(localStorage.getItem("engine") || "baidu"),
38 | );
39 | // @ts-ignore
40 | export const getIcon = computed(() => icons[engines[current.value]]);
41 | export function toggle(): void {
42 | current.value++;
43 | if (current.value >= engines.length) current.value = 0;
44 | localStorage.setItem("engine", engines[current.value]);
45 | }
46 |
47 | export function set(idx: number): void {
48 | if (current.value < engines.length) {
49 | current.value = idx;
50 | localStorage.setItem("engine", engines[current.value]);
51 | }
52 | }
53 |
54 | export function uri(query: Engine): string {
55 | return urls[engines[current.value]] + decodeURI(query);
56 | }
57 |
58 | export const getSearchSuggestion = wrap(
59 | (content: string, callback: (res: string[]) => any): void => {
60 | content = content.trim();
61 | if (!content.length) return;
62 | const script = document.createElement("script");
63 | script.src = `https://suggestion.baidu.com/su?wd=${encodeURI(
64 | content,
65 | )}&cb=window.__callback__`;
66 | script.async = true;
67 | document.body.appendChild(script); // @ts-ignore
68 | window.__callback__ = function (res: {
69 | p: boolean;
70 | q: string;
71 | s: string[];
72 | }) {
73 | try {
74 | callback(res.s);
75 | document.body.removeChild(script);
76 | } catch {
77 | return;
78 | }
79 | };
80 | },
81 | );
82 |
83 | export namespace addition {
84 | export const search =
85 | '';
86 | export const additions: Record> = {
87 | deepl: {
88 | svg: '',
89 | link: "https://www.deepl.com/translator#en/zh/",
90 | },
91 | github: {
92 | svg: '',
93 | link: "https://github.com/search?q=",
94 | },
95 | };
96 | export function uri(type: string, text: string): string {
97 | return additions[type].link + decodeURI(text);
98 | }
99 | export function svg(type: string): string {
100 | return additions[type].svg;
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src/assets/script/openai.ts:
--------------------------------------------------------------------------------
1 | import { ref } from "vue";
2 | import type { Ref } from "vue";
3 | import { connection } from "@/assets/script/connection";
4 | import type { StreamMessage } from "@/assets/script/connection";
5 |
6 | export const finished = ref(true);
7 |
8 | export class OpenAI {
9 | private readonly ref: Ref;
10 | public readonly queue: Ref;
11 |
12 | public constructor() {
13 | this.ref = ref("");
14 | this.queue = ref("");
15 |
16 | connection.setCallback((message: StreamMessage) => {
17 | this.ref.value += message.message;
18 | finished.value = message.end;
19 |
20 | setTimeout(() => {
21 | const data = this.queue.value;
22 | if (finished.value && data.length > 0) this.trigger(data);
23 | }, 500);
24 | });
25 | }
26 | public getRef(): Ref {
27 | return this.ref;
28 | }
29 |
30 | public trigger(text: string) {
31 | if (!finished.value) {
32 | this.queue.value = text;
33 | return;
34 | }
35 |
36 | finished.value = false;
37 | this.ref.value = "";
38 | this.queue.value = "";
39 | connection.sendWithRetry({
40 | type: "chat",
41 | message: text,
42 | model: "gpt-3.5-turbo",
43 | web: false,
44 | });
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/assets/script/shared.ts:
--------------------------------------------------------------------------------
1 | import { ref } from "vue";
2 |
3 | export const input = ref("");
4 | export const context = ref(true);
5 |
--------------------------------------------------------------------------------
/src/assets/script/storage.ts:
--------------------------------------------------------------------------------
1 | import { reactive, watch } from "vue";
2 | import axios from "axios";
3 | import { auth } from "@/assets/script/auth";
4 | import { ToolTypes } from "@/assets/script/tool";
5 |
6 | let migrate = false;
7 | let timeout: number;
8 |
9 | function readDictConfig(data: Record): Record {
10 | for (const key in data) {
11 | const result = localStorage.getItem(key);
12 | if (result !== null) {
13 | try {
14 | data[key] = JSON.parse(result);
15 | } catch {
16 | console.debug(result);
17 | }
18 | }
19 | }
20 |
21 | return data;
22 | }
23 |
24 | function writeDictConfig(data: Record): void {
25 | for (const key in data) localStorage.setItem(key, JSON.stringify(data[key]));
26 | }
27 |
28 | export const storage = reactive(
29 | readDictConfig({
30 | chatgpt: true,
31 | quote: true,
32 | toolbox: true,
33 | about: true,
34 | exactTime: false,
35 | focusInput: true,
36 | language: "zh",
37 | background: "/background.webp",
38 | stamp: 0,
39 | openaiSecret: "",
40 | tools: [
41 | {
42 | type: ToolTypes.BUILTIN,
43 | name: "GitHub",
44 | link: "https://github.com",
45 | icon: "/tool/github.svg",
46 | },
47 | {
48 | type: ToolTypes.BUILTIN,
49 | name: "OpenAI",
50 | link: "https://chat.openai.com",
51 | icon: "/tool/openai.svg",
52 | },
53 | {
54 | type: ToolTypes.BUILTIN,
55 | name: "Stack Overflow",
56 | link: "https://stackoverflow.com",
57 | icon: "/tool/stackoverflow.svg",
58 | },
59 | {
60 | type: ToolTypes.BUILTIN,
61 | name: "Light Notes",
62 | link: "https://notes.lightxi.com",
63 | icon: "/tool/lightnotes.ico",
64 | },
65 | {
66 | type: ToolTypes.BUILTIN,
67 | name: "Cloudflare",
68 | link: "https://dash.cloudflare.com",
69 | icon: "/tool/cloudflare.svg",
70 | },
71 | {
72 | type: ToolTypes.BUILTIN,
73 | name: "Vercel",
74 | link: "https://vercel.com",
75 | icon: "/tool/vercel.svg",
76 | },
77 | {
78 | type: ToolTypes.BUILTIN,
79 | name: "Codepen",
80 | link: "https://codepen.io",
81 | icon: "/tool/codepen.svg",
82 | },
83 | {
84 | type: ToolTypes.BUILTIN,
85 | name: "Kaggle",
86 | link: "https://kaggle.com",
87 | icon: "/tool/kaggle.svg",
88 | },
89 | {
90 | type: ToolTypes.BUILTIN,
91 | name: "Replit",
92 | link: "https://replit.com",
93 | icon: "/tool/replit.svg",
94 | },
95 | ],
96 | }),
97 | );
98 |
99 | function sync() {
100 | migrate = true;
101 | axios.post("/sync", storage).then((response) => {
102 | if (response.data.success) {
103 | for (const key in response.data.data) {
104 | storage[key] = response.data.data[key];
105 | }
106 | }
107 | migrate = false;
108 | });
109 | }
110 | watch(auth, () => {
111 | if (!auth.value) return;
112 | sync();
113 | });
114 |
115 | watch(storage, () => {
116 | if (migrate) return;
117 | storage.stamp = Date.now();
118 | writeDictConfig(storage);
119 | clearTimeout(timeout);
120 | if (auth.value) timeout = setTimeout(sync, 1000);
121 | });
122 |
--------------------------------------------------------------------------------
/src/assets/script/tool.ts:
--------------------------------------------------------------------------------
1 | export type Tool = {
2 | type: ToolType;
3 | name: string;
4 | icon: string;
5 | link: string;
6 | };
7 |
8 | export type ToolList = Tool[];
9 | export type ToolMap = { [key: string]: Tool };
10 |
11 | export enum ToolTypes {
12 | BUILTIN,
13 | CUSTOM,
14 | }
15 | export type ToolType = ToolTypes.BUILTIN | ToolTypes.CUSTOM;
16 |
17 | export function contextTool(el: HTMLElement): number {
18 | let i = el.getAttribute("fy-index");
19 | if (i === null) {
20 | el = el.parentElement as HTMLElement;
21 | i = el.getAttribute("fy-index");
22 | if (i === null) return -1;
23 | }
24 |
25 | return parseInt(i);
26 | }
27 |
--------------------------------------------------------------------------------
/src/assets/script/utils/base.ts:
--------------------------------------------------------------------------------
1 | import type { Ref } from "vue";
2 |
3 | export type Element = HTMLElement | null;
4 |
5 | export function exportScript(name: string, conf: any) {
6 | const script = document.createElement("script");
7 | script.type = "text/javascript";
8 | script.innerHTML = `window.${name} = ${JSON.stringify(conf)};`;
9 | document.body.appendChild(script);
10 | }
11 |
12 | export function insertScript(src: string) {
13 | const script = document.createElement("script");
14 | script.src = src;
15 | script.async = true;
16 | script.defer = true;
17 | document.body.appendChild(script);
18 | }
19 |
20 | export function contain(
21 | el: Element,
22 | target: HTMLElement,
23 | exclude?: boolean,
24 | ): boolean {
25 | return el ? (exclude ? el == target : false) || el.contains(target) : false;
26 | }
27 |
28 | export function contains(
29 | els: Element[],
30 | target: HTMLElement,
31 | exclude?: boolean,
32 | ): boolean {
33 | return els.some((el: Element) => contain(el, target, exclude));
34 | }
35 |
36 | export function swap(arr: T[], i: number, j: number) {
37 | [arr[i], arr[j]] = [arr[j], arr[i]];
38 | }
39 |
40 | export function clipboard(text: string) {
41 | const el = document.createElement("textarea");
42 | el.value = text;
43 | document.body.appendChild(el);
44 | el.select();
45 | document.execCommand("copy");
46 | document.body.removeChild(el);
47 | }
48 |
49 | export function getSelectionText(): string | undefined {
50 | return window.getSelection ? window.getSelection()?.toString() : "";
51 | }
52 |
53 | export function getFavicon(url: string): string {
54 | try {
55 | const instance = new URL(url);
56 | return `${instance.origin}/favicon.ico`;
57 | } catch {
58 | return "/tool/unknown.svg";
59 | }
60 | }
61 |
62 | export function getListExcludeSelf(list: T[], self: T): T[] {
63 | return list.filter((item) => item !== self);
64 | }
65 |
66 | export function getValueOfRef(ref: Ref[]): T[] {
67 | return ref.map((item) => item.value);
68 | }
69 |
70 | export function getQueryVariable(variable: string): string | null {
71 | const query = window.location.search.substring(1);
72 | const vars = query.split("&");
73 | for (const v of vars) {
74 | const pair = v.split("=");
75 | if (pair[0] === variable) return pair[1];
76 | }
77 | return null;
78 | }
79 |
80 | export function DecimalConvert(n: number): string {
81 | if (n < 1000) return n.toString();
82 | if (n < 1000000) return `${(n / 1000).toFixed(1)}k`;
83 | if (n < 1000000000) return `${(n / 1000000).toFixed(1)}m`;
84 | if (n < 10000000000) return `${(n / 1000000000).toFixed(1)}b`;
85 | return n.toString();
86 | }
87 |
--------------------------------------------------------------------------------
/src/assets/script/utils/scroll.ts:
--------------------------------------------------------------------------------
1 | import { onMounted, ref } from "vue";
2 | import type { Ref } from "vue";
3 | import type { Element } from "@/assets/script/utils/base";
4 | import {
5 | contains,
6 | getListExcludeSelf,
7 | getValueOfRef,
8 | } from "@/assets/script/utils/base";
9 |
10 | const components: Ref[] = [];
11 |
12 | export function registerScrollableComponent(
13 | component: Ref,
14 | miniUnit?: boolean,
15 | ) {
16 | const start = ref(NaN);
17 | const animationFrame = ref(NaN);
18 |
19 | if (components.includes(component)) return;
20 | components.push(component);
21 |
22 | function detectReflect(e: TouchEvent): boolean {
23 | if (miniUnit || component.value === null) return false;
24 | return contains(
25 | getListExcludeSelf(getValueOfRef(components), component.value),
26 | e.target as HTMLElement,
27 | true,
28 | );
29 | }
30 |
31 | function animateScroll(scrollTop: number) {
32 | if (component.value === null) return;
33 |
34 | const scrollDiff = scrollTop - component.value.scrollTop;
35 | const step = Math.max(1, Math.abs(scrollDiff) / 10);
36 |
37 | if (Math.abs(scrollDiff) <= step) {
38 | component.value.scrollTop = scrollTop;
39 | cancelAnimationFrame(animationFrame.value);
40 | return;
41 | }
42 |
43 | if (scrollDiff > 0) {
44 | component.value.scrollTop += step;
45 | } else {
46 | component.value.scrollTop -= step;
47 | }
48 |
49 | animationFrame.value = requestAnimationFrame(() =>
50 | animateScroll(scrollTop),
51 | );
52 | }
53 |
54 | return onMounted(() => {
55 | if (component.value === null) return;
56 |
57 | component.value.addEventListener("touchstart", (e) => {
58 | if (detectReflect(e)) return;
59 | start.value = e.touches[0].clientY;
60 | });
61 |
62 | component.value.addEventListener("touchmove", (e) => {
63 | e.preventDefault();
64 | if (detectReflect(e)) return;
65 |
66 | if (component.value === null) return;
67 |
68 | const current = e.touches[0].clientY;
69 | const height = (current - start.value) * 4;
70 | start.value = current;
71 | const newScrollTop = component.value.scrollTop - height;
72 |
73 | cancelAnimationFrame(animationFrame.value);
74 | animateScroll(newScrollTop);
75 | });
76 | });
77 | }
78 |
--------------------------------------------------------------------------------
/src/assets/script/utils/service.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { useRegisterSW } from "virtual:pwa-register/vue";
3 | import { ref, watch } from "vue";
4 |
5 | export const version = "1.13.0";
6 |
7 | export const updater = ref(false);
8 |
9 | if (
10 | localStorage.getItem("version") &&
11 | localStorage.getItem("version") !== version
12 | ) {
13 | updater.value = true;
14 | }
15 | localStorage.setItem("version", version);
16 |
17 | watch(updater, () => {
18 | if (updater.value) setTimeout(() => (updater.value = false), 5000);
19 | });
20 |
21 | async function updateServiceVersion(r: ServiceWorkerRegistration) {
22 | try {
23 | const {
24 | // @ts-ignore
25 | onupdatefound,
26 | } = await r.update();
27 | if (onupdatefound) {
28 | updater.value = true;
29 | }
30 | } catch (e) {
31 | console.debug(e);
32 | }
33 | }
34 | const updateServiceWorker = useRegisterSW({
35 | onRegistered(r) {
36 | r && updateServiceVersion(r);
37 | r &&
38 | setInterval(
39 | async () => {
40 | await updateServiceVersion(r);
41 | },
42 | 1000 * 60 * 60,
43 | );
44 | },
45 | });
46 |
--------------------------------------------------------------------------------
/src/assets/script/utils/typing.ts:
--------------------------------------------------------------------------------
1 | import { ref } from "vue";
2 | import type { Ref } from "vue";
3 |
4 | function waitSync(ms: number): Promise {
5 | return new Promise((resolve) => setTimeout(resolve, ms));
6 | }
7 |
8 | export class TypingEffect {
9 | public operation: string;
10 | public timeout: number;
11 | public enableCursor: boolean;
12 | public ref: Ref;
13 | private cursor: boolean;
14 | private index: number;
15 | protected running: boolean;
16 | private offset: number;
17 | private readonly id: number;
18 | private readonly hook?: (...args: any) => any;
19 | private animationFrameId: number | null;
20 |
21 | constructor(
22 | operation: string,
23 | timeout: number = 800,
24 | enableCursor: boolean = false,
25 | selectRef?: Ref,
26 | hook?: (...args: any) => any,
27 | ) {
28 | this.operation = operation;
29 | this.timeout = timeout;
30 | this.enableCursor = enableCursor;
31 | this.ref = selectRef || ref("");
32 | this.cursor = true;
33 | this.index = 0;
34 | this.running = true;
35 | this.offset = 0;
36 | this.hook = hook;
37 | this.animationFrameId = null;
38 | this.id = Date.now();
39 |
40 | // Using public variables to solve the js thread memory non-sharing problem.
41 | // @ts-ignore
42 | window["typing"] = this.id;
43 | }
44 |
45 | private getTimeout(): number {
46 | if (this.index <= this.operation.length)
47 | return Math.random() * (this.enableCursor ? 200 : 100);
48 | return Math.random() * this.timeout;
49 | }
50 |
51 | protected async count(): Promise {
52 | this.index += 1;
53 | this.cursor = !this.cursor; // @ts-ignore
54 | if (!this.running || window["typing"] !== this.id) {
55 | return;
56 | }
57 | if (this.index <= this.operation.length) {
58 | this.ref.value =
59 | this.operation.substring(0, this.index) +
60 | (this.cursor && this.enableCursor ? "|" : " ");
61 | await waitSync(this.getTimeout());
62 | this.animationFrameId = requestAnimationFrame(() => this.count());
63 | } else {
64 | if (this.offset === 0) this.finish(true);
65 | if (this.enableCursor && this.offset <= 12) {
66 | this.ref.value = this.operation + (this.offset % 5 <= 1 ? "|" : " ");
67 | this.offset += 1;
68 | await waitSync(this.getTimeout());
69 | this.animationFrameId = requestAnimationFrame(() => this.count());
70 | } else {
71 | this.ref.value = this.operation;
72 | }
73 | }
74 | }
75 |
76 | public finish(status?: boolean): void {
77 | if (this.hook) this.hook(status);
78 | }
79 |
80 | public run(): Ref {
81 | this.animationFrameId = requestAnimationFrame(() => this.count());
82 | return this.ref;
83 | }
84 |
85 | public stop(): boolean {
86 | const status = this.running;
87 | this.running = false;
88 | this.animationFrameId && cancelAnimationFrame(this.animationFrameId);
89 | this.finish(false);
90 | return status;
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/assets/style/base.css:
--------------------------------------------------------------------------------
1 | /* color palette from */
2 | :root {
3 | --vt-c-white: #ffffff;
4 | --vt-c-white-soft: #f8f8f8;
5 | --vt-c-white-mute: #f2f2f2;
6 |
7 | --vt-c-black: #181818;
8 | --vt-c-black-soft: #222222;
9 | --vt-c-black-mute: #282828;
10 |
11 | --vt-c-indigo: #2c3e50;
12 |
13 | --vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
14 | --vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
15 | --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
16 | --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
17 |
18 | --vt-c-text-light-1: var(--vt-c-indigo);
19 | --vt-c-text-light-2: rgba(60, 60, 60, 0.66);
20 | --vt-c-text-dark-1: var(--vt-c-white);
21 | --vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
22 |
23 | --fonts: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
24 | Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
25 | --fonts-en: "Nunito", monospace;
26 | }
27 |
28 | /* semantic color variables for this project */
29 | :root {
30 | --color-background: var(--vt-c-black);
31 | --color-background-soft: var(--vt-c-black-soft);
32 | --color-background-mute: var(--vt-c-black-mute);
33 |
34 | --color-border: var(--vt-c-divider-dark-2);
35 | --color-border-hover: var(--vt-c-divider-dark-1);
36 |
37 | --color-heading: var(--vt-c-text-dark-1);
38 | --color-text: var(--vt-c-text-dark-2);
39 |
40 | --section-gap: 160px;
41 | --height-time-widget: min(80px, max(10vh, 30px));
42 | --height-input-box: min(180px, max(25vh, 100px));
43 | --height-tool-box: calc(var(--height-time-widget) + var(--height-input-box));
44 |
45 | }
46 |
47 | * {
48 | outline: none;
49 | -webkit-tap-highlight-color: transparent;
50 | }
51 |
52 | *,
53 | *::before,
54 | *::after {
55 | scrollbar-width: none;
56 | -webkit-overflow-scrolling: touch;
57 | -webkit-font-smoothing: antialiased;
58 | box-sizing: border-box;
59 | margin: 0;
60 | position: relative;
61 | font-weight: normal;
62 | touch-action: pan-y;
63 | }
64 |
65 | html, body, #app, main {
66 | z-index: -3;
67 | width: 100%;
68 | height: 100vh;
69 | }
70 |
71 | body {
72 | min-height: 100vh;
73 | color: var(--color-text);
74 | background: var(--color-background);
75 | transition: color 0.5s, background-color 0.5s;
76 | line-height: 1.6;
77 | font-family: var(--fonts);
78 | font-size: 15px;
79 | text-rendering: optimizeLegibility;
80 | -webkit-font-smoothing: antialiased;
81 | -moz-osx-font-smoothing: grayscale;
82 | }
83 |
84 | .grow {
85 | flex-grow: 1;
86 | }
87 |
88 | .link {
89 | text-decoration: none;
90 | color: #58a6ff;
91 | transition: 0.4s;
92 | padding: 1px 2px;
93 | margin: 0 1px;
94 | border-radius: 0;
95 | }
96 |
97 | @media (hover: hover) {
98 | .link:hover {
99 | background-color: rgba(88, 166, 255, .2);
100 | }
101 | }
102 |
103 |
104 | @keyframes FadeInAnimation {
105 | 0% {
106 | opacity: 0;
107 | }
108 | 100% {
109 | opacity: 1;
110 | }
111 | }
112 |
113 | ::-webkit-scrollbar {
114 | width: 5px;
115 | }
116 |
117 | ::-webkit-scrollbar-thumb:hover {
118 | background-color: #555;
119 | }
120 |
121 | ::-webkit-scrollbar-thumb {
122 | background-color: rgba(120, 120, 120, .5);
123 | border-radius: 3px;
124 | }
125 |
--------------------------------------------------------------------------------
/src/assets/style/card/weather.css:
--------------------------------------------------------------------------------
1 | #he-plugin-standard {
2 | padding: 4px !important;
3 | }
4 |
5 | .wv-v-h-row {
6 | animation: FadeInAnimation 0.5s ease-in-out;
7 | }
8 |
9 | .wv-lt-location a {
10 | margin-left: 4px !important;
11 | color: #58a6ff !important;
12 | background: rgba(88, 166, 255, 0.2) !important;
13 | border-radius: 4px !important;
14 | padding: 2px 4px !important;
15 | text-decoration: none !important;
16 | cursor: pointer;
17 | transition: 0.25s !important;
18 | }
19 |
20 | .wv-lt-location a:hover {
21 | background: rgba(88, 166, 255, 0.3) !important;
22 | }
23 |
24 | .wv-lt-refresh a {
25 | color: rgb(139, 191, 248) !important;
26 | cursor: pointer !important;
27 | transition: 0.25s !important;
28 | }
29 |
30 | .wv-lt-refresh a:hover {
31 | color: rgb(88, 166, 255) !important;
32 | }
33 |
34 | .wv-top-backdrop {
35 | position: fixed;
36 | top: 0;
37 | left: 0;
38 | z-index: 999;
39 | width: 100%;
40 | height: 100%;
41 | background-color: rgba(0, 0, 0, .5);
42 | backdrop-filter: blur(5px);
43 | -webkit-backdrop-filter: blur(5px);
44 | display: flex;
45 | flex-direction: column;
46 | justify-content: center;
47 | align-items: center;
48 | animation: FadeInAnimation 0.5s ease-in-out;
49 | }
50 |
51 | .wv-top-col-12 select.wv-top-select {
52 | width: 74px;
53 | height: 32px;
54 | border: 0;
55 | border-radius: 4px !important;
56 | padding: 2px 10px !important;
57 | margin: 2px 0 !important;
58 | background: var(--color-background);
59 | color: #ccc;
60 | font-size: 14px;
61 | font-family: var(--fonts);
62 | transition: 0.25s;
63 | }
64 |
65 | .wv-top-col-12 select.wv-top-select:hover {
66 | color: #fff;
67 | }
68 |
69 | .wv-top-col-12 select.wv-top-select option {
70 | background: var(--color-background);
71 | color: #ddd;
72 | font-size: 14px;
73 | font-family: var(--fonts);
74 | transition: 0.25s;
75 | }
76 |
77 | .wv-top-col-12 button.wv-top-button {
78 | width: 56px;
79 | height: 32px;
80 | border: 1px solid var(--color-border);
81 | border-radius: 4px;
82 | padding: 0 8px;
83 | margin-top: 8px;
84 | background: var(--color-background);
85 | color: #ccc;
86 | font-size: 14px;
87 | font-family: var(--fonts);
88 | transition: 0.25s;
89 | cursor: pointer;
90 | }
91 |
92 | .wv-top-col-12 button.wv-top-button:hover {
93 | color: #fff;
94 | border-color: var(--color-border-hover);
95 | }
96 |
--------------------------------------------------------------------------------
/src/assets/style/engine.css:
--------------------------------------------------------------------------------
1 | @keyframes size-animation {
2 | 0% {scale: 1}
3 | 50% {scale: 0.8}
4 | 100% {scale: 1}
5 | }
6 |
7 | .search-icon,
8 | .chat-icon {
9 | position: absolute;
10 | opacity: 0;
11 | height: 42px;
12 | width: 42px;
13 | transition: .2s;
14 | transition-delay: .05s;
15 | cursor: pointer;
16 | pointer-events: none;
17 | }
18 |
19 | .chat-icon {
20 | left: 2px;
21 | }
22 |
23 | .search-icon {
24 | right: 2px;
25 | }
26 |
27 | .chat-icon svg {
28 | fill: #70C001;
29 | }
30 |
31 | .chat-icon svg,
32 | .search-icon svg {
33 | width: 30px;
34 | height: 30px;
35 | padding: 6px;
36 | margin: 6px;
37 | border-radius: 50%;
38 | background: rgba(0,0,0,.2);
39 | transition: .25s ease;
40 | }
41 |
42 | .chat-icon:active svg,
43 | .search-icon:active svg {
44 | background: rgba(0,0,0,.4);
45 | scale: 0.8;
46 | }
47 |
48 | .chat-icon.clicked {
49 | animation: size-animation .25s ease;
50 | }
51 |
52 | .chat-icon.focus,
53 | .search-icon.focus {
54 | pointer-events: all;
55 | opacity: 1;
56 | }
57 |
58 | .window .engine .icon svg {
59 | margin: 0 4px;
60 | padding: 4px;
61 | transform: translateY(2px);
62 | width: 26px;
63 | height: 26px;
64 | fill: #fff;
65 | border-radius: 6px;
66 | transition: .25s;
67 | }
68 |
69 | .window .engine .check path {
70 | stroke: #58a6ff !important;
71 | }
72 |
73 |
74 | .engine-container .engine svg {
75 | width: 34px;
76 | height: 34px;
77 | padding: 8px;
78 | fill: #fff;
79 | }
80 |
--------------------------------------------------------------------------------
/src/components/About.vue:
--------------------------------------------------------------------------------
1 |
14 |
15 |
25 |
26 |
27 |

28 |
Fystart
29 |
{{ version }}
30 |
31 |
32 | 🍏 一言:在这里,你可以发现随机的名人名言,这些名言会给你带来灵感和启发,让你的每一天都充满动力!
33 |
34 | 🍋 自定义设置:Fystart 允许你根据自己的喜好进行自定义设置。你可以选择你喜欢的主题颜色、背景图片等,让浏览器起始页与你的个性完美融合!
35 |
36 | 🍎 AI 搜索建议:通过 Chat Nio 的智能搜索建议功能,Fystart 能够为你提供更快速、更准确的搜索结果,让你轻松找到你需要的信息,提高效率。
37 |
38 | 🍉 翻译 / Github 搜索:无论是需要翻译文字还是在 GitHub 上搜索代码,Fystart 都能够为你提供便捷的功能,让你在浏览器起始页上完成这些操作,提高工作效率。
39 |
40 | 🍇 工具箱:Fystart 内置了一系列实用工具,让你无需离开浏览器起始页,就能完成各种常用任务,方便实用。Fystart 还内置了很多常用软件,方便您快速打开。
41 |
42 | 🍐 搜索引擎建议:当你输入关键词时,Fystart 会为你提供相关的搜索引擎建议,帮助你快速定位到你想要的内容,省去了繁琐的搜索过程。
43 |
44 | 🎃 PWA 应用:Fystart 为 PWA 应用,这意味着可以在离线状态下访问,并且保存到桌面,无需依赖网络连接。
45 |
46 | ✨ 国际化支持:Fystart 支持多种语言,包括简体中文、繁体中文、英语、俄语、法语和日语,让用户在不同地区和语言环境下都能享受到优质的浏览器起始页。
47 |
48 | 我希望通过 Fystart,能够为您提供一个美观、实用、与个性相匹配的浏览器起始页,让你的工作、学习和生活更加高效和轻松。感谢您一直以来对Fystart的支持和陪伴👋!
49 |
50 |
64 |
65 |
84 |
85 |
96 |
97 | GitHub
103 | © Deeptrain Team
106 |
107 | 请遵守
108 |
MIT
114 | 开源协议分发
115 |
116 |
117 |
118 |
119 |
120 | {
121 | "en": {
122 | "about": "About"
123 | },
124 | "zh": {
125 | "about": "关于"
126 | },
127 | "tw": {
128 | "about": "關於"
129 | },
130 | "ru": {
131 | "about": "О сайте"
132 | },
133 | "fr": {
134 | "about": "Sur"
135 | },
136 | "de": {
137 | "about": "Über"
138 | },
139 | "ja": {
140 | "about": "について"
141 | }
142 | }
143 |
144 |
333 |
--------------------------------------------------------------------------------
/src/components/AutoUpdater.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 | {{ t("update")
12 | }}{{
13 | t("check")
14 | }}
15 |
16 |
17 |
18 |
19 | {
20 | "zh": {
21 | "update": "更新到最新版本!",
22 | "check": "查看更新内容"
23 | },
24 | "tw": {
25 | "update": "更新到最新版本!",
26 | "check": "查看更新內容"
27 | },
28 | "en": {
29 | "update": "Update to the latest version!",
30 | "check": "Check out the update"
31 | },
32 | "ru": {
33 | "update": "Обновить до последней версии!",
34 | "check": "Проверить обновление"
35 | },
36 | "de": {
37 | "update": "Aktualisieren Sie auf die neueste Version!",
38 | "check": "Überprüfen Sie das Update"
39 | },
40 | "fr": {
41 | "update": "Mettre à jour vers la dernière version!",
42 | "check": "Vérifier la mise à jour"
43 | },
44 | "ja": {
45 | "update": "最新バージョンに更新する!",
46 | "check": "更新を確認する"
47 | }
48 | }
49 |
50 |
--------------------------------------------------------------------------------
/src/components/Background.vue:
--------------------------------------------------------------------------------
1 |
22 |
23 |
24 |
32 |
33 |
34 |
35 |
88 |
--------------------------------------------------------------------------------
/src/components/CardContainer.vue:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
43 |
--------------------------------------------------------------------------------
/src/components/InputBox.vue:
--------------------------------------------------------------------------------
1 |
81 |
82 |
83 |
152 |
153 |
154 |
155 | {
156 | "en": {
157 | "search": "search"
158 | },
159 | "zh": {
160 | "search": "搜索"
161 | },
162 | "tw": {
163 | "search": "搜索"
164 | },
165 | "ru": {
166 | "search": "поиск"
167 | },
168 | "de": {
169 | "search": "Suche"
170 | },
171 | "fr": {
172 | "search": "recherche"
173 | },
174 | "ja": {
175 | "search": "検索"
176 | }
177 | }
178 |
179 |
180 |
365 |
--------------------------------------------------------------------------------
/src/components/Quote.vue:
--------------------------------------------------------------------------------
1 |
51 |
52 |
53 |
59 |
{{ effect }}
60 |
{{ verse }}
61 |
62 |
63 |
64 |
120 |
--------------------------------------------------------------------------------
/src/components/SettingWindow.vue:
--------------------------------------------------------------------------------
1 |
46 |
47 |
48 |
49 |
50 |
51 |
69 |
126 |
149 |
177 |
196 |
197 |
198 |
199 |
200 | {
201 | "en": {
202 | "settings": "Settings",
203 | "account": "Account",
204 | "login": "Login",
205 | "logout": "Logout",
206 | "general": "General",
207 | "display": "Display",
208 | "background": "Background",
209 | "search-engine": "Search Engine Preferences",
210 | "language": "Language",
211 | "input-background": "Input the background url.",
212 | "openai": "AI Search Suggestions",
213 | "openai-secret": "Please input the Chat Nio API Key",
214 | "time": "Exact Time",
215 | "time-desc": "Show the exact time on the search bar",
216 | "focus": "Auto Focus",
217 | "focus-desc": "Auto focus on the search bar when entering",
218 | "toolbox": "Tool Box",
219 | "quote": "Quote",
220 | "about": "About"
221 | },
222 | "zh": {
223 | "settings": "设置",
224 | "account": "账号",
225 | "login": "登录",
226 | "logout": "登出",
227 | "general": "常规设置",
228 | "display": "显示",
229 | "background": "背景",
230 | "search-engine": "搜索引擎偏好",
231 | "language": "语言",
232 | "input-background": "请输入背景图片的链接",
233 | "openai": "AI 搜索建议",
234 | "openai-secret": "请输入 Chat Nio API 密钥",
235 | "time": "精确时间",
236 | "time-desc": "搜索栏上方显示的时间精确到秒",
237 | "focus": "自动聚焦",
238 | "focus-desc": "进入时自动聚焦搜索栏",
239 | "toolbox": "工具箱",
240 | "quote": "一言",
241 | "about": "关于"
242 | },
243 | "tw": {
244 | "settings": "設定",
245 | "account": "帳號",
246 | "login": "登入",
247 | "logout": "登出",
248 | "general": "常規設定",
249 | "display": "顯示",
250 | "background": "背景",
251 | "search-engine": "搜尋引擎偏好",
252 | "language": "語言",
253 | "input-background": "請輸入背景圖片的連結",
254 | "openai": "AI 搜尋建議",
255 | "openai-secret": "請輸入 Chat Nio API 金鑰",
256 | "time": "精確時間",
257 | "time-desc": "搜尋欄上方顯示的時間精確到秒",
258 | "focus": "自動聚焦",
259 | "focus-desc": "進入時自動聚焦搜尋欄",
260 | "toolbox": "工具箱",
261 | "quote": "一言",
262 | "about": "關於"
263 | },
264 | "ru": {
265 | "settings": "Настройки",
266 | "account": "Аккаунт",
267 | "login": "Войти",
268 | "logout": "Выйти",
269 | "general": "Общие",
270 | "display": "Отображение",
271 | "background": "Фон",
272 | "search-engine": "Настройки поисковой системы",
273 | "language": "Язык",
274 | "input-background": "Введите URL-адрес фона",
275 | "openai": "Поисковые предложения AI",
276 | "openai-secret": "Пожалуйста, введите ключ API Chat Nio",
277 | "time": "Точное время",
278 | "time-desc": "Показывать точное время в строке поиска",
279 | "focus": "Автофокус",
280 | "focus-desc": "Автоматически фокусироваться на строке поиска при входе",
281 | "toolbox": "Инструменты",
282 | "quote": "Цитата",
283 | "about": "О программе"
284 | },
285 | "de": {
286 | "settings": "Einstellungen",
287 | "account": "Konto",
288 | "login": "Anmelden",
289 | "logout": "Abmelden",
290 | "general": "Allgemein",
291 | "display": "Anzeige",
292 | "background": "Hintergrund",
293 | "search-engine": "Suchmaschinenpräferenzen",
294 | "language": "Sprache",
295 | "input-background": "Geben Sie die URL des Hintergrunds ein",
296 | "openai": "AI-Suchvorschläge",
297 | "openai-secret": "Bitte geben Sie den Chat Nio API-Schlüssel ein",
298 | "time": "Exakte Zeit",
299 | "time-desc": "Zeigen Sie die genaue Zeit in der Suchleiste an",
300 | "focus": "Autofokus",
301 | "focus-desc": "Autofokus auf die Suchleiste beim Eingeben",
302 | "toolbox": "Werkzeugkasten",
303 | "quote": "Zitat",
304 | "about": "Über"
305 | },
306 | "fr": {
307 | "settings": "Paramètres",
308 | "account": "Compte",
309 | "login": "S'identifier",
310 | "logout": "Se déconnecter",
311 | "general": "Général",
312 | "display": "Affichage",
313 | "background": "Arrière-plan",
314 | "search-engine": "Préférences du moteur de recherche",
315 | "language": "Langue",
316 | "input-background": "Entrez l'URL de l'arrière-plan",
317 | "openai": "Suggestions de recherche AI",
318 | "openai-secret": "Veuillez saisir la clé API Chat Nio",
319 | "time": "Heure exacte",
320 | "time-desc": "Afficher l'heure exacte dans la barre de recherche",
321 | "focus": "Mise au point automatique",
322 | "focus-desc": "Mise au point automatique sur la barre de recherche lors de la saisie",
323 | "toolbox": "Boîte à outils",
324 | "quote": "Citation",
325 | "about": "À propos"
326 | },
327 | "ja": {
328 | "settings": "設定",
329 | "account": "アカウント",
330 | "login": "ログイン",
331 | "logout": "ログアウト",
332 | "general": "一般",
333 | "display": "ショー",
334 | "background": "背景",
335 | "search-engine": "検索エンジンの設定",
336 | "language": "言語",
337 | "input-background": "背景のURLを入力してください",
338 | "openai": "AI 検索の提案",
339 | "openai-secret": "Chat Nio API キーを入力してください",
340 | "time": "正確な時間",
341 | "time-desc": "検索バーに正確な時間を表示します",
342 | "focus": "オートフォーカス",
343 | "focus-desc": "入力時に検索バーに自動的にフォーカスします",
344 | "toolbox": "ツールボックス",
345 | "quote": "引用"
346 | }
347 | }
348 |
349 |
350 |
529 |
--------------------------------------------------------------------------------
/src/components/TimeWidget.vue:
--------------------------------------------------------------------------------
1 |
32 |
33 |
34 |
39 | {{ time }}
40 |
41 |
42 |
43 |
85 |
--------------------------------------------------------------------------------
/src/components/ToolBox.vue:
--------------------------------------------------------------------------------
1 |
169 |
170 |
171 |
175 |
176 |
181 |
207 |
208 |
235 |
236 |
237 | {
238 | "zh": {
239 | "add": "添加工具",
240 | "edit": "编辑工具",
241 | "name": "名称",
242 | "name-desc": "编辑工具名称",
243 | "link": "链接",
244 | "icon": "图标",
245 | "icon-desc": "图标链接 (留空自动获取), https:// 或 data:image/png;base64",
246 | "cancel": "取消",
247 | "save": "保存"
248 | },
249 | "en": {
250 | "add": "Add Tool",
251 | "edit": "Edit Tool",
252 | "name": "Name",
253 | "name-desc": "Edit tool name",
254 | "link": "Link",
255 | "icon": "Icon",
256 | "icon-desc": "Icon link, https:// or data:image/png;base64",
257 | "cancel": "Cancel",
258 | "save": "Save"
259 | },
260 | "tw": {
261 | "add": "新增工具",
262 | "edit": "編輯工具",
263 | "name": "名稱",
264 | "name-desc": "編輯工具名稱",
265 | "link": "連結",
266 | "icon": "圖示",
267 | "icon-desc": "圖示連結, https:// 或 data:image/png;base64",
268 | "cancel": "取消",
269 | "save": "保存"
270 | },
271 | "ru": {
272 | "add": "Добавить инструмент",
273 | "edit": "Редактировать инструмент",
274 | "name": "Название",
275 | "name-desc": "Редактировать название инструмента",
276 | "link": "Ссылка",
277 | "icon": "Иконка",
278 | "icon-desc": "Ссылка на иконку, https:// или data:image/png;base64",
279 | "cancel": "Отмена",
280 | "save": "Сохранить"
281 | },
282 | "de": {
283 | "add": "Werkzeug hinzufügen",
284 | "edit": "Werkzeug bearbeiten",
285 | "name": "Name",
286 | "name-desc": "Werkzeugnamen bearbeiten",
287 | "link": "Link",
288 | "icon": "Symbol",
289 | "icon-desc": "Symbol-Link, https:// oder data:image/png;base64",
290 | "cancel": "Abbrechen",
291 | "save": "Speichern"
292 | },
293 | "fr": {
294 | "add": "Ajouter un outil",
295 | "edit": "Modifier l'outil",
296 | "name": "Nom",
297 | "name-desc": "Modifier le nom de l'outil",
298 | "link": "Lien",
299 | "icon": "Icône",
300 | "icon-desc": "Lien de l'icône, https:// ou data:image/png;base64",
301 | "cancel": "Annuler",
302 | "save": "Enregistrer"
303 | },
304 | "ja": {
305 | "add": "ツールを追加",
306 | "edit": "ツールを編集",
307 | "name": "名前",
308 | "name-desc": "ツール名を編集する",
309 | "link": "リンク",
310 | "icon": "アイコン",
311 | "icon-desc": "アイコンのリンク、https://またはdata:image/png;base64",
312 | "cancel": "キャンセル",
313 | "save": "保存"
314 | }
315 | }
316 |
317 |
318 |
465 |
--------------------------------------------------------------------------------
/src/components/cards/DateCard.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 | 日历
13 | {{ data.solar }}
14 | {{ data.zodiac }}
15 |
16 |
17 | {{ data.day }}
18 | {{ data.weekday }}
19 | {{ data.ganzhi }}
20 |
21 |
22 | {{ data.lunar }}
23 |
24 |
25 |
26 |
27 |
128 |
--------------------------------------------------------------------------------
/src/components/cards/GithubCard.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 | GitHub
15 |
16 |
17 |
18 | Explore
19 |
20 |
21 |
22 |
23 |
24 |
30 |
{{ repo.description }}
31 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
245 |
--------------------------------------------------------------------------------
/src/components/cards/WeatherCard.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
15 |
16 |
17 |
36 |
--------------------------------------------------------------------------------
/src/components/compositions/Checkbox.vue:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
41 |
--------------------------------------------------------------------------------
/src/components/compositions/Cover.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
14 |
15 |
16 |
34 |
--------------------------------------------------------------------------------
/src/components/compositions/Notification.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
37 |
--------------------------------------------------------------------------------
/src/components/compositions/Suggestion.vue:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 |
16 |
17 | {{ props.content }}
18 |
19 |
20 |
21 |
22 |
23 |
63 |
--------------------------------------------------------------------------------
/src/components/compositions/Tool.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 | {{ props.title }}
13 |
14 |
15 |
16 |
74 |
--------------------------------------------------------------------------------
/src/components/compositions/Window.vue:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 |
{{ title }}
16 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
122 |
199 |
--------------------------------------------------------------------------------
/src/components/icons/box.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
--------------------------------------------------------------------------------
/src/components/icons/chat.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
--------------------------------------------------------------------------------
/src/components/icons/check.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
--------------------------------------------------------------------------------
/src/components/icons/clock.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
--------------------------------------------------------------------------------
/src/components/icons/close.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
--------------------------------------------------------------------------------
/src/components/icons/cursor.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
--------------------------------------------------------------------------------
/src/components/icons/delete.vue:
--------------------------------------------------------------------------------
1 |
2 |
24 |
25 |
--------------------------------------------------------------------------------
/src/components/icons/edit.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
--------------------------------------------------------------------------------
/src/components/icons/github.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/src/components/icons/info.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
--------------------------------------------------------------------------------
/src/components/icons/international.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/src/components/icons/loader.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
--------------------------------------------------------------------------------
/src/components/icons/note.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
--------------------------------------------------------------------------------
/src/components/icons/openai.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/src/components/icons/qq.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/src/components/icons/search.vue:
--------------------------------------------------------------------------------
1 |
2 |
19 |
20 |
--------------------------------------------------------------------------------
/src/components/icons/settings.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
--------------------------------------------------------------------------------
/src/components/icons/star.vue:
--------------------------------------------------------------------------------
1 |
2 |
15 |
16 |
--------------------------------------------------------------------------------
/src/i18n/engine.ts:
--------------------------------------------------------------------------------
1 | const messages = {
2 | en: {
3 | baidu: "Baidu",
4 | google: "Google",
5 | bing: "Bing",
6 | duckduckgo: "DuckDuckGo",
7 | sogou: "Sogou",
8 | },
9 | zh: {
10 | baidu: "百度",
11 | google: "谷歌",
12 | bing: "必应",
13 | duckduckgo: "DuckDuckGo",
14 | sogou: "搜狗",
15 | },
16 | tw: {
17 | baidu: "百度",
18 | google: "谷歌",
19 | bing: "必應",
20 | duckduckgo: "DuckDuckGo",
21 | sogou: "搜狗",
22 | },
23 | ru: {
24 | baidu: "Baidu",
25 | google: "Google",
26 | bing: "Bing",
27 | duckduckgo: "DuckDuckGo",
28 | sogou: "Sogou",
29 | },
30 | de: {
31 | baidu: "Baidu",
32 | google: "Google",
33 | bing: "Bing",
34 | duckduckgo: "DuckDuckGo",
35 | sogou: "Sogou",
36 | },
37 | fr: {
38 | baidu: "Baidu",
39 | google: "Google",
40 | bing: "Bing",
41 | duckduckgo: "DuckDuckGo",
42 | sogou: "Sogou",
43 | },
44 | ja: {
45 | baidu: "Baidu",
46 | google: "Google",
47 | bing: "Bing",
48 | duckduckgo: "DuckDuckGo",
49 | sogou: "Sogou",
50 | },
51 | };
52 | export default messages;
53 |
--------------------------------------------------------------------------------
/src/i18n/index.ts:
--------------------------------------------------------------------------------
1 | import { createI18n } from "vue-i18n";
2 | import { storage } from "@/assets/script/storage";
3 |
4 | const messages = {
5 | en: {},
6 | zh: {},
7 | };
8 |
9 | const i18n = createI18n({
10 | legacy: false,
11 | locale: storage.language,
12 | fallbackLocale: "en",
13 | messages,
14 | });
15 |
16 | export default i18n;
17 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { createApp } from "vue";
2 | import App from "./App.vue";
3 | import i18n from "@/i18n";
4 | import { endpoint } from "@/assets/script/config";
5 | import "@/assets/script/auth";
6 | import "./assets/style/base.css";
7 | import axios from "axios";
8 |
9 | const app = createApp(App);
10 |
11 | axios.defaults.headers.post["Content-Type"] = "application/json";
12 | axios.defaults.baseURL = endpoint;
13 |
14 | app.use(i18n);
15 | app.mount("#app");
16 |
--------------------------------------------------------------------------------
/src/types/calendar.d.ts:
--------------------------------------------------------------------------------
1 | declare module "lunar-calendar" {
2 | interface LunarDate {
3 | GanZhiDay: string;
4 | GanZhiMonth: string;
5 | GanZhiYear: string;
6 | lunarDay: number;
7 | lunarDayName: string;
8 | lunarFestival?: string;
9 | lunarLeapMonth: number;
10 | lunarMonth: number;
11 | lunarMonthName: string;
12 | lunarYear: number;
13 | }
14 |
15 | interface SolarDate {
16 | solarFestival?: string;
17 | term?: string;
18 | worktime: number;
19 | zodiac: string;
20 | }
21 |
22 | interface CalendarData extends LunarDate, SolarDate {}
23 |
24 | export function solarToLunar(
25 | year: number,
26 | month: number,
27 | day: number,
28 | ): CalendarData;
29 | }
30 |
--------------------------------------------------------------------------------
/src/types/pwa.d.ts:
--------------------------------------------------------------------------------
1 | declare module "virtual:pwa-register/vue" {
2 | // eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
3 | // @ts-expect-error ignore when vue is not installed
4 | import type { Ref } from "vue";
5 | import type { RegisterSWOptions } from "vite-plugin-pwa/types";
6 |
7 | export type { RegisterSWOptions };
8 |
9 | export function useRegisterSW(options?: RegisterSWOptions): {
10 | needRefresh: Ref;
11 | offlineReady: Ref;
12 | updateServiceWorker: (reloadPage?: boolean) => Promise;
13 | };
14 | }
15 |
--------------------------------------------------------------------------------
/src/types/vue.d.ts:
--------------------------------------------------------------------------------
1 | declare module "vue" {
2 | export interface Ref {
3 | value: T;
4 | }
5 | export function ref(value: T): Ref;
6 | export function reactive(value: T): T;
7 | export function computed(value: T): Ref;
8 | export function onMounted(callback: () => void): void;
9 | export function watch(
10 | ref: Record,
11 | callback: (value: T) => void,
12 | ): void;
13 | export function createApp(...args: any[]): any;
14 | }
15 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@vue/tsconfig/tsconfig.web.json",
3 | "include": [
4 | "env.d.ts",
5 | "src/**/*",
6 | "src/**/*.vue",
7 | "src/types/*.d.ts"
8 | ],
9 | "compilerOptions": {
10 | "baseUrl": "../fystart",
11 | "paths": {
12 | "@/*": ["./src/*"]
13 | },
14 | "strict": true,
15 | "types": [
16 | "vite-plugin-pwa/client",
17 | ]
18 | },
19 | "references": [
20 | {
21 | "path": "./tsconfig.node.json"
22 | }
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@vue/tsconfig/tsconfig.node.json",
3 | "include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "playwright.config.*", "plugins/*.*"],
4 | "compilerOptions": {
5 | "composite": true,
6 | "types": ["node"]
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { fileURLToPath, URL } from 'node:url'
2 |
3 | import { defineConfig } from 'vite'
4 | import vue from '@vitejs/plugin-vue'
5 | import { createHtmlPlugin } from 'vite-plugin-html'
6 | import { VitePWA } from "vite-plugin-pwa";
7 | import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite';
8 |
9 | // https://vitejs.dev/config/
10 | export default defineConfig({
11 | plugins: [
12 | vue(),
13 | VueI18nPlugin({
14 | include: [fileURLToPath(new URL('./src', import.meta.url)) + '/**/*.vue'],
15 | }),
16 | VitePWA({
17 | registerType: 'autoUpdate',
18 | injectRegister: 'inline',
19 | includeAssets: ['favicon.ico', 'icon.png', 'background.webp'],
20 | manifest: {
21 | name: 'Fystart',
22 | short_name: 'Fystart',
23 | theme_color: '#1E1E1EFF',
24 | icons: [{
25 | src: 'icon.png',
26 | sizes: '192x192',
27 | type: 'image/png',
28 | }],
29 | },
30 | workbox: {
31 | clientsClaim: true,
32 | skipWaiting: true,
33 | cleanupOutdatedCaches: true,
34 | globPatterns: ['**/*.{js,css,html,webp,png}'],
35 | globDirectory: 'dist',
36 | runtimeCaching: [{
37 | urlPattern: new RegExp('^https://open.lightxi.com/'),
38 | handler: "CacheFirst",
39 | options: {
40 | cacheName: "lightxi-cdn",
41 | expiration: {
42 | maxEntries: 10,
43 | maxAgeSeconds: 60 * 60 * 24 * 365,
44 | }
45 | }
46 | }],
47 | },
48 | devOptions: {
49 | enabled: true,
50 | }
51 | }),
52 | createHtmlPlugin({
53 | minify: true,
54 | }),
55 | ],
56 | build: {
57 | manifest: true,
58 | rollupOptions: {
59 | output: {
60 | entryFileNames: `assets/[name].[hash].js`,
61 | chunkFileNames: `assets/[name].[hash].js`,
62 | },
63 | },
64 | },
65 | resolve: {
66 | alias: {
67 | '@': fileURLToPath(new URL('./src', import.meta.url)),
68 | }
69 | }
70 | })
71 |
--------------------------------------------------------------------------------