Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ Make sure Git is installed on your computer.

Download the app, open it, and first grab a Personal Access Token (PAT) so you can sign in to GitHub. You can follow this guide: [how to get your PAT](docs/githubtoken_en.md).

### New: Image → Contribution Heatmap

- Upload PNG/JPG/SVG, auto-resize to a 7-row grid with N columns (N ≈ width/height × 7, clamped to 1–52), map brightness to contribution levels.
- Optional brightness invert and threshold (clear pixels darker than the threshold before quantisation).
- Preview directly on the calendar: hover to position, left-click to apply, right-click to cancel.

Once you’re logged in you’ll see your avatar and name in the upper-left corner. Drag across the calendar to paint your design. When you’re satisfied, click **Create Remote Repo**. You can edit the repo name and description, choose whether it’s public or private, and then press **Generate & Push** to let the app create and push the repository for you automatically.

> **Heads-up:** GitHub may take anywhere from 5 minutes to 2 days to show the contributions on your profile. You can keep the repo private and enable “Include private contributions” in your profile settings so others can’t see the repo content but the contribution streak still counts.
Expand Down
6 changes: 6 additions & 0 deletions README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@

下载软件,打开后,首先要获取你的PAT来登录github,你可以参考这个:[如何获取你的github访问令牌](docs/githubtoken.md)

### 新增:图片转贡献图

- 支持上传 PNG/JPG/SVG,自动按 7 行与 N 列生成贡献网格(N ≈ 宽高比 × 7,范围 1~52),并按亮度映射为贡献强度。
- 可选反转亮度、亮度阈值(先将低于阈值的像素清零再量化)。
- 直接在日历上预览:鼠标悬停定位,左键应用,右键取消。

登录成功左上角会显示你的头像和名字。拖动鼠标在日历上尽情画画,发挥你的艺术才能!画完后点击创建远程仓库,你可以自定义仓库名称和描述,选择仓库是否公开,确认无误后点击生成并且推送,软件会自动在你的GitHub上创建对应的仓库。

注意: GitHub 可能需要 5 分钟至两天才会显示你的贡献度图案。你可以把仓库设置为私人仓库,并在贡献统计中允许显示私人仓库的贡献,这样他人看不到仓库内容但可以看到贡献记录。
Expand Down
17 changes: 17 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

77 changes: 61 additions & 16 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import GitPathSettings from './components/GitPathSettings';
import LoginModal from './components/LoginModal';
import { TranslationProvider, useTranslations, Language } from './i18n';
import { BrowserOpenURL, EventsOn } from '../wailsjs/runtime/runtime';
import type { main } from '../wailsjs/go/models';

function App() {
Expand Down Expand Up @@ -48,6 +47,12 @@
};

const AppLayout: React.FC<AppLayoutProps> = ({ contributions }) => {
const hasWailsApp = React.useCallback(() => {
if (typeof window === 'undefined') return false;
const w = window as typeof window & { go?: { main?: { App?: unknown } } };
return Boolean(w?.go?.main?.App);
}, []);

const { language, setLanguage, t } = useTranslations();
const [isGitInstalled, setIsGitInstalled] = React.useState<boolean | null>(null);
const [isGitPathSettingsOpen, setIsGitPathSettingsOpen] = React.useState<boolean>(false);
Expand All @@ -56,14 +61,22 @@

const checkGit = React.useCallback(async () => {
try {
const { CheckGitInstalled } = await import('../wailsjs/go/main/App');
const response = await CheckGitInstalled();
if (typeof window !== 'undefined' && !hasWailsApp()) {
console.warn('CheckGitInstalled skipped: wails runtime not available (dev mode)');
setIsGitInstalled(false);
return;
}
const mod = await import('../wailsjs/go/main/App');
if (!mod || typeof mod.CheckGitInstalled !== 'function') {
throw new Error('CheckGitInstalled not available');
}
const response = await mod.CheckGitInstalled();
setIsGitInstalled(response.installed);
} catch (error) {
console.error('Failed to check Git installation:', error);
setIsGitInstalled(false);
}
}, []);

Check warning on line 79 in frontend/src/App.tsx

View workflow job for this annotation

GitHub Actions / build (macos-latest, macOS, green-wall)

React Hook React.useCallback has a missing dependency: 'hasWailsApp'. Either include it or remove the dependency array

Check warning on line 79 in frontend/src/App.tsx

View workflow job for this annotation

GitHub Actions / build (windows-latest, Windows, green-wall.exe)

React Hook React.useCallback has a missing dependency: 'hasWailsApp'. Either include it or remove the dependency array

Check warning on line 79 in frontend/src/App.tsx

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, Linux, green-wall)

React Hook React.useCallback has a missing dependency: 'hasWailsApp'. Either include it or remove the dependency array

React.useEffect(() => {
checkGit();
Expand All @@ -72,8 +85,15 @@
React.useEffect(() => {
(async () => {
try {
const { GetGithubLoginStatus } = await import('../wailsjs/go/main/App');
const status = await GetGithubLoginStatus();
if (typeof window !== 'undefined' && !hasWailsApp()) {
console.warn('GetGithubLoginStatus skipped: wails runtime not available (dev mode)');
return;
}
const mod = await import('../wailsjs/go/main/App');
if (!mod || typeof mod.GetGithubLoginStatus !== 'function') {
throw new Error('GetGithubLoginStatus not available');
}
const status = await mod.GetGithubLoginStatus();
if (status.authenticated && status.user) {
setGithubUser(status.user);
} else {
Expand All @@ -83,19 +103,31 @@
console.error('Failed to fetch GitHub login status:', error);
}
})();
}, []);

Check warning on line 106 in frontend/src/App.tsx

View workflow job for this annotation

GitHub Actions / build (macos-latest, macOS, green-wall)

React Hook React.useEffect has a missing dependency: 'hasWailsApp'. Either include it or remove the dependency array

Check warning on line 106 in frontend/src/App.tsx

View workflow job for this annotation

GitHub Actions / build (windows-latest, Windows, green-wall.exe)

React Hook React.useEffect has a missing dependency: 'hasWailsApp'. Either include it or remove the dependency array

Check warning on line 106 in frontend/src/App.tsx

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, Linux, green-wall)

React Hook React.useEffect has a missing dependency: 'hasWailsApp'. Either include it or remove the dependency array

React.useEffect(() => {
const unsubscribe = EventsOn('github:auth-changed', (status: main.GithubLoginStatus) => {
if (status && status.authenticated && status.user) {
setGithubUser(status.user);
return;
let unsubscribe: (() => void) | undefined;
(async () => {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Async dynamic import in the effect can register EventsOn after unmount, leaving an uncleaned listener and possible state updates on an unmounted component.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frontend/src/App.tsx, line 110:

<comment>Async dynamic import in the effect can register `EventsOn` after unmount, leaving an uncleaned listener and possible state updates on an unmounted component.</comment>

<file context>
@@ -86,16 +106,28 @@ const AppLayout: React.FC<AppLayoutProps> = ({ contributions }) => {
-        setGithubUser(status.user);
-        return;
+    let unsubscribe: (() => void) | undefined;
+    (async () => {
+      try {
+        const { EventsOn } = await import('../wailsjs/runtime/runtime');
</file context>
Fix with Cubic

try {
const { EventsOn } = await import('../wailsjs/runtime/runtime');
if (typeof EventsOn === 'function') {
unsubscribe = EventsOn('github:auth-changed', (status: main.GithubLoginStatus) => {
if (status && status.authenticated && status.user) {
setGithubUser(status.user);
return;
}
setGithubUser(null);
});
}
} catch (error) {
console.warn('EventsOn not available (dev mode)', error);
}
setGithubUser(null);
});
})();

return () => {
unsubscribe();
if (unsubscribe) {
unsubscribe();
}
};
}, []);

Expand All @@ -115,8 +147,10 @@
const logoutLabel = language === 'zh' ? '退出' : 'Log out';
const handleLogout = React.useCallback(async () => {
try {
const { LogoutGithub } = await import('../wailsjs/go/main/App');
await LogoutGithub();
const mod = await import('../wailsjs/go/main/App');
if (typeof mod?.LogoutGithub === 'function') {
await mod.LogoutGithub();
}
setGithubUser(null);
} catch (error) {
console.error('Failed to log out from GitHub:', error);
Expand All @@ -127,8 +161,19 @@
}, []);
const displayName = githubUser?.name?.trim() || githubUser?.login || '';

const openRepository = React.useCallback(() => {
BrowserOpenURL('https://github.com/zmrlft/GreenWall');
const openRepository = React.useCallback(async () => {
try {
const { BrowserOpenURL } = await import('../wailsjs/runtime/runtime');
if (typeof BrowserOpenURL === 'function') {
BrowserOpenURL('https://github.com/zmrlft/GreenWall');
return;
}
} catch (error) {
console.warn('BrowserOpenURL not available (dev mode)', error);
}
if (typeof window !== 'undefined') {
window.open('https://github.com/zmrlft/GreenWall', '_blank', 'noopener,noreferrer');
}
}, []);

return (
Expand Down
35 changes: 19 additions & 16 deletions frontend/src/components/CalendarControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -185,29 +185,32 @@ export const CalendarControls: React.FC<Props> = ({
<div className="flex flex-col gap-2">
<span className="text-sm font-medium text-black">{t('labels.drawMode')}</span>
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
<button
type="button"
onClick={() => onDrawModeChange('pen')}
className={clsx(
'flex w-full items-center justify-center gap-2 rounded-none px-3 py-2 text-sm font-medium transition-all duration-200',
drawMode === 'pen'
? 'scale-105 transform bg-black text-white shadow-lg'
: 'border border-black bg-white text-black hover:bg-gray-100'
)}
title={t('titles.pen')}
>
<span>{t('drawModes.pen')}</span>
<div className="flex w-full flex-row gap-2">
<button
type="button"
onClick={() => onDrawModeChange('pen')}
className={clsx(
'flex flex-1 items-center justify-center gap-2 rounded-none px-3 py-2 text-sm font-medium transition-all duration-200',
drawMode === 'pen'
? 'scale-105 transform bg-black text-white shadow-lg'
: 'border border-black bg-white text-black hover:bg-gray-100'
)}
title={t('titles.pen')}
>
<span>{t('drawModes.pen')}</span>
</button>
{onPenIntensityChange && (
<button
type="button"
className={clsx(
'flex items-center gap-1 rounded-full px-2 py-1 text-[11px] font-semibold uppercase tracking-wide transition-colors duration-200',
'flex items-center justify-center gap-1 rounded-none border px-2 py-2 text-[11px] font-semibold uppercase tracking-wide transition-colors duration-200',
drawMode === 'pen'
? 'bg-white/15 text-white hover:bg-white/25'
: 'bg-black/5 text-black'
? 'border-black bg-black text-white hover:bg-gray-900'
: 'border-black bg-white text-black hover:bg-gray-100'
)}
aria-label={getPenSettingsAriaLabel()}
onClick={handlePenSettingsButtonClick}
title={getPenSettingsAriaLabel()}
>
{penMode === 'auto' ? (
<span>{t('penModes.auto')}</span>
Expand All @@ -222,7 +225,7 @@ export const CalendarControls: React.FC<Props> = ({
)}
</button>
)}
</button>
</div>
<button
type="button"
onClick={() => {
Expand Down
19 changes: 18 additions & 1 deletion frontend/src/components/ContributionCalendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { useTranslations } from '../i18n';
import { WindowIsMaximised, WindowIsFullscreen } from '../../wailsjs/runtime/runtime';
import { getPatternById, gridToBoolean } from '../data/characterPatterns';
import { useContributionHistory } from '../hooks/useContributionHistory';
import { ImageImportCard } from './ImageImportCard';

// 根据贡献数量计算level
function calculateLevel(count: number): 0 | 1 | 2 | 3 | 4 {
Expand Down Expand Up @@ -454,6 +455,19 @@ function ContributionCalendar({
[selectionBuffer, year, isFutureDate, pushSnapshot, setUserContributions]
);

const handlePreviewImageGrid = React.useCallback(
(grid: { width: number; height: number; data: number[][] }) => {
setSelectionBuffer(grid);
setPastePreviewActive(true);
setPastePreviewDates(new Set());
setSelectionStart(null);
setSelectionEnd(null);
setSelectionDates(new Set());
setPreviewMode(false);
},
[]
);

// 取消字符预览
const handleCancelCharacterPreview = React.useCallback(() => {
setPreviewMode(false);
Expand Down Expand Up @@ -948,6 +962,7 @@ function ContributionCalendar({
computeSelectionDates,
pushSnapshot,
setUserContributions,
t,
]);

const tiles = filteredContributions.map((c, i) => {
Expand Down Expand Up @@ -1186,7 +1201,9 @@ function ContributionCalendar({
/>
</aside>
<aside className="workbench__panel">
<p className="text-center">{t('workbench.placeholder')}</p>
<div className="flex flex-col gap-4">
<ImageImportCard onPreview={handlePreviewImageGrid} />
</div>
</aside>
</div>
{isRemoteModalOpen && (
Expand Down
Loading