Skip to content
Merged
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
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Checkpoint is a save game backup tool for local games. Create timestamped backup
- **100% Private** - Your data stays on your device. We have zero access to your saves or Google Drive
- **Smart Protection** - Automatically backs up current save before restoring
- **Process Detection** - Prevents restore while game is running
- **Cross-Platform** - Windows and Linux support
- **Cross-Platform** - Windows, macOS, and Linux support

## Your Data, Your Control

Expand All @@ -29,6 +29,9 @@ Checkpoint is built with privacy as the foundation:
### Windows
- Download the `.exe` installer

### macOS
- Download the `.dmg` installer or `.app.tar.gz` archive

### Linux
- **Ubuntu/Debian**: `checkpoint_0.1.0_amd64.deb`
- **Fedora**: `checkpoint-0.1.0-1.x86_64.rpm`
Expand Down Expand Up @@ -72,6 +75,7 @@ Checkpoint is built with privacy as the foundation:
## System Requirements

- **Windows**: Windows 10 or later
- **macOS**: macOS 10.15 Catalina or later
- **Linux**: Any modern distro with GTK3

## Support
Expand Down
18 changes: 18 additions & 0 deletions src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,24 @@ html {
color: var(--text-primary);
}

.cloud-backup-modal-title {
display: flex;
align-items: center;
gap: 0.5rem;
}

.cloud-backup-modal-title-icon {
flex-shrink: 0;
display: block;
}

.cloud-backup-modal-heading {
font-size: 1rem;
font-weight: 700;
line-height: 1;
color: var(--text-primary);
}

.modal-close {
background: var(--bg-tertiary);
border: none;
Expand Down
22 changes: 17 additions & 5 deletions src/components/CloudBackupListModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useProfile } from '../lib/profileContext';
import { useToast } from '../lib/toastContext';
import { ConfirmModal } from './ConfirmModal';
import { listAllCloudSnapshots, downloadSnapshot, deleteCloudSnapshot, type CloudBackupItem } from '../lib/googleDrive';
import { listGames } from '../lib/api';

interface CloudBackupListModalProps {
isOpen: boolean;
Expand Down Expand Up @@ -36,8 +37,19 @@ export function CloudBackupListModal({ isOpen, onClose, onDownload }: CloudBacku
throw new Error(t('errors.notAuthenticated'));
}

const allBackups = await listAllCloudSnapshots(token);
setBackups(allBackups);
const [allBackups, games] = await Promise.all([
listAllCloudSnapshots(token),
listGames()
]);

const gamesMap = new Map(games.map(g => [g.id, g.name]));

const backupsWithNames = allBackups.map(b => ({
...b,
gameName: b.gameName || (b.gameId ? gamesMap.get(b.gameId) : undefined)
}));

setBackups(backupsWithNames);
} catch (err) {
const errorMsg = err instanceof Error ? err.message : t('cloud.failedLoadCloudBackups');
addToast(errorMsg, 'error');
Expand Down Expand Up @@ -161,9 +173,9 @@ export function CloudBackupListModal({ isOpen, onClose, onDownload }: CloudBacku
>
<div className="modal-content cloud-backup-list-modal" onClick={(e) => e.stopPropagation()} style={{ maxWidth: '600px' }}>
<div className="modal-header">
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<Cloud size={20} />
<h2>{t('cloud.cloudBackups')}</h2>
<div className="cloud-backup-modal-title">
<Cloud size={20} className="cloud-backup-modal-title-icon" />
<div className="cloud-backup-modal-heading">{t('cloud.cloudBackups')}</div>
</div>
<button className="modal-close" onClick={onClose}>
<X size={20} />
Expand Down
1 change: 1 addition & 0 deletions src/components/GameDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,7 @@ export function GameDetail({ game, onBack, onGameDeleted, onGameUpdated, setLoad
const fileId = await uploadSnapshot(
token,
game.id,
game.name,
snapshot.name,
zipBlob
);
Expand Down
12 changes: 8 additions & 4 deletions src/lib/googleDrive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,12 +121,16 @@ export async function getDriveStorageInfo(accessToken: string): Promise<{ used:
export async function uploadSnapshot(
accessToken: string,
gameId: string,
gameName: string,
snapshotName: string,
fileBlob: Blob
): Promise<string> {
const metadata = {
name: `${gameId}/${snapshotName}.zip`,
parents: ['appDataFolder']
parents: ['appDataFolder'],
appProperties: {
gameName: gameName
}
};

const boundary = '-------314159265358979323846';
Expand Down Expand Up @@ -191,7 +195,7 @@ export async function listCloudSnapshots(accessToken: string, gameId: string): P

export async function listAllCloudSnapshots(accessToken: string): Promise<CloudBackupItem[]> {
const query = encodeURIComponent(`name contains '/' and trashed = false`);
const response = await fetch(`${GOOGLE_DRIVE_ENDPOINT}/files?q=${query}&spaces=appDataFolder&fields=files(id,name,modifiedTime,size)`, {
const response = await fetch(`${GOOGLE_DRIVE_ENDPOINT}/files?q=${query}&spaces=appDataFolder&fields=files(id,name,modifiedTime,size,appProperties)`, {
headers: { Authorization: `Bearer ${accessToken}` }
});

Expand All @@ -202,7 +206,7 @@ export async function listAllCloudSnapshots(accessToken: string): Promise<CloudB
const data = await response.json();
const files = data.files || [];

return files.map((file: { id: string; name: string; modifiedTime: string; size?: string }) => {
return files.map((file: { id: string; name: string; modifiedTime: string; size?: string; appProperties?: { gameName?: string } }) => {
const nameParts = file.name.split('/');
const isSnapshot = nameParts.length === 2 && nameParts[1].endsWith('.zip');

Expand All @@ -213,7 +217,7 @@ export async function listAllCloudSnapshots(accessToken: string): Promise<CloudB
size: file.size ? parseInt(file.size) : undefined,
gameId: isSnapshot ? nameParts[0] : undefined,
snapshotName: isSnapshot ? nameParts[1].replace('.zip', '') : undefined,
gameName: undefined
gameName: file.appProperties?.gameName
};
}).filter((file: CloudBackupItem) => file.gameId);
}
Expand Down
Loading