Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eea826dd5c | |||
| 3c5b0ea90a | |||
| 15e1037433 | |||
| 5da4c98f05 | |||
| a73ee7aefc | |||
| 6c5bea9caf | |||
| 93ba2f4fab | |||
| 9417797b99 | |||
| 167e5596f2 | |||
| 2a1e34fe03 | |||
| 3de00cf4a8 | |||
| 1d55545e30 | |||
| 9b5a555d1f |
@@ -10,23 +10,19 @@
|
||||
|
||||
<p align="center">
|
||||
<a href="https://memos.onrender.com/">Live Demo</a> •
|
||||
<a href="https://github.com/usememos/memos/discussions">Discussions</a>
|
||||
<a href="https://t.me/+-_tNF1k70UU4ZTc9">Discuss in Telegram 👾</a>
|
||||
</p>
|
||||
|
||||

|
||||
|
||||
## 🎯 Intentions
|
||||
|
||||
- ✍️ Write down the light-card memos very easily;
|
||||
- 🏗️ Build the fragmented knowledge management tool for yourself;
|
||||
- 📒 For noting your 📅 daily/weekly plans, 💡 fantastic ideas, 📕 reading thoughts...
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- 🦄 Fully open source;
|
||||
- 👍 Write in the plain textarea without any burden;
|
||||
- 🤠 Great UI and never miss any detail;
|
||||
- 🚀 Super quick self-hosted with `Docker` and `SQLite`;
|
||||
- 📜 Writing in plain textarea without any burden,
|
||||
- and support some useful markdown syntax 💪.
|
||||
- 🌄 Share the memo in a pretty image or personal page like Twitter;
|
||||
- 🚀 Fast self-hosting with `Docker`;
|
||||
- 🤠 Pleasant UI and UX;
|
||||
|
||||
## ⚓️ Deploy with Docker
|
||||
|
||||
@@ -42,7 +38,7 @@ docker run \
|
||||
|
||||
Memos should now be running at [http://localhost:5230](http://localhost:5230). If the `~/.memos/` does not have a `memos_prod.db` file, then `memos` will auto generate it.
|
||||
|
||||
⚠️ Please DO NOT use `dev` of docker image if you have no experience.
|
||||
⚠️ Please DO NOT use `dev` tag of docker image if you have no experience.
|
||||
|
||||
## 🏗 Development
|
||||
|
||||
|
||||
@@ -7,10 +7,10 @@ import (
|
||||
|
||||
// Version is the service current released version.
|
||||
// Semantic versioning: https://semver.org/
|
||||
var Version = "0.2.0"
|
||||
var Version = "0.2.2"
|
||||
|
||||
// DevVersion is the service current development version.
|
||||
var DevVersion = "0.2.0"
|
||||
var DevVersion = "0.2.2"
|
||||
|
||||
func GetCurrentVersion(mode string) string {
|
||||
if mode == "dev" {
|
||||
|
||||
@@ -38,8 +38,7 @@ func checkDSN(dataDir string) (string, error) {
|
||||
dataDir = strings.TrimRight(dataDir, "/")
|
||||
|
||||
if _, err := os.Stat(dataDir); err != nil {
|
||||
error := fmt.Errorf("unable to access -data %s, err %w", dataDir, err)
|
||||
return "", error
|
||||
return "", fmt.Errorf("unable to access data folder %s, err %w", dataDir, err)
|
||||
}
|
||||
|
||||
return dataDir, nil
|
||||
@@ -67,6 +66,7 @@ func GetProfile() *Profile {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
profile.Data = dataDir
|
||||
profile.DSN = fmt.Sprintf("%s/memos_%s.db", dataDir, profile.Mode)
|
||||
profile.Version = common.GetCurrentVersion(profile.Mode)
|
||||
|
||||
|
||||
@@ -6,9 +6,11 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/usememos/memos/common"
|
||||
"github.com/usememos/memos/server/profile"
|
||||
@@ -24,37 +26,33 @@ var seedFS embed.FS
|
||||
|
||||
type DB struct {
|
||||
// sqlite db connection instance
|
||||
Db *sql.DB
|
||||
// datasource name
|
||||
DSN string
|
||||
// mode should be prod or dev
|
||||
mode string
|
||||
Db *sql.DB
|
||||
profile *profile.Profile
|
||||
}
|
||||
|
||||
// NewDB returns a new instance of DB associated with the given datasource name.
|
||||
func NewDB(profile *profile.Profile) *DB {
|
||||
db := &DB{
|
||||
DSN: profile.DSN,
|
||||
mode: profile.Mode,
|
||||
profile: profile,
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func (db *DB) Open() (err error) {
|
||||
// Ensure a DSN is set before attempting to open the database.
|
||||
if db.DSN == "" {
|
||||
if db.profile.DSN == "" {
|
||||
return fmt.Errorf("dsn required")
|
||||
}
|
||||
|
||||
// Connect to the database.
|
||||
sqlDB, err := sql.Open("sqlite3", db.DSN)
|
||||
sqlDB, err := sql.Open("sqlite3", db.profile.DSN)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open db with dsn: %s, err: %w", db.DSN, err)
|
||||
return fmt.Errorf("failed to open db with dsn: %s, err: %w", db.profile.DSN, err)
|
||||
}
|
||||
|
||||
db.Db = sqlDB
|
||||
// If mode is dev, we should migrate and seed the database.
|
||||
if db.mode == "dev" {
|
||||
if db.profile.Mode == "dev" {
|
||||
if err := db.applyLatestSchema(); err != nil {
|
||||
return fmt.Errorf("failed to apply latest schema: %w", err)
|
||||
}
|
||||
@@ -63,7 +61,7 @@ func (db *DB) Open() (err error) {
|
||||
}
|
||||
} else {
|
||||
// If db file not exists, we should migrate the database.
|
||||
if _, err := os.Stat(db.DSN); errors.Is(err, os.ErrNotExist) {
|
||||
if _, err := os.Stat(db.profile.DSN); errors.Is(err, os.ErrNotExist) {
|
||||
err := db.applyLatestSchema()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to apply latest schema: %w", err)
|
||||
@@ -74,7 +72,7 @@ func (db *DB) Open() (err error) {
|
||||
return fmt.Errorf("failed to create migration_history table: %w", err)
|
||||
}
|
||||
|
||||
currentVersion := common.GetCurrentVersion(db.mode)
|
||||
currentVersion := common.GetCurrentVersion(db.profile.Mode)
|
||||
migrationHistory, err := findMigrationHistory(db.Db, &MigrationHistoryFind{})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -90,18 +88,34 @@ func (db *DB) Open() (err error) {
|
||||
|
||||
if common.IsVersionGreaterThan(currentVersion, migrationHistory.Version) {
|
||||
minorVersionList := getMinorVersionList()
|
||||
|
||||
// backup the raw database file before migration
|
||||
rawBytes, err := ioutil.ReadFile(db.profile.DSN)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read raw database file, err: %w", err)
|
||||
}
|
||||
backupDBFilePath := fmt.Sprintf("%s/memos_%s_%d_backup.db", db.profile.Data, db.profile.Version, time.Now().Unix())
|
||||
if err := ioutil.WriteFile(backupDBFilePath, rawBytes, 0644); err != nil {
|
||||
return fmt.Errorf("failed to write raw database file, err: %w", err)
|
||||
}
|
||||
|
||||
println("succeed to copy a backup database file")
|
||||
println("start migrate")
|
||||
for _, minorVersion := range minorVersionList {
|
||||
normalizedVersion := minorVersion + ".0"
|
||||
if common.IsVersionGreaterThan(normalizedVersion, migrationHistory.Version) && common.IsVersionGreaterOrEqualThan(currentVersion, normalizedVersion) {
|
||||
println("applying migration for", normalizedVersion)
|
||||
err := db.applyMigrationForMinorVersion(minorVersion)
|
||||
if err != nil {
|
||||
if err := db.applyMigrationForMinorVersion(minorVersion); err != nil {
|
||||
return fmt.Errorf("failed to apply minor version migration: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println("end migrate")
|
||||
// remove the created backup db file after migrate succeed
|
||||
if err := os.Remove(backupDBFilePath); err != nil {
|
||||
println(fmt.Sprintf("Failed to remove temp database file, err %v", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<meta name="theme-color" content="#f6f5f4" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" />
|
||||
<title>Memos</title>
|
||||
<script src="https://kit.fontawesome.com/41e3aaa6af.js" crossorigin="anonymous"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "memos",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.2",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
@@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "^1.8.1",
|
||||
"axios": "^0.27.2",
|
||||
"dayjs": "^1.11.3",
|
||||
"lodash-es": "^4.17.21",
|
||||
"qs": "^6.11.0",
|
||||
"react": "^18.1.0",
|
||||
|
||||
|
Before Width: | Height: | Size: 2.5 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M22.5 38V25.5H10V22.5H22.5V10H25.5V22.5H38V25.5H25.5V38Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 137 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M28.05 36 16 23.95 28.05 11.9l2.15 2.15-9.9 9.9 9.9 9.9Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 137 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="m18.75 36-2.15-2.15 9.9-9.9-9.9-9.9 2.15-2.15L30.8 23.95Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 138 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#37352f"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V5h14v14zM17.99 9l-1.41-1.42-6.59 6.59-2.58-2.57-1.42 1.41 4 3.99z"/></svg>
|
||||
|
Before Width: | Height: | Size: 308 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#37352f"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"/></svg>
|
||||
|
Before Width: | Height: | Size: 249 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M6.1 44 4 41.9 18.9 27H9v-3h15v15h-3v-9.9ZM24 24V9h3v9.9L41.9 4 44 6.1 29.1 21H39v3Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 165 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M12.45 37.65 10.35 35.55 21.9 24 10.35 12.45 12.45 10.35 24 21.9 35.55 10.35 37.65 12.45 26.1 24 37.65 35.55 35.55 37.65 24 26.1Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 210 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M11 40q-1.2 0-2.1-.9Q8 38.2 8 37v-7.15h3V37h26v-7.15h3V37q0 1.2-.9 2.1-.9.9-2.1.9Zm13-7.65-9.65-9.65 2.15-2.15 6 6V8h3v18.55l6-6 2.15 2.15Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 220 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M9 39H11.2L33.35 16.85L31.15 14.65L9 36.8ZM39.7 14.7 33.3 8.3 35.4 6.2Q36.25 5.35 37.5 5.35Q38.75 5.35 39.6 6.2L41.8 8.4Q42.65 9.25 42.65 10.5Q42.65 11.75 41.8 12.6ZM37.6 16.8 12.4 42H6V35.6L31.2 10.4ZM32.25 15.75 31.15 14.65 33.35 16.85Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 319 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-4.86 8.86l-3 3.87L9 13.14 6 17h12l-3.86-5.14z"/></svg>
|
||||
|
Before Width: | Height: | Size: 296 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#37352f"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/></svg>
|
||||
|
Before Width: | Height: | Size: 204 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#FFFFFF"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M6 10c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm12 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm-6 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/></svg>
|
||||
|
Before Width: | Height: | Size: 306 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#37352f"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M6 10c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm12 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm-6 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/></svg>
|
||||
|
Before Width: | Height: | Size: 306 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M6 42V27h3v9.9L36.9 9H27V6h15v15h-3v-9.9L11.1 39H21v3Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 135 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M31.7 25.6 36 29.45V32.45H25.5V44.5L24 46L22.5 44.5V32.45H12V29.45L16 25.6V9H13.5V6H34.2V9H31.7ZM16.05 29.45H31.65L28.7 26.7V9H19V26.7ZM23.85 29.45Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 229 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#37352f"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>
|
||||
|
Before Width: | Height: | Size: 393 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M36.35 44Q34 44 32.325 42.325Q30.65 40.65 30.65 38.3Q30.65 37.95 30.725 37.475Q30.8 37 30.95 36.6L15.8 27.8Q15.05 28.65 13.95 29.175Q12.85 29.7 11.7 29.7Q9.35 29.7 7.675 28.025Q6 26.35 6 24Q6 21.6 7.675 19.95Q9.35 18.3 11.7 18.3Q12.85 18.3 13.9 18.75Q14.95 19.2 15.8 20.05L30.95 11.35Q30.8 11 30.725 10.55Q30.65 10.1 30.65 9.7Q30.65 7.3 32.325 5.65Q34 4 36.35 4Q38.75 4 40.4 5.65Q42.05 7.3 42.05 9.7Q42.05 12.05 40.4 13.725Q38.75 15.4 36.35 15.4Q35.2 15.4 34.125 15.025Q33.05 14.65 32.3 13.8L17.15 22.2Q17.25 22.6 17.325 23.125Q17.4 23.65 17.4 24Q17.4 24.35 17.325 24.75Q17.25 25.15 17.15 25.55L32.3 34.15Q33.05 33.45 34.05 33.025Q35.05 32.6 36.35 32.6Q38.75 32.6 40.4 34.25Q42.05 35.9 42.05 38.3Q42.05 40.65 40.4 42.325Q38.75 44 36.35 44ZM36.35 12.4Q37.5 12.4 38.275 11.625Q39.05 10.85 39.05 9.7Q39.05 8.55 38.275 7.775Q37.5 7 36.35 7Q35.2 7 34.425 7.775Q33.65 8.55 33.65 9.7Q33.65 10.85 34.425 11.625Q35.2 12.4 36.35 12.4ZM11.7 26.7Q12.85 26.7 13.625 25.925Q14.4 25.15 14.4 24Q14.4 22.85 13.625 22.075Q12.85 21.3 11.7 21.3Q10.55 21.3 9.775 22.075Q9 22.85 9 24Q9 25.15 9.775 25.925Q10.55 26.7 11.7 26.7ZM36.35 41Q37.5 41 38.275 40.225Q39.05 39.45 39.05 38.3Q39.05 37.15 38.275 36.375Q37.5 35.6 36.35 35.6Q35.2 35.6 34.425 36.375Q33.65 37.15 33.65 38.3Q33.65 39.45 34.425 40.225Q35.2 41 36.35 41ZM36.35 9.7Q36.35 9.7 36.35 9.7Q36.35 9.7 36.35 9.7Q36.35 9.7 36.35 9.7Q36.35 9.7 36.35 9.7Q36.35 9.7 36.35 9.7Q36.35 9.7 36.35 9.7Q36.35 9.7 36.35 9.7Q36.35 9.7 36.35 9.7ZM11.7 24Q11.7 24 11.7 24Q11.7 24 11.7 24Q11.7 24 11.7 24Q11.7 24 11.7 24Q11.7 24 11.7 24Q11.7 24 11.7 24Q11.7 24 11.7 24Q11.7 24 11.7 24ZM36.35 38.3Q36.35 38.3 36.35 38.3Q36.35 38.3 36.35 38.3Q36.35 38.3 36.35 38.3Q36.35 38.3 36.35 38.3Q36.35 38.3 36.35 38.3Q36.35 38.3 36.35 38.3Q36.35 38.3 36.35 38.3Q36.35 38.3 36.35 38.3Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><g><rect fill="none" height="24" width="24"/></g><g><path d="M20,10V8h-4V4h-2v4h-4V4H8v4H4v2h4v4H4v2h4v4h2v-4h4v4h2v-4h4v-2h-4v-4H20z M14,14h-4v-4h4V14z"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 301 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M24 31.5q3.55 0 6.025-2.475Q32.5 26.55 32.5 23q0-3.55-2.475-6.025Q27.55 14.5 24 14.5q-3.55 0-6.025 2.475Q15.5 19.45 15.5 23q0 3.55 2.475 6.025Q20.45 31.5 24 31.5Zm0-2.9q-2.35 0-3.975-1.625T18.4 23q0-2.35 1.625-3.975T24 17.4q2.35 0 3.975 1.625T29.6 23q0 2.35-1.625 3.975T24 28.6Zm0 9.4q-7.3 0-13.2-4.15Q4.9 29.7 2 23q2.9-6.7 8.8-10.85Q16.7 8 24 8q7.3 0 13.2 4.15Q43.1 16.3 46 23q-2.9 6.7-8.8 10.85Q31.3 38 24 38Zm0-15Zm0 12q6.05 0 11.125-3.275T42.85 23q-2.65-5.45-7.725-8.725Q30.05 11 24 11t-11.125 3.275Q7.8 17.55 5.1 23q2.7 5.45 7.775 8.725Q17.95 35 24 35Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 638 B |
@@ -37,7 +37,7 @@ const AboutSiteDialog: React.FC<Props> = ({ destroy }: Props) => {
|
||||
<span className="icon-text">🤠</span>About <b>Memos</b>
|
||||
</p>
|
||||
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
||||
<img className="icon-img" src="/icons/close.svg" />
|
||||
<i className="fa-solid fa-xmark fa-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div className="dialog-content-container">
|
||||
|
||||
@@ -37,7 +37,7 @@ const ArchivedMemoDialog: React.FC<Props> = (props: Props) => {
|
||||
Archived Memos
|
||||
</p>
|
||||
<button className="btn close-btn" onClick={destroy}>
|
||||
<img className="icon-img" src="/icons/close.svg" />
|
||||
<i className="fa-solid fa-xmark fa-lg icon-img"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div className="dialog-content-container">
|
||||
|
||||
@@ -70,7 +70,7 @@ const ChangePasswordDialog: React.FC<Props> = ({ destroy }: Props) => {
|
||||
<div className="dialog-header-container">
|
||||
<p className="title-text">Change Password</p>
|
||||
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
||||
<img className="icon-img" src="/icons/close.svg" />
|
||||
<i className="fa-solid fa-xmark fa-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div className="dialog-content-container">
|
||||
|
||||
@@ -41,7 +41,7 @@ const ConfirmResetOpenIdDialog: React.FC<Props> = ({ destroy }: Props) => {
|
||||
<div className="dialog-header-container">
|
||||
<p className="title-text">Reset Open API</p>
|
||||
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
||||
<img className="icon-img" src="/icons/close.svg" />
|
||||
<i className="fa-solid fa-xmark fa-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div className="dialog-content-container">
|
||||
|
||||
@@ -100,7 +100,7 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
|
||||
{shortcutId ? "Edit Shortcut" : "Create Shortcut"}
|
||||
</p>
|
||||
<button className="btn close-btn" onClick={destroy}>
|
||||
<img className="icon-img" src="/icons/close.svg" />
|
||||
<i className="fa-solid fa-xmark fa-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div className="dialog-content-container">
|
||||
@@ -296,7 +296,7 @@ const FilterInputer: React.FC<MemoFilterInputerProps> = (props: MemoFilterInpute
|
||||
/>
|
||||
|
||||
{inputElements}
|
||||
<img className="remove-btn" src="/icons/close.svg" onClick={handleRemoveBtnClick} />
|
||||
<i className="fa-solid fa-xmark remove-btn" onClick={handleRemoveBtnClick}></i>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -64,16 +64,16 @@ const DailyReviewDialog: React.FC<Props> = (props: Props) => {
|
||||
</p>
|
||||
<div className="btns-container">
|
||||
<button className="btn-text" onClick={() => setCurrentDateStamp(currentDateStamp - DAILY_TIMESTAMP)}>
|
||||
<img className="icon-img" src="/icons/arrow-left.svg" />
|
||||
<i className="fa-solid fa-chevron-left icon-img"></i>
|
||||
</button>
|
||||
<button className="btn-text" onClick={() => setCurrentDateStamp(currentDateStamp + DAILY_TIMESTAMP)}>
|
||||
<img className="icon-img" src="/icons/arrow-right.svg" />
|
||||
<i className="fa-solid fa-chevron-right icon-img"></i>
|
||||
</button>
|
||||
<button className="btn-text share-btn" onClick={handleShareBtnClick}>
|
||||
<img className="icon-img" src="/icons/share.svg" />
|
||||
<button className="btn-text" onClick={handleShareBtnClick}>
|
||||
<i className="fa-solid fa-share-nodes icon-img"></i>
|
||||
</button>
|
||||
<button className="btn-text" onClick={() => props.destroy()}>
|
||||
<img className="icon-img" src="/icons/close.svg" />
|
||||
<i className="fa-solid fa-xmark fa-lg icon-img"></i>
|
||||
</button>
|
||||
</div>
|
||||
<DatePicker
|
||||
|
||||
@@ -13,18 +13,17 @@ const GitHubBadge: React.FC<Props> = () => {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleClick = () => {
|
||||
window.location.href = "https://github.com/usememos/memos";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="github-badge-container" onClick={handleClick}>
|
||||
<a className="github-badge-container" href="https://github.com/usememos/memos">
|
||||
<div className="github-icon">
|
||||
<img className="icon-img" src="/github.webp" alt="" />
|
||||
<i className="fa-brands fa-github fa-lg icon-img"></i>
|
||||
Star
|
||||
</div>
|
||||
<span className={`count-text ${starCount || "pulse"}`}>{starCount || "🌟"}</span>
|
||||
</div>
|
||||
<div className="count-text">
|
||||
{starCount || ""}
|
||||
<span className="icon-text">🌟</span>
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { memo, useEffect, useRef, useState } from "react";
|
||||
import { escape, indexOf } from "lodash-es";
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import { IMAGE_URL_REG, LINK_URL_REG, MEMO_LINK_REG, TAG_REG, UNKNOWN_ID } from "../helpers/consts";
|
||||
import * as utils from "../helpers/utils";
|
||||
import { DONE_BLOCK_REG, parseMarkedToHtml, TODO_BLOCK_REG } from "../helpers/marked";
|
||||
import { editorStateService, locationService, memoService, userService } from "../services";
|
||||
import Only from "./common/OnlyWhen";
|
||||
@@ -11,29 +12,37 @@ import showMemoCardDialog from "./MemoCardDialog";
|
||||
import showShareMemoImageDialog from "./ShareMemoImageDialog";
|
||||
import "../less/memo.less";
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
const MAX_MEMO_CONTAINER_HEIGHT = 384;
|
||||
|
||||
type ExpandButtonStatus = -1 | 0 | 1;
|
||||
|
||||
interface Props {
|
||||
memo: Memo;
|
||||
}
|
||||
|
||||
type ExpandButtonStatus = -1 | 0 | 1;
|
||||
|
||||
interface State {
|
||||
expandButtonStatus: ExpandButtonStatus;
|
||||
}
|
||||
|
||||
export const getFormatedMemoCreatedAtStr = (createdTs: number): string => {
|
||||
if (Date.now() - createdTs < 1000 * 60 * 60 * 24) {
|
||||
return dayjs(createdTs).fromNow();
|
||||
} else {
|
||||
return dayjs(createdTs).format("YYYY/MM/DD HH:mm:ss");
|
||||
}
|
||||
};
|
||||
|
||||
const Memo: React.FC<Props> = (props: Props) => {
|
||||
const { memo: propsMemo } = props;
|
||||
const memo = {
|
||||
...propsMemo,
|
||||
createdAtStr: utils.getDateTimeString(propsMemo.createdTs),
|
||||
};
|
||||
const memo = props.memo;
|
||||
const [state, setState] = useState<State>({
|
||||
expandButtonStatus: -1,
|
||||
});
|
||||
const [createdAtStr, setCreatedAtStr] = useState<string>(getFormatedMemoCreatedAtStr(memo.createdTs));
|
||||
const memoContainerRef = useRef<HTMLDivElement>(null);
|
||||
const imageUrls = Array.from(memo.content.match(IMAGE_URL_REG) ?? []).map((s) => s.replace(IMAGE_URL_REG, "$1"));
|
||||
const isVisitorMode = userService.isVisitorMode();
|
||||
|
||||
useEffect(() => {
|
||||
if (!memoContainerRef) {
|
||||
@@ -46,6 +55,12 @@ const Memo: React.FC<Props> = (props: Props) => {
|
||||
expandButtonStatus: 0,
|
||||
});
|
||||
}
|
||||
|
||||
if (Date.now() - memo.createdTs < 1000 * 60 * 60 * 24) {
|
||||
setInterval(() => {
|
||||
setCreatedAtStr(dayjs(memo.createdTs).fromNow());
|
||||
}, 1000 * 1);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleShowMemoStoryDialog = () => {
|
||||
@@ -146,9 +161,8 @@ const Memo: React.FC<Props> = (props: Props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleShowMoreBtnClick = () => {
|
||||
const handleExpandBtnClick = () => {
|
||||
setState({
|
||||
...state,
|
||||
expandButtonStatus: Number(Boolean(!state.expandButtonStatus)) as ExpandButtonStatus,
|
||||
});
|
||||
};
|
||||
@@ -156,32 +170,32 @@ const Memo: React.FC<Props> = (props: Props) => {
|
||||
return (
|
||||
<div className={`memo-wrapper ${"memos-" + memo.id} ${memo.pinned ? "pinned" : ""}`}>
|
||||
<div className="memo-top-wrapper">
|
||||
<span className="time-text" onClick={handleShowMemoStoryDialog}>
|
||||
{memo.createdAtStr}
|
||||
<div className="status-text-container" onClick={handleShowMemoStoryDialog}>
|
||||
<span className="time-text">{createdAtStr}</span>
|
||||
<Only when={memo.pinned}>
|
||||
<span className="ml-2">PINNED</span>
|
||||
<span className="status-text">PINNED</span>
|
||||
</Only>
|
||||
<Only when={memo.visibility === "PUBLIC"}>
|
||||
<span className="ml-2">PUBLIC</span>
|
||||
<Only when={memo.visibility === "PUBLIC" && !isVisitorMode}>
|
||||
<span className="status-text">PUBLIC</span>
|
||||
</Only>
|
||||
</span>
|
||||
</div>
|
||||
<div className={`btns-container ${userService.isVisitorMode() ? "!hidden" : ""}`}>
|
||||
<span className="btn more-action-btn">
|
||||
<img className="icon-img" src="/icons/more.svg" />
|
||||
<i className="fa-solid fa-ellipsis icon-img"></i>
|
||||
</span>
|
||||
<div className="more-action-btns-wrapper">
|
||||
<div className="more-action-btns-container">
|
||||
<div className="btns-container">
|
||||
<div className="btn" onClick={handleTogglePinMemoBtnClick}>
|
||||
<img className="icon-img" src="/icons/pin.svg" alt="" />
|
||||
<i className={`fa-solid fa-thumbtack icon-img ${memo.pinned ? "" : "opacity-20"}`}></i>
|
||||
<span className="tip-text">{memo.pinned ? "Unpin" : "Pin"}</span>
|
||||
</div>
|
||||
<div className="btn" onClick={handleEditMemoClick}>
|
||||
<img className="icon-img" src="/icons/edit.svg" alt="" />
|
||||
<i className="fa-solid fa-pen-to-square icon-img"></i>
|
||||
<span className="tip-text">Edit</span>
|
||||
</div>
|
||||
<div className="btn" onClick={handleGenMemoImageBtnClick}>
|
||||
<img className="icon-img" src="/icons/share.svg" alt="" />
|
||||
<i className="fa-solid fa-share-nodes icon-img"></i>
|
||||
<span className="tip-text">Share</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -206,9 +220,9 @@ const Memo: React.FC<Props> = (props: Props) => {
|
||||
></div>
|
||||
{state.expandButtonStatus !== -1 && (
|
||||
<div className="expand-btn-container">
|
||||
<span className={`btn ${state.expandButtonStatus === 0 ? "expand-btn" : "fold-btn"}`} onClick={handleShowMoreBtnClick}>
|
||||
<span className={`btn ${state.expandButtonStatus === 0 ? "expand-btn" : "fold-btn"}`} onClick={handleExpandBtnClick}>
|
||||
{state.expandButtonStatus === 0 ? "Expand" : "Fold"}
|
||||
<img className="icon-img" src="/icons/arrow-right.svg" alt="" />
|
||||
<i className="fa-solid fa-chevron-right icon-img"></i>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { editorStateService, memoService, userService } from "../services";
|
||||
import { IMAGE_URL_REG, MEMO_LINK_REG, UNKNOWN_ID } from "../helpers/consts";
|
||||
import * as utils from "../helpers/utils";
|
||||
import { editorStateService, memoService, userService } from "../services";
|
||||
import { parseHtmlToRawText } from "../helpers/marked";
|
||||
import Only from "./common/OnlyWhen";
|
||||
import toastHelper from "./Toast";
|
||||
@@ -103,6 +103,18 @@ const MemoCardDialog: React.FC<Props> = (props: Props) => {
|
||||
editorStateService.setEditMemoWithId(memo.id);
|
||||
}, [memo.id]);
|
||||
|
||||
const handlePinClick = async () => {
|
||||
if (memo.pinned) {
|
||||
await memoService.unpinMemo(memo.id);
|
||||
} else {
|
||||
await memoService.pinMemo(memo.id);
|
||||
}
|
||||
setMemo({
|
||||
...memo,
|
||||
pinned: !memo.pinned,
|
||||
});
|
||||
};
|
||||
|
||||
const handleVisibilityClick = async () => {
|
||||
const visibility = memo.visibility === "PRIVATE" ? "PUBLIC" : "PRIVATE";
|
||||
await memoService.patchMemo({
|
||||
@@ -123,16 +135,19 @@ const MemoCardDialog: React.FC<Props> = (props: Props) => {
|
||||
<div className="btns-container">
|
||||
<Only when={!userService.isVisitorMode()}>
|
||||
<>
|
||||
<button className="btn edit-btn" onClick={handleVisibilityClick}>
|
||||
<img className={`icon-img ${memo.visibility === "PRIVATE" ? "opacity-30" : ""}`} src="/icons/visibility.svg" />
|
||||
<button className="btn" onClick={handlePinClick}>
|
||||
<i className={`fa-solid fa-thumbtack icon-img ${memo.pinned ? "" : "opacity-20"}`}></i>
|
||||
</button>
|
||||
<button className="btn" onClick={handleVisibilityClick}>
|
||||
<i className={`fa-solid fa-eye icon-img ${memo.visibility === "PUBLIC" ? "" : "opacity-20"}`}></i>
|
||||
</button>
|
||||
<button className="btn edit-btn" onClick={handleEditMemoBtnClick}>
|
||||
<img className="icon-img" src="/icons/edit.svg" />
|
||||
<i className="fa-solid fa-pen-to-square icon-img"></i>
|
||||
</button>
|
||||
</>
|
||||
</Only>
|
||||
<button className="btn close-btn" onClick={props.destroy}>
|
||||
<img className="icon-img" src="/icons/close.svg" />
|
||||
<i className="fa-solid fa-xmark fa-lg icon-img"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -231,7 +231,7 @@ const MemoEditor: React.FC<Props> = () => {
|
||||
tools={
|
||||
<>
|
||||
<div className="action-btn tag-action">
|
||||
<img className="icon-img" src="/icons/tag.svg" />
|
||||
<i className="fa-solid fa-hashtag icon-img"></i>
|
||||
<div ref={tagSeletorRef} className="tag-list" onClick={handleTagSeletorClick}>
|
||||
{tags.map((t) => {
|
||||
return <span key={t}>{t}</span>;
|
||||
@@ -239,11 +239,11 @@ const MemoEditor: React.FC<Props> = () => {
|
||||
</div>
|
||||
</div>
|
||||
<button className="action-btn">
|
||||
<img className="icon-img" src="/icons/image.svg" onClick={handleUploadFileBtnClick} />
|
||||
<i className="fa-solid fa-image icon-img" onClick={handleUploadFileBtnClick}></i>
|
||||
<span className={`tip-text ${state.isUploadingResource ? "!block" : ""}`}>Uploading</span>
|
||||
</button>
|
||||
<button className="action-btn" onClick={handleFullscreenBtnClick}>
|
||||
<img className="icon-img" src={`/icons/${state.fullscreen ? "close" : "open"}-fullscreen.svg`} alt="" />
|
||||
<i className={`fa-solid fa-${state.fullscreen ? "compress" : "expand"} icon-img`}></i>
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ const MemosHeader: React.FC<Props> = () => {
|
||||
<div className="section-header-container memos-header-container">
|
||||
<div className="title-container">
|
||||
<div className="action-btn" onClick={toggleSiderbar}>
|
||||
<img src="/icons/menu.svg" className="icon-img" alt="" />
|
||||
<i className="fa-solid fa-bars icon-img"></i>
|
||||
</div>
|
||||
<span className="title-text" onClick={handleTitleTextClick}>
|
||||
{titleText}
|
||||
|
||||
@@ -22,10 +22,10 @@ const PreviewImageDialog: React.FC<Props> = ({ destroy, imgUrl }: Props) => {
|
||||
<>
|
||||
<div className="btns-container">
|
||||
<button className="btn" onClick={handleCloseBtnClick}>
|
||||
<img className="icon-img" src="/icons/close.svg" />
|
||||
<i className="fa-solid fa-xmark fa-lg icon-img"></i>
|
||||
</button>
|
||||
<button className="btn" onClick={handleDownloadBtnClick}>
|
||||
<img className="icon-img" src="/icons/download.svg" />
|
||||
<i className="fa-solid fa-download icon-img"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div className="img-container">
|
||||
|
||||
@@ -24,7 +24,7 @@ const SearchBar: React.FC<Props> = () => {
|
||||
return (
|
||||
<div className="search-bar-container">
|
||||
<div className="search-bar-inputer">
|
||||
<img className="icon-img" src="/icons/search.svg" />
|
||||
<i className="fa-solid fa-magnifying-glass fa-sm icon-img"></i>
|
||||
<input className="text-input" type="text" placeholder="" onChange={handleTextQueryInput} />
|
||||
</div>
|
||||
<div className="quickly-action-wrapper">
|
||||
|
||||
@@ -30,7 +30,7 @@ const SettingDialog: React.FC<Props> = (props: Props) => {
|
||||
return (
|
||||
<div className="dialog-content-container">
|
||||
<button className="btn close-btn" onClick={destroy}>
|
||||
<img className="icon-img" src="/icons/close.svg" />
|
||||
<i className="fa-solid fa-xmark fa-lg icon-img"></i>
|
||||
</button>
|
||||
<div className="section-selector-container">
|
||||
<span className="section-title">Basic</span>
|
||||
|
||||
@@ -75,12 +75,12 @@ const ShareMemoImageDialog: React.FC<Props> = (props: Props) => {
|
||||
<span className="icon-text">🌄</span>Share Memo
|
||||
</p>
|
||||
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
||||
<img className="icon-img" src="/icons/close.svg" />
|
||||
<i className="fa-solid fa-xmark fa-lg icon-img"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div className="dialog-content-container">
|
||||
<div className={`tip-words-container ${shortcutImgUrl ? "finish" : "loading"}`}>
|
||||
<p className="tip-text">{shortcutImgUrl ? "Click or press to save the image 👇" : "Generating the screenshot..."}</p>
|
||||
<p className="tip-text">{shortcutImgUrl ? "Click to save the image 👇" : "Generating the screenshot..."}</p>
|
||||
</div>
|
||||
<div className="memo-container" ref={memoElRef}>
|
||||
<Only when={shortcutImgUrl !== ""}>
|
||||
|
||||
@@ -39,7 +39,7 @@ const ShortcutList: React.FC<Props> = () => {
|
||||
<p className="title-text">
|
||||
<span className="normal-text">Shortcuts</span>
|
||||
<button className="btn" onClick={() => showCreateShortcutDialog()}>
|
||||
<img src="/icons/add.svg" alt="add shortcut" />
|
||||
<i className="fa-solid fa-plus icon-img fa-xs"></i>
|
||||
</button>
|
||||
</p>
|
||||
<div className="shortcuts-container">
|
||||
@@ -113,7 +113,7 @@ const ShortcutContainer: React.FC<ShortcutContainerProps> = (props: ShortcutCont
|
||||
</div>
|
||||
<div className="btns-container">
|
||||
<span className="action-btn toggle-btn">
|
||||
<img className="icon-img" src="/icons/more.svg" />
|
||||
<i className="fa-solid fa-ellipsis fa-sm icon-img"></i>
|
||||
</span>
|
||||
<div className="action-btns-wrapper">
|
||||
<div className="action-btns-container">
|
||||
|
||||
@@ -27,7 +27,7 @@ const Sidebar: React.FC<Props> = () => {
|
||||
<aside className="sidebar-wrapper">
|
||||
<div className="close-container">
|
||||
<span className="action-btn" onClick={toggleSiderbar}>
|
||||
<img src="/icons/close.svg" className="icon-img" alt="" />
|
||||
<i className="fa-solid fa-xmark fa-lg icon-img"></i>
|
||||
</span>
|
||||
</div>
|
||||
<UserBanner />
|
||||
|
||||
@@ -116,7 +116,7 @@ const TagItemContainer: React.FC<TagItemContainerProps> = (props: TagItemContain
|
||||
<div className="btns-container">
|
||||
{hasSubTags ? (
|
||||
<span className={`action-btn toggle-btn ${showSubTags ? "shown" : ""}`} onClick={handleToggleBtnClick}>
|
||||
<img className="icon-img" src="/icons/arrow-right.svg" />
|
||||
<i className="fa-solid fa-chevron-right icon-img"></i>
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -27,7 +27,7 @@ const UserBanner: React.FC<Props> = () => {
|
||||
setUsername(user.name);
|
||||
setCreatedDays(Math.ceil((Date.now() - utils.getTimeStampByDate(user.createdTs)) / 1000 / 3600 / 24));
|
||||
}
|
||||
}, []);
|
||||
}, [isVisitorMode, user, owner]);
|
||||
|
||||
const handleUsernameClick = useCallback(() => {
|
||||
locationService.clearQuery();
|
||||
@@ -45,11 +45,11 @@ const UserBanner: React.FC<Props> = () => {
|
||||
{!isVisitorMode && user?.role === "HOST" ? <span className="tag">MOD</span> : null}
|
||||
</div>
|
||||
<button className="action-btn menu-popup-btn" onClick={handlePopupBtnClick}>
|
||||
<img src="/icons/more.svg" className="icon-img" />
|
||||
<i className="fa-solid fa-ellipsis icon-img"></i>
|
||||
</button>
|
||||
<MenuBtnsPopup shownStatus={shouldShowPopupBtns} setShownStatus={setShouldShowPopupBtns} />
|
||||
</div>
|
||||
<div className="status-text-container">
|
||||
<div className="amount-text-container">
|
||||
<div className="status-text memos-text">
|
||||
<span className="amount-text">{memos.length}</span>
|
||||
<span className="type-text">MEMO</span>
|
||||
|
||||
@@ -55,13 +55,13 @@ const DatePicker: React.FC<DatePickerProps> = (props: DatePickerProps) => {
|
||||
<div className={`date-picker-wrapper ${className}`}>
|
||||
<div className="date-picker-header">
|
||||
<span className="btn-text" onClick={() => handleChangeMonthBtnClick(-1)}>
|
||||
<img className="icon-img" src="/icons/arrow-left.svg" />
|
||||
<i className="fa-solid fa-chevron-left icon-img"></i>
|
||||
</span>
|
||||
<span className="normal-text">
|
||||
{firstDate.getFullYear()}/{firstDate.getMonth() + 1}
|
||||
</span>
|
||||
<span className="btn-text" onClick={() => handleChangeMonthBtnClick(1)}>
|
||||
<img className="icon-img" src="/icons/arrow-right.svg" />
|
||||
<i className="fa-solid fa-chevron-right icon-img"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div className="date-picker-day-container">
|
||||
|
||||
@@ -64,7 +64,7 @@ const Selector: React.FC<Props> = (props: Props) => {
|
||||
<div className={`current-value-container ${showSelector ? "active" : ""}`} onClick={handleCurrentValueClick}>
|
||||
<span className="value-text">{currentItem.text}</span>
|
||||
<span className="arrow-text">
|
||||
<img className="icon-img" src="/icons/arrow-right.svg" />
|
||||
<i className="fa-solid fa-chevron-down fa-sm icon-img"></i>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,34 +1,33 @@
|
||||
import convertResourceToDataURL from "./convertResourceToDataURL";
|
||||
|
||||
const applyStyles = async (sourceElement: HTMLElement, clonedElement: HTMLElement) => {
|
||||
if (!sourceElement || !clonedElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sourceElement.tagName === "IMG") {
|
||||
try {
|
||||
const url = await convertResourceToDataURL(sourceElement.getAttribute("src") ?? "");
|
||||
(clonedElement as HTMLImageElement).src = url;
|
||||
} catch (error) {
|
||||
// do nth
|
||||
}
|
||||
}
|
||||
|
||||
const sourceStyles = window.getComputedStyle(sourceElement);
|
||||
for (const item of sourceStyles) {
|
||||
clonedElement.style.setProperty(item, sourceStyles.getPropertyValue(item), sourceStyles.getPropertyPriority(item));
|
||||
}
|
||||
|
||||
for (let i = 0; i < clonedElement.childElementCount; i++) {
|
||||
await applyStyles(sourceElement.children[i] as HTMLElement, clonedElement.children[i] as HTMLElement);
|
||||
}
|
||||
};
|
||||
|
||||
const getCloneStyledElement = async (element: HTMLElement) => {
|
||||
const clonedElementContainer = document.createElement(element.tagName);
|
||||
clonedElementContainer.innerHTML = element.innerHTML;
|
||||
|
||||
const applyStyles = async (sourceElement: HTMLElement, clonedElement: HTMLElement) => {
|
||||
if (!sourceElement || !clonedElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceStyles = window.getComputedStyle(sourceElement);
|
||||
|
||||
if (sourceElement.tagName === "IMG") {
|
||||
try {
|
||||
const url = await convertResourceToDataURL(sourceElement.getAttribute("src") ?? "");
|
||||
(clonedElement as HTMLImageElement).src = url;
|
||||
} catch (error) {
|
||||
// do nth
|
||||
}
|
||||
}
|
||||
|
||||
for (const item of sourceStyles) {
|
||||
clonedElement.style.setProperty(item, sourceStyles.getPropertyValue(item), sourceStyles.getPropertyPriority(item));
|
||||
}
|
||||
|
||||
for (let i = 0; i < clonedElement.childElementCount; i++) {
|
||||
await applyStyles(sourceElement.children[i] as HTMLElement, clonedElement.children[i] as HTMLElement);
|
||||
}
|
||||
};
|
||||
|
||||
await applyStyles(element, clonedElementContainer);
|
||||
|
||||
return clonedElementContainer;
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
* 2. <foreignObject>: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/foreignObject
|
||||
*/
|
||||
import getCloneStyledElement from "./getCloneStyledElement";
|
||||
import waitImageLoaded from "./waitImageLoaded";
|
||||
|
||||
type Options = Partial<{
|
||||
backgroundColor: string;
|
||||
@@ -51,7 +52,6 @@ const generateSVGElement = (width: number, height: number, element: HTMLElement)
|
||||
|
||||
export const toSVG = async (element: HTMLElement, options?: Options) => {
|
||||
const { width, height } = getElementSize(element);
|
||||
|
||||
const clonedElement = await getCloneStyledElement(element);
|
||||
|
||||
if (options?.backgroundColor) {
|
||||
@@ -65,11 +65,6 @@ export const toSVG = async (element: HTMLElement, options?: Options) => {
|
||||
};
|
||||
|
||||
export const toCanvas = async (element: HTMLElement, options?: Options): Promise<HTMLCanvasElement> => {
|
||||
const url = await toSVG(element, options);
|
||||
|
||||
const imageEl = new Image();
|
||||
imageEl.src = url;
|
||||
|
||||
const ratio = options?.pixelRatio || 1;
|
||||
const { width, height } = getElementSize(element);
|
||||
|
||||
@@ -91,18 +86,20 @@ export const toCanvas = async (element: HTMLElement, options?: Options): Promise
|
||||
context.fillRect(0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
imageEl.onload = () => {
|
||||
context.drawImage(imageEl, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
resolve(canvas);
|
||||
};
|
||||
});
|
||||
const url = await toSVG(element, options);
|
||||
const imageEl = new Image();
|
||||
imageEl.style.zIndex = "-1";
|
||||
imageEl.style.position = "fixed";
|
||||
imageEl.style.top = "0";
|
||||
document.body.append(imageEl);
|
||||
await waitImageLoaded(imageEl, url);
|
||||
context.drawImage(imageEl, 0, 0, canvas.width, canvas.height);
|
||||
imageEl.remove();
|
||||
return canvas;
|
||||
};
|
||||
|
||||
const toImage = async (element: HTMLElement, options?: Options) => {
|
||||
const canvas = await toCanvas(element, options);
|
||||
|
||||
return canvas.toDataURL();
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
const waitImageLoaded = (image: HTMLImageElement, url: string): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
image.loading = "eager";
|
||||
image.onload = () => {
|
||||
// NOTE: There is image loading problem in Safari, fix it with some trick
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, 200);
|
||||
};
|
||||
image.onerror = () => {
|
||||
reject("Image load failed");
|
||||
};
|
||||
image.src = url;
|
||||
});
|
||||
};
|
||||
|
||||
export default waitImageLoaded;
|
||||
@@ -1,33 +1,17 @@
|
||||
@import "../mixin.less";
|
||||
|
||||
.date-picker-wrapper {
|
||||
.flex(column, flex-start, flex-start);
|
||||
padding: 16px;
|
||||
@apply flex flex-col justify-start items-start p-4;
|
||||
|
||||
> .date-picker-header {
|
||||
.flex(row, center, center);
|
||||
width: 100%;
|
||||
@apply flex flex-row justify-center items-center w-full mb-2;
|
||||
|
||||
> .btn-text {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
> .icon-img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: @bg-whitegray;
|
||||
}
|
||||
@apply w-6 h-6 rounded cursor-pointer select-none flex flex-col justify-center items-center opacity-40 hover:bg-gray-200;
|
||||
}
|
||||
|
||||
> .normal-text {
|
||||
margin: 0 4px;
|
||||
line-height: 24px;
|
||||
@apply mx-1 leading-6 font-mono;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
@import "../mixin.less";
|
||||
|
||||
.selector-wrapper {
|
||||
.flex(column, flex-start, flex-start);
|
||||
@apply relative h-7;
|
||||
@apply flex flex-col justify-start items-start relative h-7;
|
||||
|
||||
> .current-value-container {
|
||||
.flex(row, space-between, center);
|
||||
@apply w-full h-full rounded px-2 pr-1 bg-white border cursor-pointer select-none;
|
||||
@apply flex flex-row justify-between items-center w-full h-full rounded px-2 pr-1 bg-white border cursor-pointer select-none;
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
@@ -19,26 +17,23 @@
|
||||
}
|
||||
|
||||
> .arrow-text {
|
||||
.flex(row, center, center);
|
||||
@apply w-4 shrink-0;
|
||||
@apply flex flex-row justify-center items-center w-4 shrink-0;
|
||||
|
||||
> .icon-img {
|
||||
@apply w-4 h-auto opacity-80 rotate-90;
|
||||
@apply w-4 h-auto opacity-40;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .items-wrapper {
|
||||
.flex(column, flex-start, flex-start);
|
||||
@apply absolute top-full left-0 w-auto p-1 mt-1 -ml-2 bg-white rounded-md overflow-y-auto;
|
||||
@apply flex flex-col justify-start items-start absolute top-full left-0 w-auto p-1 mt-1 -ml-2 bg-white rounded-md overflow-y-auto z-1;
|
||||
min-width: calc(100% + 16px);
|
||||
max-height: 256px;
|
||||
box-shadow: 0 0 8px 0 rgb(0 0 0 / 20%);
|
||||
.hide-scroll-bar();
|
||||
|
||||
> .item-container {
|
||||
.flex(column, flex-start, flex-start);
|
||||
@apply w-full px-3 text-sm select-none leading-8 cursor-pointer rounded whitespace-nowrap hover:bg-gray-100;
|
||||
@apply flex flex-col justify-start items-start w-full px-3 text-sm select-none leading-8 cursor-pointer rounded whitespace-nowrap hover:bg-gray-100;
|
||||
|
||||
&.selected {
|
||||
color: @text-green;
|
||||
|
||||
@@ -89,6 +89,6 @@
|
||||
}
|
||||
|
||||
> .remove-btn {
|
||||
@apply w-4 h-auto cursor-pointer opacity-80 hover:opacity-60;
|
||||
@apply w-4 h-auto ml-1 cursor-pointer opacity-60 hover:opacity-80;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
@import "./mixin.less";
|
||||
|
||||
.daily-memo-wrapper {
|
||||
.flex(row, flex-start, flex-start);
|
||||
@apply flex flex-row justify-start items-start relative w-full flex-nowrap pb-6;
|
||||
|
||||
&:last-child {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
@apply flex flex-row justify-start items-center;
|
||||
|
||||
> .btn-text {
|
||||
@apply w-6 h-6 mr-2 rounded cursor-pointer select-none last:mr-0 hover:bg-gray-200;
|
||||
@apply w-6 h-6 mr-2 rounded cursor-pointer select-none text-gray-600 last:mr-0 hover:bg-gray-200;
|
||||
|
||||
> .icon-img {
|
||||
@apply w-full h-auto;
|
||||
|
||||
@@ -30,10 +30,6 @@
|
||||
.btn {
|
||||
.flex(column, center, center);
|
||||
@apply w-6 h-6 rounded hover:bg-gray-100 hover:shadow;
|
||||
|
||||
> .icon-img {
|
||||
@apply w-5 h-5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,10 +28,10 @@
|
||||
@apply flex flex-row justify-start items-center;
|
||||
|
||||
> .action-btn {
|
||||
@apply flex flex-row justify-start items-center p-1 w-auto h-auto mr-1 select-none rounded cursor-pointer opacity-60 hover:opacity-90 hover:bg-gray-300 hover:shadow;
|
||||
@apply flex flex-row justify-center items-center p-1 w-auto h-auto mr-1 select-none rounded cursor-pointer opacity-60 hover:opacity-90 hover:bg-gray-300 hover:shadow;
|
||||
|
||||
> .icon-img {
|
||||
@apply w-5 h-auto;
|
||||
@apply w-5 h-5 mx-auto flex flex-row justify-center items-center;
|
||||
}
|
||||
|
||||
> .tip-text {
|
||||
|
||||
@@ -5,34 +5,30 @@
|
||||
@apply w-auto h-full px-2 border-r rounded-l flex flex-row justify-center items-center text-xs font-bold text-gray-800 bg-gray-100;
|
||||
|
||||
> .icon-img {
|
||||
@apply w-4 h-auto mr-1;
|
||||
@apply mr-1;
|
||||
}
|
||||
}
|
||||
|
||||
> .count-text {
|
||||
@apply px-3 text-xs font-bold text-gray-800;
|
||||
@apply w-auto h-full flex flex-row justify-center items-center px-3 text-xs font-bold text-gray-800;
|
||||
|
||||
&.pulse {
|
||||
@apply text-sm;
|
||||
animation: 1s linear 0s infinite pulse;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
30% {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
60% {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
70% {
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
100% {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
> .icon-text {
|
||||
@apply text-sm ml-1;
|
||||
animation: 1.6s linear 0s infinite pulse;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
70% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,11 +27,7 @@
|
||||
|
||||
> .btn {
|
||||
.flex(row, center, center);
|
||||
@apply w-6 h-6 ml-1 rounded hover:bg-white;
|
||||
|
||||
> .icon-img {
|
||||
@apply w-5 h-5;
|
||||
}
|
||||
@apply w-6 h-6 ml-2 rounded text-gray-600 hover:bg-white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,19 @@
|
||||
> .memo-top-wrapper {
|
||||
@apply flex flex-row justify-between items-center w-full h-6 mb-1;
|
||||
|
||||
> .status-text-container {
|
||||
@apply flex flex-row justify-start items-center;
|
||||
|
||||
> .time-text {
|
||||
@apply text-xs text-gray-400 cursor-pointer;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
> .status-text {
|
||||
@apply text-xs cursor-pointer ml-2 rounded border border-green-600 px-1 text-green-600;
|
||||
}
|
||||
}
|
||||
|
||||
> .time-text {
|
||||
@apply text-xs text-gray-400 cursor-pointer;
|
||||
}
|
||||
@@ -33,7 +46,7 @@
|
||||
@apply w-full flex flex-row justify-between items-center border-b border-gray-100 p-1 mb-1;
|
||||
|
||||
> .btn {
|
||||
@apply relative w-7 h-7 p-1;
|
||||
@apply relative w-7 h-7 p-1 text-gray-600;
|
||||
|
||||
&:hover > .tip-text {
|
||||
@apply block;
|
||||
@@ -60,18 +73,16 @@
|
||||
}
|
||||
|
||||
.btn {
|
||||
@apply flex flex-row justify-center items-center px-2 leading-6 text-sm rounded hover:bg-gray-100;
|
||||
@apply flex flex-row justify-center items-center px-2 leading-6 text-sm rounded hover:bg-gray-200;
|
||||
|
||||
&.more-action-btn {
|
||||
@apply w-8 -mr-2 opacity-60 cursor-default;
|
||||
@apply w-8 -mr-2 opacity-60 cursor-default hover:bg-transparent;
|
||||
|
||||
> .icon-img {
|
||||
@apply w-4 h-auto;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: unset;
|
||||
|
||||
& + .more-action-btns-wrapper {
|
||||
display: flex;
|
||||
}
|
||||
@@ -117,7 +128,7 @@
|
||||
}
|
||||
|
||||
> .icon-img {
|
||||
@apply w-4 h-auto transition-all;
|
||||
@apply w-4 h-auto ml-1 transition-all;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
@apply w-full py-2 px-4 rounded-lg flex flex-row justify-start items-center bg-zinc-200;
|
||||
|
||||
> .icon-img {
|
||||
@apply mr-2 w-4 h-auto opacity-80;
|
||||
@apply mr-2 h-auto opacity-30;
|
||||
}
|
||||
|
||||
> .text-input {
|
||||
|
||||
@@ -7,21 +7,15 @@
|
||||
@apply w-180 max-w-full mb-8 p-0;
|
||||
|
||||
> .dialog-content-container {
|
||||
.flex(column, flex-start, flex-start);
|
||||
@apply relative w-full overflow-y-scroll p-0 flex flex-col sm:flex-row justify-start items-start;
|
||||
@apply flex flex-col sm:flex-row justify-start items-start relative w-full overflow-y-scroll p-0;
|
||||
.hide-scroll-bar();
|
||||
|
||||
> .close-btn {
|
||||
.flex(column, center, center);
|
||||
@apply absolute top-4 right-4 w-6 h-6 rounded hover:bg-gray-200 hover:shadow;
|
||||
|
||||
> .icon-img {
|
||||
@apply w-5 h-5;
|
||||
}
|
||||
@apply flex flex-col justify-center items-center absolute top-4 right-4 w-6 h-6 rounded hover:bg-gray-200 hover:shadow;
|
||||
}
|
||||
|
||||
> .section-selector-container {
|
||||
@apply w-full sm:w-40 h-auto sm:h-full shrink-0 rounded-t-lg sm:rounded-l-lg p-4 border-r bg-gray-100 flex flex-col justify-start items-start;
|
||||
@apply w-full sm:w-44 h-auto sm:h-full shrink-0 rounded-t-lg sm:rounded-none sm:rounded-l-lg p-4 border-r bg-gray-100 flex flex-col justify-start items-start;
|
||||
|
||||
> .section-title {
|
||||
@apply text-sm mt-2 sm:mt-4 first:mt-3 mb-1 font-mono text-gray-400;
|
||||
@@ -49,8 +43,7 @@
|
||||
.hide-scroll-bar();
|
||||
|
||||
> .section-container {
|
||||
.flex(column, flex-start, flex-start);
|
||||
@apply w-full my-2;
|
||||
@apply flex flex-col justify-start items-start w-full my-2;
|
||||
|
||||
> .title-text {
|
||||
@apply text-sm mb-3 font-mono text-gray-500;
|
||||
|
||||
@@ -63,14 +63,14 @@
|
||||
@apply flex flex-row justify-start items-center w-full py-3 px-6;
|
||||
|
||||
> .normal-text {
|
||||
@apply w-full flex flex-row justify-start items-center text-sm text-gray-500;
|
||||
|
||||
> .name-text {
|
||||
@apply text-black ml-2;
|
||||
}
|
||||
@apply w-full flex flex-row justify-start items-center text-sm leading-6 text-gray-500;
|
||||
|
||||
> .icon-text {
|
||||
@apply text-lg ml-1 mr-2;
|
||||
@apply text-lg ml-1 mr-2 leading-6;
|
||||
}
|
||||
|
||||
> .name-text {
|
||||
@apply text-black ml-2 leading-6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,22 +12,10 @@
|
||||
}
|
||||
|
||||
> .btn {
|
||||
@apply flex flex-col justify-center items-center w-5 h-5 bg-gray-200 rounded ml-2 shadow hover:opacity-80;
|
||||
@apply flex flex-col justify-center items-center w-5 p-1 h-5 bg-gray-200 rounded ml-2 shadow hover:opacity-80;
|
||||
|
||||
> img {
|
||||
@apply w-4 h-4 opacity-80;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .create-shortcut-btn-container {
|
||||
@apply flex flex-row justify-start items-center w-full mt-4 mb-2 ml-4;
|
||||
|
||||
> .btn {
|
||||
@apply flex p-2 px-4 rounded-lg text-sm border border-dashed border-blue-600;
|
||||
|
||||
&:hover {
|
||||
@apply bg-blue-600 text-white;
|
||||
> .icon-img {
|
||||
@apply opacity-60;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,14 +54,11 @@
|
||||
@apply flex-row justify-end items-center hidden shrink-0;
|
||||
|
||||
> .action-btn {
|
||||
.flex(row, center, center);
|
||||
@apply w-6 h-6 shrink-0;
|
||||
|
||||
> .icon-img {
|
||||
@apply w-4 h-auto;
|
||||
}
|
||||
@apply flex flex-row justify-center items-center w-6 h-6 shrink-0;
|
||||
|
||||
&.toggle-btn {
|
||||
@apply opacity-60;
|
||||
|
||||
&:hover {
|
||||
& + .action-btns-wrapper {
|
||||
@apply flex;
|
||||
|
||||
@@ -8,11 +8,7 @@
|
||||
@apply w-full pr-6 my-2 flex sm:hidden flex-row justify-end items-center;
|
||||
|
||||
> .action-btn {
|
||||
@apply p-1 bg-gray-100 rounded shadow;
|
||||
|
||||
> .icon-img {
|
||||
@apply w-4 h-auto;
|
||||
}
|
||||
@apply p-1 bg-gray-100 rounded shadow w-7 h-7 flex flex-row justify-center items-center;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.status-text-container {
|
||||
.amount-text-container {
|
||||
@apply flex flex-row justify-between items-start w-full px-6 select-none shrink-0 pb-4;
|
||||
|
||||
> .status-text {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect } from "react";
|
||||
import { locationService, userService } from "../services";
|
||||
import { useAppSelector } from "../store";
|
||||
import useLoading from "../hooks/useLoading";
|
||||
import Only from "../components/common/OnlyWhen";
|
||||
import Sidebar from "../components/Sidebar";
|
||||
@@ -11,6 +12,7 @@ import toastHelper from "../components/Toast";
|
||||
import "../less/home.less";
|
||||
|
||||
function Home() {
|
||||
const location = useAppSelector((state) => state.location);
|
||||
const loadingState = useLoading();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -35,7 +37,7 @@ function Home() {
|
||||
}
|
||||
loadingState.setFinish();
|
||||
});
|
||||
}, []);
|
||||
}, [location]);
|
||||
|
||||
return (
|
||||
<section className="page-wrapper home">
|
||||
|
||||
@@ -786,6 +786,11 @@ csstype@^3.0.2:
|
||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.0.tgz#4ddcac3718d787cf9df0d1b7d15033925c8f29f2"
|
||||
integrity sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==
|
||||
|
||||
dayjs@^1.11.3:
|
||||
version "1.11.3"
|
||||
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.3.tgz#4754eb694a624057b9ad2224b67b15d552589258"
|
||||
integrity sha512-xxwlswWOlGhzgQ4TKzASQkUhqERI3egRNqgV4ScR8wlANA/A9tZ7miXa44vTTKEq5l7vWoL5G57bG3zA+Kow0A==
|
||||
|
||||
debug@^3.2.6:
|
||||
version "3.2.7"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
|
||||
|
||||