Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 85 additions & 7 deletions .github/workflows/build-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
# GitHub Actions — Build LarkSync for Windows
# Runs on a free Windows VM provided by GitHub.
# Triggers: manual (workflow_dispatch) or push to 'windows' branch.
# Output: LarkSync-Windows.zip artifact (downloadable from Actions tab)
# Output:
# - LarkSync-Windows-Portable.zip (single .exe, no install needed)
# - LarkSync-Windows-Full.zip (folder with all DLLs, faster startup)
# ─────────────────────────────────────────────────────────────────────

name: Build LarkSync (Windows)
Expand Down Expand Up @@ -57,8 +59,19 @@ jobs:
print('No icon source found, building without icon')
"

# ── 5. Build with PyInstaller ────────────────────────────────
- name: Build executable
# ── 5. Common PyInstaller arguments ──────────────────────────
- name: Define build args
id: args
shell: bash
run: |
ICON_ARG=""
if [ -f "assets/icon.ico" ]; then
ICON_ARG="--icon=assets/icon.ico"
fi
echo "icon_arg=$ICON_ARG" >> "$GITHUB_OUTPUT"

# ── 6a. Build PORTABLE single-file .exe ──────────────────────
- name: Build portable exe (--onefile)
run: |
python -c "
import PyInstaller.__main__
Expand All @@ -71,6 +84,7 @@ jobs:
PyInstaller.__main__.run([
'main.py',
'--name=LarkSync',
'--onefile',
'--windowed',
*icon_arg,
'--add-data=assets;assets',
Expand Down Expand Up @@ -106,11 +120,75 @@ jobs:
])
"

# ── 6. Package output ────────────────────────────────────────
- name: Upload artifact
# ── 6b. Copy portable exe to a separate folder ───────────────
- name: Prepare portable artifact
run: |
mkdir -p dist\portable
copy dist\LarkSync.exe dist\portable\LarkSync.exe

# ── 6c. Build FULL folder-based distribution ─────────────────
- name: Build full distribution (--onedir)
run: |
python -c "
import PyInstaller.__main__
import os

icon_arg = []
if os.path.exists('assets/icon.ico'):
icon_arg = ['--icon=assets/icon.ico']

PyInstaller.__main__.run([
'main.py',
'--name=LarkSync',
'--onedir',
'--windowed',
*icon_arg,
'--add-data=assets;assets',
'--hidden-import=PyQt6',
'--hidden-import=PyQt6.QtCore',
'--hidden-import=PyQt6.QtGui',
'--hidden-import=PyQt6.QtWidgets',
'--hidden-import=google.oauth2',
'--hidden-import=google.auth',
'--hidden-import=google.auth.transport.requests',
'--hidden-import=googleapiclient.discovery',
'--hidden-import=googleapiclient.http',
'--hidden-import=google_auth_oauthlib.flow',
'--hidden-import=requests',
'--hidden-import=app',
'--hidden-import=app.config_manager',
'--hidden-import=app.setup_wizard',
'--hidden-import=app.tray_app',
'--hidden-import=app.settings_dialog',
'--hidden-import=app.log_viewer',
'--hidden-import=app.sync_thread',
'--hidden-import=app.win_menu',
'--hidden-import=sync',
'--hidden-import=sync.lark_auth',
'--hidden-import=sync.lark_client',
'--hidden-import=sync.google_client',
'--hidden-import=sync.sync_engine',
'--hidden-import=sync.lark_notifier',
'--collect-submodules=app',
'--collect-submodules=sync',
'--clean',
'-y',
])
"

# ── 7. Upload artifacts ──────────────────────────────────────
- name: Upload portable exe
uses: actions/upload-artifact@v4
with:
name: LarkSync-Windows-Portable
path: dist/portable/LarkSync.exe
retention-days: 90
compression-level: 9

- name: Upload full distribution
uses: actions/upload-artifact@v4
with:
name: LarkSync-Windows
name: LarkSync-Windows-Full
path: dist/LarkSync/
retention-days: 30
retention-days: 90
compression-level: 9
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ A lightweight desktop app for seamless file synchronization — available for **
- **Google Drive upload** — Uploads to any target folder in your Google Drive via OAuth 2.0
- **Format conversion** — Lark-native files (Docs, Sheets, Mindnotes) exported to Google-compatible formats (Docx, Xlsx, PDF)
- **Incremental sync** — Only sync new and modified files since last run (faster)
- **Smart Settings UX** — Sync Now button saves + triggers sync; Cancel Sync mid-flight; singleton window guard
- **Secure credential fields** — All API keys and IDs hidden by default with a 👁 eye toggle
- **Setup Wizard** — Guided first-time configuration for both Lark and Google credentials
- **Sync log** — In-app log viewer for reviewing sync history and diagnosing errors
- **Lark group notification** — Get notified in your Lark group chat after each sync
Expand Down Expand Up @@ -206,11 +208,13 @@ larksync/

## Documentation

| Document | Description |
|----------|-------------|
| [User Guide](docs/USER_GUIDE.md) | Installation, setup, and usage instructions (EN + VI) |
| [Terms of Use](docs/TERMS_OF_USE.md) | Terms governing use of LarkSync (EN + VI) |
| [Disclaimer](docs/DISCLAIMER.md) | Liability disclaimer and warranty information (EN + VI) |
| Document | Audience | Description |
|----------|----------|-------------|
| [User Guide](docs/USER_GUIDE.md) | End users | Installation, setup, and usage instructions (EN + VI) |
| [Architecture](docs/ARCHITECTURE.md) | Developers | System design, module reference, data flows, build pipeline |
| [Developer Guide](docs/DEVELOPER_GUIDE.md) | Developers | Dev environment, patterns, how-to guides, pitfalls |
| [Terms of Use](docs/TERMS_OF_USE.md) | End users | Terms governing use of LarkSync (EN + VI) |
| [Disclaimer](docs/DISCLAIMER.md) | End users | Liability disclaimer and warranty information (EN + VI) |

---

Expand Down
6 changes: 2 additions & 4 deletions app/mac_menu_bar.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,8 @@ def build_menu_bar(config, tray_app) -> QMenuBar:


def _open_settings(config, tray_app):
from app.settings_dialog import SettingsDialog
dlg = SettingsDialog(config, tray_app=tray_app)
dlg.exec()
tray_app._refresh_menu()
# Delegate to tray_app so the singleton guard is enforced
tray_app._open_settings()


def _show_about():
Expand Down
64 changes: 60 additions & 4 deletions app/settings_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,62 @@ def _setting_row(label: str, sub: str, widget: QWidget) -> QHBoxLayout:
return row


# ── Secure input field (password mode + eye toggle) ──────────────────

class _SecretField(QWidget):
"""
A QLineEdit that defaults to password-echo mode with a 👁 button on the
right that toggles visibility. Exposes the same .text() / .setText() /
.textChanged API as a plain QLineEdit so it can be used as a drop-in.
"""

def __init__(self, placeholder: str = ""):
super().__init__()
row = QHBoxLayout(self)
row.setContentsMargins(0, 0, 0, 0)
row.setSpacing(6)

self._edit = _input(placeholder, password=True)

bdr = _c("#d0d0d0", "#555555")
bg = _c("#f5f5f7", "#2c2c2e")
fg = _c("#888888", "#aaaaaa")
hover = _c("#e8e8e8", "#3a3a3c")
act = _c("#ddeeff", "#1a2a3a")

self._eye = QPushButton("👁")
self._eye.setFixedSize(36, 36)
self._eye.setCheckable(True)
self._eye.setCursor(Qt.CursorShape.PointingHandCursor)
self._eye.setToolTip("Show / hide")
self._eye.setStyleSheet(f"""
QPushButton {{
background: {bg}; color: {fg};
border: 1px solid {bdr}; border-radius: 8px;
font-size: 15px; padding: 0;
}}
QPushButton:hover {{ background: {hover}; }}
QPushButton:checked {{ color: #007AFF; background: {act}; }}
""")
self._eye.toggled.connect(
lambda on: self._edit.setEchoMode(
QLineEdit.EchoMode.Normal if on else QLineEdit.EchoMode.Password
)
)

row.addWidget(self._edit, 1)
row.addWidget(self._eye)

# ── Proxy API ──────────────────────────────────────────────────────
def text(self) -> str: return self._edit.text()
def setText(self, t: str): self._edit.setText(t)

@property
def textChanged(self):
"""Return the inner QLineEdit's textChanged signal so callers can connect."""
return self._edit.textChanged


# ── Main dialog ──────────────────────────────────────────────────────

class SettingsDialog(QDialog):
Expand Down Expand Up @@ -390,12 +446,12 @@ def _lark_tab(self) -> QWidget:

layout.addWidget(_section_title("Lark App Credentials"))
layout.addWidget(_label("App ID", bold=True, size=12, color="#555"))
self._lark_id_edit = _input("cli_xxx...")
self._lark_id_edit = _SecretField("cli_xxx...")
self._lark_id_edit.setText(self.config.get("lark_app_id",""))
layout.addWidget(self._lark_id_edit)

layout.addWidget(_label("App Secret", bold=True, size=12, color="#555"))
self._lark_sec_edit = _input("App Secret", password=True)
self._lark_sec_edit = _SecretField("App Secret")
self._lark_sec_edit.setText(self.config.get("lark_app_secret",""))
layout.addWidget(self._lark_sec_edit)

Expand Down Expand Up @@ -473,7 +529,7 @@ def _gdrive_tab(self) -> QWidget:

layout.addWidget(_section_title("Destination"))
layout.addWidget(_label("Google Drive Folder ID", bold=True, size=12, color="#555"))
self._folder_edit = _input("Folder ID or leave blank for root")
self._folder_edit = _SecretField("Folder ID or leave blank for root")
self._folder_edit.setText(self.config.get("gdrive_root_folder_id",""))
layout.addWidget(self._folder_edit)
layout.addWidget(_label(
Expand Down Expand Up @@ -564,7 +620,7 @@ def _notify_tab(self) -> QWidget:
))

layout.addWidget(_label("Lark Group Chat ID", bold=True, size=12, color="#555"))
self._notify_edit = _input("oc_xxxxxxxxxxxxxxxx")
self._notify_edit = _SecretField("oc_xxxxxxxxxxxxxxxx")
self._notify_edit.setText(self.config.get("lark_notify_chat_id",""))
layout.addWidget(self._notify_edit)

Expand Down
Loading
Loading