├── .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 | ![fystart](/public/favicon.ico) 4 | # [Fyrrum Start · 极目起始页](https://fystart.com/) 5 | 6 | [![GitHub stars](https://img.shields.io/github/stars/Deeptrain-Community/fystart?style=flat-square)](https://fystart.com) 7 | [![GitHub forks](https://img.shields.io/github/forks/Deeptrain-Community/fystart?style=flat-square)](https://fystart.com) 8 | [![GitHub issues](https://img.shields.io/github/issues/Deeptrain-Community/fystart?style=flat-square)](https://fystart.com) 9 | [![GitHub license](https://img.shields.io/github/license/Deeptrain-Community/fystart?style=flat-square)](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 | ![main](/screenshot/main.png) 58 | 59 | ![search](/screenshot/search.png) 60 | 61 | ![customize](/screenshot/customize.png) 62 | 63 | ![settings](/screenshot/settings.png) 64 | 65 | ![engine](/screenshot/engine.png) 66 | 67 | ![i18n](/screenshot/i18n.png) 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 | Codepen 2 | -------------------------------------------------------------------------------- /public/tool/convert.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/tool/github.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/tool/jsdelivr.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/tool/kaggle.svg: -------------------------------------------------------------------------------- 1 | Kaggle 2 | -------------------------------------------------------------------------------- /public/tool/lightnotes.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmh-program/fystart/d3013fc1daede8e84520743a544a619d2ea9ecb8/public/tool/lightnotes.ico -------------------------------------------------------------------------------- /public/tool/openai.svg: -------------------------------------------------------------------------------- 1 | AI 2 | -------------------------------------------------------------------------------- /public/tool/readthedocs.svg: -------------------------------------------------------------------------------- 1 | Read the Docs 2 | -------------------------------------------------------------------------------- /public/tool/replit.svg: -------------------------------------------------------------------------------- 1 | replit 2 | -------------------------------------------------------------------------------- /public/tool/stackoverflow.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/tool/twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/tool/unknown.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/tool/vercel.svg: -------------------------------------------------------------------------------- 1 | Vercel 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 | 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 | 'Baidu', 25 | bing: 'Microsoft Bing', 26 | google: 27 | 'Google', 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: 'GitHub', 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 | 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 | 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 | 34 | 35 | 88 | -------------------------------------------------------------------------------- /src/components/CardContainer.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 18 | 19 | 43 | -------------------------------------------------------------------------------- /src/components/InputBox.vue: -------------------------------------------------------------------------------- 1 | 81 | 82 | 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 | 63 | 64 | 120 | -------------------------------------------------------------------------------- /src/components/SettingWindow.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 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 | 42 | 43 | 85 | -------------------------------------------------------------------------------- /src/components/ToolBox.vue: -------------------------------------------------------------------------------- 1 | 169 | 170 | 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 | 26 | 27 | 128 | -------------------------------------------------------------------------------- /src/components/cards/GithubCard.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 44 | 45 | 245 | -------------------------------------------------------------------------------- /src/components/cards/WeatherCard.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | 17 | 36 | -------------------------------------------------------------------------------- /src/components/compositions/Checkbox.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | 18 | 41 | -------------------------------------------------------------------------------- /src/components/compositions/Cover.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 15 | 16 | 34 | -------------------------------------------------------------------------------- /src/components/compositions/Notification.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 37 | -------------------------------------------------------------------------------- /src/components/compositions/Suggestion.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 22 | 23 | 63 | -------------------------------------------------------------------------------- /src/components/compositions/Tool.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 15 | 16 | 74 | -------------------------------------------------------------------------------- /src/components/compositions/Window.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 27 | 122 | 199 | -------------------------------------------------------------------------------- /src/components/icons/box.vue: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /src/components/icons/chat.vue: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /src/components/icons/check.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /src/components/icons/clock.vue: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /src/components/icons/close.vue: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /src/components/icons/cursor.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /src/components/icons/delete.vue: -------------------------------------------------------------------------------- 1 | 25 | -------------------------------------------------------------------------------- /src/components/icons/edit.vue: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /src/components/icons/github.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /src/components/icons/info.vue: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /src/components/icons/international.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /src/components/icons/loader.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /src/components/icons/note.vue: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /src/components/icons/openai.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /src/components/icons/qq.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /src/components/icons/search.vue: -------------------------------------------------------------------------------- 1 | 20 | -------------------------------------------------------------------------------- /src/components/icons/settings.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /src/components/icons/star.vue: -------------------------------------------------------------------------------- 1 | 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 | --------------------------------------------------------------------------------