mirror of
https://github.com/Novattz/creamlinux-installer.git
synced 2025-12-06 03:55:37 -05:00
feat: Started on updates and workflows
This commit is contained in:
135
.github/workflows/build-release.yml
vendored
Normal file
135
.github/workflows/build-release.yml
vendored
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
name: 'Build and Release CreamLinux'
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ['main']
|
||||||
|
pull_request:
|
||||||
|
branches: ['main']
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-release:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
platform: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.platform }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # Required for semantic-release
|
||||||
|
|
||||||
|
- name: Set up semantic-release environment
|
||||||
|
run: |
|
||||||
|
echo "GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}" >> $GITHUB_ENV
|
||||||
|
# More environment setup for release if needed
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install frontend dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Run ESLint
|
||||||
|
run: npm run lint
|
||||||
|
|
||||||
|
- name: Install Rust toolchain
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Cache Rust dependencies
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: 'src-tauri -> target'
|
||||||
|
cache-on-failure: true
|
||||||
|
|
||||||
|
# Setup platform-specific dependencies
|
||||||
|
- name: Install Linux dependencies
|
||||||
|
if: matrix.platform == 'ubuntu-latest'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
||||||
|
|
||||||
|
- name: Install macOS dependencies
|
||||||
|
if: matrix.platform == 'macos-latest'
|
||||||
|
run: |
|
||||||
|
rustup target add aarch64-apple-darwin
|
||||||
|
|
||||||
|
- name: Install Windows dependencies
|
||||||
|
if: matrix.platform == 'windows-latest'
|
||||||
|
run: |
|
||||||
|
# Windows typically doesn't need additional dependencies
|
||||||
|
|
||||||
|
# Sync package version
|
||||||
|
- name: Sync Version
|
||||||
|
run: npm run sync-version
|
||||||
|
|
||||||
|
# Run semantic-release only on the ubuntu runner to avoid conflicts
|
||||||
|
- name: Semantic Release
|
||||||
|
if: matrix.platform == 'ubuntu-latest' && github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
|
run: npx semantic-release
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
# Build the app with updater artifacts
|
||||||
|
- name: Build the app
|
||||||
|
run: npm run tauri build
|
||||||
|
|
||||||
|
# Upload the artifacts for each platform
|
||||||
|
- name: Upload Linux artifacts
|
||||||
|
if: matrix.platform == 'ubuntu-latest'
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: creamlinux-linux
|
||||||
|
path: |
|
||||||
|
src-tauri/target/release/bundle/deb/*.deb
|
||||||
|
src-tauri/target/release/bundle/appimage/*.AppImage
|
||||||
|
src-tauri/target/release/bundle/appimage/*.AppImage.sig
|
||||||
|
|
||||||
|
- name: Upload macOS artifacts
|
||||||
|
if: matrix.platform == 'macos-latest'
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: creamlinux-macos
|
||||||
|
path: |
|
||||||
|
src-tauri/target/release/bundle/macos/*.app
|
||||||
|
src-tauri/target/release/bundle/macos/*.app.tar.gz
|
||||||
|
src-tauri/target/release/bundle/macos/*.app.tar.gz.sig
|
||||||
|
src-tauri/target/release/bundle/dmg/*.dmg
|
||||||
|
|
||||||
|
- name: Upload Windows artifacts
|
||||||
|
if: matrix.platform == 'windows-latest'
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: creamlinux-windows
|
||||||
|
path: |
|
||||||
|
src-tauri/target/release/bundle/msi/*.msi
|
||||||
|
src-tauri/target/release/bundle/msi/*.msi.sig
|
||||||
|
src-tauri/target/release/bundle/nsis/*.exe
|
||||||
|
src-tauri/target/release/bundle/nsis/*.exe.sig
|
||||||
|
|
||||||
|
# Generate updater JSON file (run only on ubuntu)
|
||||||
|
- name: Generate updater JSON
|
||||||
|
if: matrix.platform == 'ubuntu-latest' && github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
|
run: |
|
||||||
|
node scripts/generate-updater-json.js
|
||||||
|
|
||||||
|
# Create GitHub release with all artifacts
|
||||||
|
- name: Create Release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
if: matrix.platform == 'ubuntu-latest' && github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
latest.json
|
||||||
|
src-tauri/target/release/bundle/**/*.{AppImage,AppImage.sig,deb,dmg,app.tar.gz,app.tar.gz.sig,msi,msi.sig,exe,exe.sig}
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
generate_release_notes: true
|
||||||
59
.github/workflows/build.yml
vendored
59
.github/workflows/build.yml
vendored
@@ -1,59 +0,0 @@
|
|||||||
name: 'Build CreamLinux'
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: ['main']
|
|
||||||
pull_request:
|
|
||||||
branches: ['main']
|
|
||||||
|
|
||||||
env:
|
|
||||||
CARGO_TERM_COLOR: always
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-tauri:
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
platform: [ubuntu-latest]
|
|
||||||
|
|
||||||
runs-on: ${{ matrix.platform }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
cache: 'npm'
|
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
|
||||||
|
|
||||||
- name: Cache Rust dependencies
|
|
||||||
uses: Swatinem/rust-cache@v2
|
|
||||||
with:
|
|
||||||
workspaces: 'src-tauri -> target'
|
|
||||||
cache-on-failure: true
|
|
||||||
|
|
||||||
- name: Install system dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
|
||||||
|
|
||||||
- name: Install frontend dependencies
|
|
||||||
run: npm install
|
|
||||||
|
|
||||||
- name: Run ESLint
|
|
||||||
run: npm run lint
|
|
||||||
|
|
||||||
- name: Build the app
|
|
||||||
run: npm run tauri build
|
|
||||||
|
|
||||||
- name: Upload binary artifacts
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: creamlinux-${{ runner.os }}
|
|
||||||
path: |
|
|
||||||
src-tauri/target/release/creamlinux
|
|
||||||
src-tauri/target/release/bundle/deb/*.deb
|
|
||||||
src-tauri/target/release/bundle/appimage/*.AppImage
|
|
||||||
31
.releaserc.js
Normal file
31
.releaserc.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
module.exports = {
|
||||||
|
branches: ['main'],
|
||||||
|
plugins: [
|
||||||
|
'@semantic-release/commit-analyzer',
|
||||||
|
'@semantic-release/release-notes-generator',
|
||||||
|
'@semantic-release/changelog',
|
||||||
|
[
|
||||||
|
'@semantic-release/npm',
|
||||||
|
{
|
||||||
|
// Don't actually publish to npm
|
||||||
|
npmPublish: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'@semantic-release/git',
|
||||||
|
{
|
||||||
|
assets: ['package.json', 'CHANGELOG.md'],
|
||||||
|
message: 'chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'@semantic-release/github',
|
||||||
|
{
|
||||||
|
assets: [
|
||||||
|
{ path: 'latest.json', label: 'Updater manifest file' },
|
||||||
|
// We don't need to specify release assets here as they're handled by the GitHub release action
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}
|
||||||
6268
package-lock.json
generated
6268
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@@ -9,10 +9,18 @@
|
|||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"tauri": "tauri",
|
"tauri": "tauri",
|
||||||
"optimize-svg": "node scripts/optimize-svg.js"
|
"optimize-svg": "node scripts/optimize-svg.js",
|
||||||
|
"sync-version": "node scripts/sync-version.js",
|
||||||
|
"generate-updater": "node scripts/generate-updater-json.js",
|
||||||
|
"update-tauri-config": "node scripts/update-tauri-config.js",
|
||||||
|
"update-api-changelog": "node scripts/api-changelog.js",
|
||||||
|
"prepare-release": "npm run sync-version && npm run update-api-changelog && npm run update-tauri-config",
|
||||||
|
"release": "semantic-release"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2.5.0",
|
"@tauri-apps/api": "^2.5.0",
|
||||||
|
"@tauri-apps/plugin-process": "^2.2.1",
|
||||||
|
"@tauri-apps/plugin-updater": "^2.7.1",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"sass": "^1.89.0",
|
"sass": "^1.89.0",
|
||||||
@@ -20,6 +28,9 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.22.0",
|
"@eslint/js": "^9.22.0",
|
||||||
|
"@semantic-release/changelog": "^6.0.3",
|
||||||
|
"@semantic-release/git": "^10.0.1",
|
||||||
|
"@semantic-release/github": "^11.0.2",
|
||||||
"@svgr/core": "^8.1.0",
|
"@svgr/core": "^8.1.0",
|
||||||
"@svgr/webpack": "^8.1.0",
|
"@svgr/webpack": "^8.1.0",
|
||||||
"@tauri-apps/cli": "^2.5.0",
|
"@tauri-apps/cli": "^2.5.0",
|
||||||
@@ -32,6 +43,7 @@
|
|||||||
"eslint-plugin-react-refresh": "^0.4.19",
|
"eslint-plugin-react-refresh": "^0.4.19",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
"sass-embedded": "^1.86.3",
|
"sass-embedded": "^1.86.3",
|
||||||
|
"semantic-release": "^24.2.4",
|
||||||
"typescript": "~5.7.2",
|
"typescript": "~5.7.2",
|
||||||
"typescript-eslint": "^8.26.1",
|
"typescript-eslint": "^8.26.1",
|
||||||
"vite": "^6.3.1",
|
"vite": "^6.3.1",
|
||||||
|
|||||||
215
scripts/api-changelog.js
Normal file
215
scripts/api-changelog.js
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
const { execSync } = require('child_process')
|
||||||
|
|
||||||
|
// Define paths
|
||||||
|
const rustFilesPath = path.join(__dirname, '..', 'src-tauri')
|
||||||
|
|
||||||
|
function getApiDefinitions() {
|
||||||
|
// Get a list of all Rust files
|
||||||
|
const rustFiles = findRustFiles(rustFilesPath)
|
||||||
|
|
||||||
|
// Extract API functions and structs from Rust files
|
||||||
|
const apiDefinitions = {
|
||||||
|
commands: [],
|
||||||
|
structs: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
rustFiles.forEach((filePath) => {
|
||||||
|
const content = fs.readFileSync(filePath, 'utf8')
|
||||||
|
|
||||||
|
// Find Tauri commands (API endpoints)
|
||||||
|
const commandRegex = /#\[tauri::command\]\s+(?:pub\s+)?(?:async\s+)?fn\s+([a-zA-Z0-9_]+)/g
|
||||||
|
let match
|
||||||
|
while ((match = commandRegex.exec(content)) !== null) {
|
||||||
|
apiDefinitions.commands.push({
|
||||||
|
name: match[1],
|
||||||
|
file: path.relative(rustFilesPath, filePath),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find structs that are likely part of the API
|
||||||
|
const structRegex = /#\[derive\(.*Serialize.*\)\]\s+(?:pub\s+)?struct\s+([a-zA-Z0-9_]+)/g
|
||||||
|
while ((match = structRegex.exec(content)) !== null) {
|
||||||
|
apiDefinitions.structs.push({
|
||||||
|
name: match[1],
|
||||||
|
file: path.relative(rustFilesPath, filePath),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return apiDefinitions
|
||||||
|
}
|
||||||
|
|
||||||
|
function findRustFiles(dir) {
|
||||||
|
let results = []
|
||||||
|
const list = fs.readdirSync(dir)
|
||||||
|
|
||||||
|
list.forEach((file) => {
|
||||||
|
const filePath = path.join(dir, file)
|
||||||
|
const stat = fs.statSync(filePath)
|
||||||
|
|
||||||
|
if (stat && stat.isDirectory() && file !== 'target') {
|
||||||
|
// Recursively search subdirectories, but skip the 'target' directory
|
||||||
|
results = results.concat(findRustFiles(filePath))
|
||||||
|
} else if (path.extname(file) === '.rs') {
|
||||||
|
// Add Rust files to the results
|
||||||
|
results.push(filePath)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareApiDefinitions(oldApi, newApi) {
|
||||||
|
const changes = {
|
||||||
|
commands: {
|
||||||
|
added: [],
|
||||||
|
removed: [],
|
||||||
|
},
|
||||||
|
structs: {
|
||||||
|
added: [],
|
||||||
|
removed: [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find added and removed commands
|
||||||
|
const oldCommandNames = oldApi.commands.map((cmd) => cmd.name)
|
||||||
|
const newCommandNames = newApi.commands.map((cmd) => cmd.name)
|
||||||
|
|
||||||
|
changes.commands.added = newApi.commands.filter((cmd) => !oldCommandNames.includes(cmd.name))
|
||||||
|
changes.commands.removed = oldApi.commands.filter((cmd) => !newCommandNames.includes(cmd.name))
|
||||||
|
|
||||||
|
// Find added and removed structs
|
||||||
|
const oldStructNames = oldApi.structs.map((struct) => struct.name)
|
||||||
|
const newStructNames = newApi.structs.map((struct) => struct.name)
|
||||||
|
|
||||||
|
changes.structs.added = newApi.structs.filter((struct) => !oldStructNames.includes(struct.name))
|
||||||
|
changes.structs.removed = oldApi.structs.filter((struct) => !newStructNames.includes(struct.name))
|
||||||
|
|
||||||
|
return changes
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateChangelog(changes) {
|
||||||
|
const changelogPath = path.join(__dirname, '..', 'CHANGELOG.md')
|
||||||
|
let changelog = ''
|
||||||
|
|
||||||
|
// Create changelog if it doesn't exist
|
||||||
|
if (fs.existsSync(changelogPath)) {
|
||||||
|
changelog = fs.readFileSync(changelogPath, 'utf8')
|
||||||
|
} else {
|
||||||
|
changelog =
|
||||||
|
'# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current version and date
|
||||||
|
const packageJson = require(path.join(__dirname, '..', 'package.json'))
|
||||||
|
const version = packageJson.version
|
||||||
|
const date = new Date().toISOString().split('T')[0]
|
||||||
|
|
||||||
|
// Create the new changelog entry
|
||||||
|
let newEntry = `## [${version}] - ${date}\n\n`
|
||||||
|
|
||||||
|
if (
|
||||||
|
changes.commands.added.length > 0 ||
|
||||||
|
changes.commands.removed.length > 0 ||
|
||||||
|
changes.structs.added.length > 0 ||
|
||||||
|
changes.structs.removed.length > 0
|
||||||
|
) {
|
||||||
|
newEntry += '### API Changes\n\n'
|
||||||
|
|
||||||
|
if (changes.commands.added.length > 0) {
|
||||||
|
newEntry += '#### New Commands\n\n'
|
||||||
|
changes.commands.added.forEach((cmd) => {
|
||||||
|
newEntry += `- \`${cmd.name}\` in \`${cmd.file}\`\n`
|
||||||
|
})
|
||||||
|
newEntry += '\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changes.commands.removed.length > 0) {
|
||||||
|
newEntry += '#### Removed Commands\n\n'
|
||||||
|
changes.commands.removed.forEach((cmd) => {
|
||||||
|
newEntry += `- \`${cmd.name}\` (was in \`${cmd.file}\`)\n`
|
||||||
|
})
|
||||||
|
newEntry += '\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changes.structs.added.length > 0) {
|
||||||
|
newEntry += '#### New Structures\n\n'
|
||||||
|
changes.structs.added.forEach((struct) => {
|
||||||
|
newEntry += `- \`${struct.name}\` in \`${struct.file}\`\n`
|
||||||
|
})
|
||||||
|
newEntry += '\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changes.structs.removed.length > 0) {
|
||||||
|
newEntry += '#### Removed Structures\n\n'
|
||||||
|
changes.structs.removed.forEach((struct) => {
|
||||||
|
newEntry += `- \`${struct.name}\` (was in \`${struct.file}\`)\n`
|
||||||
|
})
|
||||||
|
newEntry += '\n'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newEntry += 'No API changes in this release.\n\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add git commit information since last release
|
||||||
|
try {
|
||||||
|
// Get the latest tag
|
||||||
|
const latestTag = execSync('git describe --tags --abbrev=0', { encoding: 'utf8' }).trim()
|
||||||
|
|
||||||
|
// Get commits since the latest tag
|
||||||
|
const commits = execSync(`git log ${latestTag}..HEAD --pretty=format:"%h %s (%an)"`, {
|
||||||
|
encoding: 'utf8',
|
||||||
|
}).trim()
|
||||||
|
|
||||||
|
if (commits) {
|
||||||
|
newEntry += '### Other Changes\n\n'
|
||||||
|
|
||||||
|
// Split commits by line and add them to the changelog
|
||||||
|
commits.split('\n').forEach((commit) => {
|
||||||
|
newEntry += `- ${commit}\n`
|
||||||
|
})
|
||||||
|
|
||||||
|
newEntry += '\n'
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// If there are no tags or other git issues, just continue without commit info
|
||||||
|
console.warn('Could not get git commit information:', error.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the new entry to the changelog
|
||||||
|
if (changelog.includes('## [')) {
|
||||||
|
// Insert new entry after the first heading
|
||||||
|
changelog = changelog.replace('# Changelog\n\n', `# Changelog\n\n${newEntry}`)
|
||||||
|
} else {
|
||||||
|
// Append to the end if no existing versions
|
||||||
|
changelog += newEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the updated changelog
|
||||||
|
fs.writeFileSync(changelogPath, changelog)
|
||||||
|
console.log(`Updated CHANGELOG.md with API changes for version ${version}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main execution
|
||||||
|
try {
|
||||||
|
// Check if we have a saved API definition
|
||||||
|
const apiCachePath = path.join(__dirname, '.api-cache.json')
|
||||||
|
let oldApi = { commands: [], structs: [] }
|
||||||
|
let newApi = getApiDefinitions()
|
||||||
|
|
||||||
|
if (fs.existsSync(apiCachePath)) {
|
||||||
|
oldApi = JSON.parse(fs.readFileSync(apiCachePath, 'utf8'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare and update the changelog
|
||||||
|
const changes = compareApiDefinitions(oldApi, newApi)
|
||||||
|
updateChangelog(changes)
|
||||||
|
|
||||||
|
// Save the new API definition for next time
|
||||||
|
fs.writeFileSync(apiCachePath, JSON.stringify(newApi, null, 2))
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating API changelog:', error)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
82
scripts/generate-updater-json.js
Normal file
82
scripts/generate-updater-json.js
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
const { execSync } = require('child_process')
|
||||||
|
|
||||||
|
// Read the current version from package.json
|
||||||
|
const packageJson = require('../package.json')
|
||||||
|
const version = packageJson.version
|
||||||
|
console.log(`Current version: ${version}`)
|
||||||
|
|
||||||
|
// Get the current date in RFC 3339 format for pub_date
|
||||||
|
const pubDate = new Date().toISOString()
|
||||||
|
|
||||||
|
// Base URL where the assets will be available
|
||||||
|
const baseUrl = 'https://github.com/tickbase/creamlinux/releases/download'
|
||||||
|
const releaseTag = `v${version}`
|
||||||
|
const releaseUrl = `${baseUrl}/${releaseTag}`
|
||||||
|
|
||||||
|
// Create the updater JSON structure
|
||||||
|
const updaterJson = {
|
||||||
|
version,
|
||||||
|
notes: `Release version ${version}`,
|
||||||
|
pub_date: pubDate,
|
||||||
|
platforms: {
|
||||||
|
// Windows x64
|
||||||
|
'windows-x86_64': {
|
||||||
|
url: `${releaseUrl}/creamlinux-setup.exe`,
|
||||||
|
signature: readSignature('windows', 'creamlinux-setup.exe'),
|
||||||
|
},
|
||||||
|
// Linux x64
|
||||||
|
'linux-x86_64': {
|
||||||
|
url: `${releaseUrl}/creamlinux.AppImage`,
|
||||||
|
signature: readSignature('linux', 'creamlinux.AppImage'),
|
||||||
|
},
|
||||||
|
// macOS x64 and arm64 (universal)
|
||||||
|
'darwin-universal': {
|
||||||
|
url: `${releaseUrl}/creamlinux.app.tar.gz`,
|
||||||
|
signature: readSignature('macos', 'creamlinux.app.tar.gz'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the updater JSON file
|
||||||
|
fs.writeFileSync('latest.json', JSON.stringify(updaterJson, null, 2))
|
||||||
|
console.log('Created latest.json updater file')
|
||||||
|
|
||||||
|
// Helper function to read signature files
|
||||||
|
function readSignature(platform, filename) {
|
||||||
|
try {
|
||||||
|
// Determine path based on platform
|
||||||
|
let sigPath
|
||||||
|
|
||||||
|
switch (platform) {
|
||||||
|
case 'windows':
|
||||||
|
// Check both NSIS and MSI
|
||||||
|
try {
|
||||||
|
sigPath = path.join('src-tauri', 'target', 'release', 'bundle', 'nsis', `${filename}.sig`)
|
||||||
|
return fs.readFileSync(sigPath, 'utf8').trim()
|
||||||
|
} catch (e) {
|
||||||
|
sigPath = path.join('src-tauri', 'target', 'release', 'bundle', 'msi', `${filename}.sig`)
|
||||||
|
return fs.readFileSync(sigPath, 'utf8').trim()
|
||||||
|
}
|
||||||
|
case 'linux':
|
||||||
|
sigPath = path.join(
|
||||||
|
'src-tauri',
|
||||||
|
'target',
|
||||||
|
'release',
|
||||||
|
'bundle',
|
||||||
|
'appimage',
|
||||||
|
`${filename}.sig`
|
||||||
|
)
|
||||||
|
return fs.readFileSync(sigPath, 'utf8').trim()
|
||||||
|
case 'macos':
|
||||||
|
sigPath = path.join('src-tauri', 'target', 'release', 'bundle', 'macos', `${filename}.sig`)
|
||||||
|
return fs.readFileSync(sigPath, 'utf8').trim()
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown platform: ${platform}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error reading signature for ${platform}/${filename}:`, error.message)
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
29
scripts/sync-version.js
Normal file
29
scripts/sync-version.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
// Read current version from package.json
|
||||||
|
const packageJsonPath = path.join(__dirname, '..', 'package.json')
|
||||||
|
const packageJson = require(packageJsonPath)
|
||||||
|
const version = packageJson.version
|
||||||
|
|
||||||
|
console.log(`Current version: ${version}`)
|
||||||
|
|
||||||
|
// Update Cargo.toml
|
||||||
|
const cargoTomlPath = path.join(__dirname, '..', 'src-tauri', 'Cargo.toml')
|
||||||
|
let cargoToml = fs.readFileSync(cargoTomlPath, 'utf8')
|
||||||
|
|
||||||
|
// Replace the version in Cargo.toml
|
||||||
|
cargoToml = cargoToml.replace(/version\s*=\s*"[^"]+"/m, `version = "${version}"`)
|
||||||
|
fs.writeFileSync(cargoTomlPath, cargoToml)
|
||||||
|
console.log(`Updated Cargo.toml version to ${version}`)
|
||||||
|
|
||||||
|
// Update tauri.conf.json
|
||||||
|
const tauriConfigPath = path.join(__dirname, '..', 'src-tauri', 'tauri.conf.json')
|
||||||
|
const tauriConfig = JSON.parse(fs.readFileSync(tauriConfigPath, 'utf8'))
|
||||||
|
|
||||||
|
// Set the version in tauri.conf.json
|
||||||
|
tauriConfig.version = version
|
||||||
|
fs.writeFileSync(tauriConfigPath, JSON.stringify(tauriConfig, null, 2))
|
||||||
|
console.log(`Updated tauri.conf.json version to ${version}`)
|
||||||
|
|
||||||
|
console.log('Version synchronization completed!')
|
||||||
42
scripts/update-tauri-config.js
Normal file
42
scripts/update-tauri-config.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
// Path to your tauri.conf.json file
|
||||||
|
const tauriConfigPath = path.join(__dirname, '..', 'src-tauri', 'tauri.conf.json')
|
||||||
|
|
||||||
|
// Read the current config
|
||||||
|
const rawConfig = fs.readFileSync(tauriConfigPath, 'utf8')
|
||||||
|
const config = JSON.parse(rawConfig)
|
||||||
|
|
||||||
|
// Add or update the updater configuration
|
||||||
|
if (!config.plugins) {
|
||||||
|
config.plugins = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the public key from environment variable
|
||||||
|
const pubkey = process.env.TAURI_PUBLIC_KEY || ''
|
||||||
|
|
||||||
|
if (!pubkey) {
|
||||||
|
console.warn(
|
||||||
|
'Warning: TAURI_PUBLIC_KEY environment variable is not set. Updater will not work correctly!'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure the updater plugin
|
||||||
|
config.plugins.updater = {
|
||||||
|
pubkey,
|
||||||
|
endpoints: ['https://github.com/tickbase/creamlinux/releases/latest/download/latest.json'],
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure bundle settings for updater artifacts
|
||||||
|
if (!config.bundle) {
|
||||||
|
config.bundle = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set createUpdaterArtifacts to true
|
||||||
|
config.bundle.createUpdaterArtifacts = true
|
||||||
|
|
||||||
|
// Write the updated config back to the file
|
||||||
|
fs.writeFileSync(tauriConfigPath, JSON.stringify(config, null, 2))
|
||||||
|
|
||||||
|
console.log('Tauri config updated with updater configuration')
|
||||||
@@ -30,6 +30,10 @@ tauri-plugin-shell = "2.0.0-rc"
|
|||||||
tauri-plugin-dialog = "2.0.0-rc"
|
tauri-plugin-dialog = "2.0.0-rc"
|
||||||
tauri-plugin-fs = "2.0.0-rc"
|
tauri-plugin-fs = "2.0.0-rc"
|
||||||
num_cpus = "1.16.0"
|
num_cpus = "1.16.0"
|
||||||
|
tauri-plugin-process = "2"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
custom-protocol = ["tauri/custom-protocol"]
|
custom-protocol = ["tauri/custom-protocol"]
|
||||||
|
|
||||||
|
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||||
|
tauri-plugin-updater = "2"
|
||||||
|
|||||||
14
src-tauri/capabilities/desktop.json
Normal file
14
src-tauri/capabilities/desktop.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"identifier": "desktop-capability",
|
||||||
|
"platforms": [
|
||||||
|
"macOS",
|
||||||
|
"windows",
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"windows": [
|
||||||
|
"main"
|
||||||
|
],
|
||||||
|
"permissions": [
|
||||||
|
"updater:default"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
#![cfg_attr(
|
#![cfg_attr(
|
||||||
all(not(debug_assertions), target_os = "windows"),
|
all(not(debug_assertions), target_os = "windows"),
|
||||||
windows_subsystem = "windows"
|
windows_subsystem = "windows"
|
||||||
)]
|
)]
|
||||||
|
|
||||||
mod dlc_manager;
|
mod dlc_manager;
|
||||||
@@ -22,527 +22,529 @@ use tokio::time::Instant;
|
|||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub struct GameAction {
|
pub struct GameAction {
|
||||||
game_id: String,
|
game_id: String,
|
||||||
action: String,
|
action: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark fields with # to allow unused fields
|
// Mark fields with # to allow unused fields
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct DlcCache {
|
struct DlcCache {
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
data: Vec<DlcInfoWithState>,
|
data: Vec<DlcInfoWithState>,
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
timestamp: Instant,
|
timestamp: Instant,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Structure to hold the state of installed games
|
// Structure to hold the state of installed games
|
||||||
struct AppState {
|
struct AppState {
|
||||||
games: Mutex<HashMap<String, Game>>,
|
games: Mutex<HashMap<String, Game>>,
|
||||||
dlc_cache: Mutex<HashMap<String, DlcCache>>,
|
dlc_cache: Mutex<HashMap<String, DlcCache>>,
|
||||||
fetch_cancellation: Arc<AtomicBool>,
|
fetch_cancellation: Arc<AtomicBool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn get_all_dlcs_command(game_path: String) -> Result<Vec<DlcInfoWithState>, String> {
|
fn get_all_dlcs_command(game_path: String) -> Result<Vec<DlcInfoWithState>, String> {
|
||||||
info!("Getting all DLCs (enabled and disabled) for: {}", game_path);
|
info!("Getting all DLCs (enabled and disabled) for: {}", game_path);
|
||||||
dlc_manager::get_all_dlcs(&game_path)
|
dlc_manager::get_all_dlcs(&game_path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scan and get the list of Steam games
|
// Scan and get the list of Steam games
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn scan_steam_games(
|
async fn scan_steam_games(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
app_handle: tauri::AppHandle,
|
app_handle: tauri::AppHandle,
|
||||||
) -> Result<Vec<Game>, String> {
|
) -> Result<Vec<Game>, String> {
|
||||||
info!("Starting Steam games scan");
|
info!("Starting Steam games scan");
|
||||||
emit_scan_progress(&app_handle, "Locating Steam libraries...", 10);
|
emit_scan_progress(&app_handle, "Locating Steam libraries...", 10);
|
||||||
|
|
||||||
// Get default Steam paths
|
// Get default Steam paths
|
||||||
let paths = searcher::get_default_steam_paths();
|
let paths = searcher::get_default_steam_paths();
|
||||||
|
|
||||||
// Find Steam libraries
|
// Find Steam libraries
|
||||||
emit_scan_progress(&app_handle, "Finding Steam libraries...", 15);
|
emit_scan_progress(&app_handle, "Finding Steam libraries...", 15);
|
||||||
let libraries = searcher::find_steam_libraries(&paths);
|
let libraries = searcher::find_steam_libraries(&paths);
|
||||||
|
|
||||||
// Group libraries by path to avoid duplicates in logs
|
// Group libraries by path to avoid duplicates in logs
|
||||||
let mut unique_libraries = std::collections::HashSet::new();
|
let mut unique_libraries = std::collections::HashSet::new();
|
||||||
for lib in &libraries {
|
for lib in &libraries {
|
||||||
unique_libraries.insert(lib.to_string_lossy().to_string());
|
unique_libraries.insert(lib.to_string_lossy().to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
"Found {} Steam library directories:",
|
"Found {} Steam library directories:",
|
||||||
unique_libraries.len()
|
unique_libraries.len()
|
||||||
);
|
);
|
||||||
for (i, lib) in unique_libraries.iter().enumerate() {
|
for (i, lib) in unique_libraries.iter().enumerate() {
|
||||||
info!(" Library {}: {}", i + 1, lib);
|
info!(" Library {}: {}", i + 1, lib);
|
||||||
}
|
}
|
||||||
|
|
||||||
emit_scan_progress(
|
emit_scan_progress(
|
||||||
&app_handle,
|
&app_handle,
|
||||||
&format!(
|
&format!(
|
||||||
"Found {} Steam libraries. Starting game scan...",
|
"Found {} Steam libraries. Starting game scan...",
|
||||||
unique_libraries.len()
|
unique_libraries.len()
|
||||||
),
|
),
|
||||||
20,
|
20,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Find installed games
|
// Find installed games
|
||||||
let games_info = searcher::find_installed_games(&libraries).await;
|
let games_info = searcher::find_installed_games(&libraries).await;
|
||||||
|
|
||||||
emit_scan_progress(
|
emit_scan_progress(
|
||||||
&app_handle,
|
&app_handle,
|
||||||
&format!("Found {} games. Processing...", games_info.len()),
|
&format!("Found {} games. Processing...", games_info.len()),
|
||||||
90,
|
90,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Log summary of games found
|
// Log summary of games found
|
||||||
info!("Games scan complete - Found {} games", games_info.len());
|
info!("Games scan complete - Found {} games", games_info.len());
|
||||||
info!(
|
info!(
|
||||||
"Native games: {}",
|
"Native games: {}",
|
||||||
games_info.iter().filter(|g| g.native).count()
|
games_info.iter().filter(|g| g.native).count()
|
||||||
);
|
);
|
||||||
info!(
|
info!(
|
||||||
"Proton games: {}",
|
"Proton games: {}",
|
||||||
games_info.iter().filter(|g| !g.native).count()
|
games_info.iter().filter(|g| !g.native).count()
|
||||||
);
|
);
|
||||||
info!(
|
info!(
|
||||||
"Games with CreamLinux: {}",
|
"Games with CreamLinux: {}",
|
||||||
games_info.iter().filter(|g| g.cream_installed).count()
|
games_info.iter().filter(|g| g.cream_installed).count()
|
||||||
);
|
);
|
||||||
info!(
|
info!(
|
||||||
"Games with SmokeAPI: {}",
|
"Games with SmokeAPI: {}",
|
||||||
games_info.iter().filter(|g| g.smoke_installed).count()
|
games_info.iter().filter(|g| g.smoke_installed).count()
|
||||||
);
|
);
|
||||||
|
|
||||||
// Convert to our Game struct
|
// Convert to our Game struct
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
|
|
||||||
info!("Processing games into application state...");
|
info!("Processing games into application state...");
|
||||||
for game_info in games_info {
|
for game_info in games_info {
|
||||||
// Only log detailed game info at Debug level to keep Info logs cleaner
|
// Only log detailed game info at Debug level to keep Info logs cleaner
|
||||||
debug!(
|
debug!(
|
||||||
"Processing game: {}, Native: {}, CreamLinux: {}, SmokeAPI: {}",
|
"Processing game: {}, Native: {}, CreamLinux: {}, SmokeAPI: {}",
|
||||||
game_info.title, game_info.native, game_info.cream_installed, game_info.smoke_installed
|
game_info.title, game_info.native, game_info.cream_installed, game_info.smoke_installed
|
||||||
);
|
);
|
||||||
|
|
||||||
let game = Game {
|
let game = Game {
|
||||||
id: game_info.id,
|
id: game_info.id,
|
||||||
title: game_info.title,
|
title: game_info.title,
|
||||||
path: game_info.path.to_string_lossy().to_string(),
|
path: game_info.path.to_string_lossy().to_string(),
|
||||||
native: game_info.native,
|
native: game_info.native,
|
||||||
api_files: game_info.api_files,
|
api_files: game_info.api_files,
|
||||||
cream_installed: game_info.cream_installed,
|
cream_installed: game_info.cream_installed,
|
||||||
smoke_installed: game_info.smoke_installed,
|
smoke_installed: game_info.smoke_installed,
|
||||||
installing: false,
|
installing: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
result.push(game.clone());
|
result.push(game.clone());
|
||||||
|
|
||||||
// Store in state for later use
|
// Store in state for later use
|
||||||
state.games.lock().insert(game.id.clone(), game);
|
state.games.lock().insert(game.id.clone(), game);
|
||||||
}
|
}
|
||||||
|
|
||||||
emit_scan_progress(
|
emit_scan_progress(
|
||||||
&app_handle,
|
&app_handle,
|
||||||
&format!("Scan complete. Found {} games.", result.len()),
|
&format!("Scan complete. Found {} games.", result.len()),
|
||||||
100,
|
100,
|
||||||
);
|
);
|
||||||
|
|
||||||
info!("Game scan completed successfully");
|
info!("Game scan completed successfully");
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to emit scan progress events
|
// Helper function to emit scan progress events
|
||||||
fn emit_scan_progress(app_handle: &tauri::AppHandle, message: &str, progress: u32) {
|
fn emit_scan_progress(app_handle: &tauri::AppHandle, message: &str, progress: u32) {
|
||||||
// Log first, then emit the event
|
// Log first, then emit the event
|
||||||
info!("Scan progress: {}% - {}", progress, message);
|
info!("Scan progress: {}% - {}", progress, message);
|
||||||
|
|
||||||
let payload = serde_json::json!({
|
let payload = serde_json::json!({
|
||||||
"message": message,
|
"message": message,
|
||||||
"progress": progress
|
"progress": progress
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Err(e) = app_handle.emit("scan-progress", payload) {
|
if let Err(e) = app_handle.emit("scan-progress", payload) {
|
||||||
warn!("Failed to emit scan-progress event: {}", e);
|
warn!("Failed to emit scan-progress event: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch game info by ID - useful for single game updates
|
// Fetch game info by ID - useful for single game updates
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn get_game_info(game_id: String, state: State<AppState>) -> Result<Game, String> {
|
fn get_game_info(game_id: String, state: State<AppState>) -> Result<Game, String> {
|
||||||
let games = state.games.lock();
|
let games = state.games.lock();
|
||||||
games
|
games
|
||||||
.get(&game_id)
|
.get(&game_id)
|
||||||
.cloned()
|
.cloned()
|
||||||
.ok_or_else(|| format!("Game with ID {} not found", game_id))
|
.ok_or_else(|| format!("Game with ID {} not found", game_id))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unified action handler for installation and uninstallation
|
// Unified action handler for installation and uninstallation
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn process_game_action(
|
async fn process_game_action(
|
||||||
game_action: GameAction,
|
game_action: GameAction,
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
app_handle: tauri::AppHandle,
|
app_handle: tauri::AppHandle,
|
||||||
) -> Result<Game, String> {
|
) -> Result<Game, String> {
|
||||||
// Clone the information we need from state to avoid lifetime issues
|
// Clone the information we need from state to avoid lifetime issues
|
||||||
let game = {
|
let game = {
|
||||||
let games = state.games.lock();
|
let games = state.games.lock();
|
||||||
games
|
games
|
||||||
.get(&game_action.game_id)
|
.get(&game_action.game_id)
|
||||||
.cloned()
|
.cloned()
|
||||||
.ok_or_else(|| format!("Game with ID {} not found", game_action.game_id))?
|
.ok_or_else(|| format!("Game with ID {} not found", game_action.game_id))?
|
||||||
};
|
};
|
||||||
|
|
||||||
// Parse the action string to determine type and operation
|
// Parse the action string to determine type and operation
|
||||||
let (installer_type, action) = match game_action.action.as_str() {
|
let (installer_type, action) = match game_action.action.as_str() {
|
||||||
"install_cream" => (InstallerType::Cream, InstallerAction::Install),
|
"install_cream" => (InstallerType::Cream, InstallerAction::Install),
|
||||||
"uninstall_cream" => (InstallerType::Cream, InstallerAction::Uninstall),
|
"uninstall_cream" => (InstallerType::Cream, InstallerAction::Uninstall),
|
||||||
"install_smoke" => (InstallerType::Smoke, InstallerAction::Install),
|
"install_smoke" => (InstallerType::Smoke, InstallerAction::Install),
|
||||||
"uninstall_smoke" => (InstallerType::Smoke, InstallerAction::Uninstall),
|
"uninstall_smoke" => (InstallerType::Smoke, InstallerAction::Uninstall),
|
||||||
_ => return Err(format!("Invalid action: {}", game_action.action)),
|
_ => return Err(format!("Invalid action: {}", game_action.action)),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Execute the action
|
// Execute the action
|
||||||
installer::process_action(
|
installer::process_action(
|
||||||
game_action.game_id.clone(),
|
game_action.game_id.clone(),
|
||||||
installer_type,
|
installer_type,
|
||||||
action,
|
action,
|
||||||
game.clone(),
|
game.clone(),
|
||||||
app_handle.clone(),
|
app_handle.clone(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Update game status in state based on the action
|
// Update game status in state based on the action
|
||||||
let updated_game = {
|
let updated_game = {
|
||||||
let mut games_map = state.games.lock();
|
let mut games_map = state.games.lock();
|
||||||
let game = games_map.get_mut(&game_action.game_id).ok_or_else(|| {
|
let game = games_map.get_mut(&game_action.game_id).ok_or_else(|| {
|
||||||
format!(
|
format!(
|
||||||
"Game with ID {} not found after action",
|
"Game with ID {} not found after action",
|
||||||
game_action.game_id
|
game_action.game_id
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Update installation status
|
// Update installation status
|
||||||
match (installer_type, action) {
|
match (installer_type, action) {
|
||||||
(InstallerType::Cream, InstallerAction::Install) => {
|
(InstallerType::Cream, InstallerAction::Install) => {
|
||||||
game.cream_installed = true;
|
game.cream_installed = true;
|
||||||
}
|
}
|
||||||
(InstallerType::Cream, InstallerAction::Uninstall) => {
|
(InstallerType::Cream, InstallerAction::Uninstall) => {
|
||||||
game.cream_installed = false;
|
game.cream_installed = false;
|
||||||
}
|
}
|
||||||
(InstallerType::Smoke, InstallerAction::Install) => {
|
(InstallerType::Smoke, InstallerAction::Install) => {
|
||||||
game.smoke_installed = true;
|
game.smoke_installed = true;
|
||||||
}
|
}
|
||||||
(InstallerType::Smoke, InstallerAction::Uninstall) => {
|
(InstallerType::Smoke, InstallerAction::Uninstall) => {
|
||||||
game.smoke_installed = false;
|
game.smoke_installed = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset installing flag
|
// Reset installing flag
|
||||||
game.installing = false;
|
game.installing = false;
|
||||||
|
|
||||||
// Return updated game info
|
// Return updated game info
|
||||||
game.clone()
|
game.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Emit an event to update the UI for this specific game
|
// Emit an event to update the UI for this specific game
|
||||||
if let Err(e) = app_handle.emit("game-updated", &updated_game) {
|
if let Err(e) = app_handle.emit("game-updated", &updated_game) {
|
||||||
warn!("Failed to emit game-updated event: {}", e);
|
warn!("Failed to emit game-updated event: {}", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(updated_game)
|
Ok(updated_game)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch DLC list for a game
|
// Fetch DLC list for a game
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn fetch_game_dlcs(
|
async fn fetch_game_dlcs(
|
||||||
game_id: String,
|
game_id: String,
|
||||||
app_handle: tauri::AppHandle,
|
app_handle: tauri::AppHandle,
|
||||||
) -> Result<Vec<DlcInfoWithState>, String> {
|
) -> Result<Vec<DlcInfoWithState>, String> {
|
||||||
info!("Fetching DLCs for game ID: {}", game_id);
|
info!("Fetching DLCs for game ID: {}", game_id);
|
||||||
|
|
||||||
// Fetch DLC data
|
// Fetch DLC data
|
||||||
match installer::fetch_dlc_details(&game_id).await {
|
match installer::fetch_dlc_details(&game_id).await {
|
||||||
Ok(dlcs) => {
|
Ok(dlcs) => {
|
||||||
// Convert to DlcInfoWithState
|
// Convert to DlcInfoWithState
|
||||||
let dlcs_with_state = dlcs
|
let dlcs_with_state = dlcs
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|dlc| DlcInfoWithState {
|
.map(|dlc| DlcInfoWithState {
|
||||||
appid: dlc.appid,
|
appid: dlc.appid,
|
||||||
name: dlc.name,
|
name: dlc.name,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
// Cache in memory for this session (but not on disk)
|
// Cache in memory for this session (but not on disk)
|
||||||
let state = app_handle.state::<AppState>();
|
let state = app_handle.state::<AppState>();
|
||||||
let mut cache = state.dlc_cache.lock();
|
let mut cache = state.dlc_cache.lock();
|
||||||
cache.insert(
|
cache.insert(
|
||||||
game_id.clone(),
|
game_id.clone(),
|
||||||
DlcCache {
|
DlcCache {
|
||||||
data: dlcs_with_state.clone(),
|
data: dlcs_with_state.clone(),
|
||||||
timestamp: Instant::now(),
|
timestamp: Instant::now(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(dlcs_with_state)
|
Ok(dlcs_with_state)
|
||||||
}
|
}
|
||||||
Err(e) => Err(format!("Failed to fetch DLC details: {}", e)),
|
Err(e) => Err(format!("Failed to fetch DLC details: {}", e)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn abort_dlc_fetch(game_id: String, app_handle: tauri::AppHandle) -> Result<(), String> {
|
fn abort_dlc_fetch(game_id: String, app_handle: tauri::AppHandle) -> Result<(), String> {
|
||||||
info!("Request to abort DLC fetch for game ID: {}", game_id);
|
info!("Request to abort DLC fetch for game ID: {}", game_id);
|
||||||
|
|
||||||
let state = app_handle.state::<AppState>();
|
let state = app_handle.state::<AppState>();
|
||||||
state.fetch_cancellation.store(true, Ordering::SeqCst);
|
state.fetch_cancellation.store(true, Ordering::SeqCst);
|
||||||
|
|
||||||
// Reset after a short delay
|
// Reset after a short delay
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||||
let state = app_handle.state::<AppState>();
|
let state = app_handle.state::<AppState>();
|
||||||
state.fetch_cancellation.store(false, Ordering::SeqCst);
|
state.fetch_cancellation.store(false, Ordering::SeqCst);
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch DLC list with progress updates (streaming)
|
// Fetch DLC list with progress updates (streaming)
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn stream_game_dlcs(game_id: String, app_handle: tauri::AppHandle) -> Result<(), String> {
|
async fn stream_game_dlcs(game_id: String, app_handle: tauri::AppHandle) -> Result<(), String> {
|
||||||
info!("Streaming DLCs for game ID: {}", game_id);
|
info!("Streaming DLCs for game ID: {}", game_id);
|
||||||
|
|
||||||
// Fetch DLC data from API
|
// Fetch DLC data from API
|
||||||
match installer::fetch_dlc_details_with_progress(&game_id, &app_handle).await {
|
match installer::fetch_dlc_details_with_progress(&game_id, &app_handle).await {
|
||||||
Ok(dlcs) => {
|
Ok(dlcs) => {
|
||||||
info!(
|
info!(
|
||||||
"Successfully streamed {} DLCs for game {}",
|
"Successfully streamed {} DLCs for game {}",
|
||||||
dlcs.len(),
|
dlcs.len(),
|
||||||
game_id
|
game_id
|
||||||
);
|
);
|
||||||
|
|
||||||
// Convert to DLCInfoWithState for in-memory caching only
|
// Convert to DLCInfoWithState for in-memory caching only
|
||||||
let dlcs_with_state = dlcs
|
let dlcs_with_state = dlcs
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|dlc| DlcInfoWithState {
|
.map(|dlc| DlcInfoWithState {
|
||||||
appid: dlc.appid,
|
appid: dlc.appid,
|
||||||
name: dlc.name,
|
name: dlc.name,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
// Update in-memory cache without storing to disk
|
// Update in-memory cache without storing to disk
|
||||||
let state = app_handle.state::<AppState>();
|
let state = app_handle.state::<AppState>();
|
||||||
let mut dlc_cache = state.dlc_cache.lock();
|
let mut dlc_cache = state.dlc_cache.lock();
|
||||||
dlc_cache.insert(
|
dlc_cache.insert(
|
||||||
game_id.clone(),
|
game_id.clone(),
|
||||||
DlcCache {
|
DlcCache {
|
||||||
data: dlcs_with_state,
|
data: dlcs_with_state,
|
||||||
timestamp: tokio::time::Instant::now(),
|
timestamp: tokio::time::Instant::now(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to stream DLC details: {}", e);
|
error!("Failed to stream DLC details: {}", e);
|
||||||
// Emit error event
|
// Emit error event
|
||||||
let error_payload = serde_json::json!({
|
let error_payload = serde_json::json!({
|
||||||
"error": format!("Failed to fetch DLC details: {}", e)
|
"error": format!("Failed to fetch DLC details: {}", e)
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Err(emit_err) = app_handle.emit("dlc-error", error_payload) {
|
if let Err(emit_err) = app_handle.emit("dlc-error", error_payload) {
|
||||||
warn!("Failed to emit dlc-error event: {}", emit_err);
|
warn!("Failed to emit dlc-error event: {}", emit_err);
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(format!("Failed to fetch DLC details: {}", e))
|
Err(format!("Failed to fetch DLC details: {}", e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear caches command renamed to flush_data for clarity
|
// Clear caches command renamed to flush_data for clarity
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn clear_caches() -> Result<(), String> {
|
fn clear_caches() -> Result<(), String> {
|
||||||
info!("Data flush requested - cleaning in-memory state only");
|
info!("Data flush requested - cleaning in-memory state only");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the list of enabled DLCs for a game
|
// Get the list of enabled DLCs for a game
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn get_enabled_dlcs_command(game_path: String) -> Result<Vec<String>, String> {
|
fn get_enabled_dlcs_command(game_path: String) -> Result<Vec<String>, String> {
|
||||||
info!("Getting enabled DLCs for: {}", game_path);
|
info!("Getting enabled DLCs for: {}", game_path);
|
||||||
dlc_manager::get_enabled_dlcs(&game_path)
|
dlc_manager::get_enabled_dlcs(&game_path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the DLC configuration for a game
|
// Update the DLC configuration for a game
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn update_dlc_configuration_command(
|
fn update_dlc_configuration_command(
|
||||||
game_path: String,
|
game_path: String,
|
||||||
dlcs: Vec<DlcInfoWithState>,
|
dlcs: Vec<DlcInfoWithState>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
info!("Updating DLC configuration for: {}", game_path);
|
info!("Updating DLC configuration for: {}", game_path);
|
||||||
dlc_manager::update_dlc_configuration(&game_path, dlcs)
|
dlc_manager::update_dlc_configuration(&game_path, dlcs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Install CreamLinux with selected DLCs
|
// Install CreamLinux with selected DLCs
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn install_cream_with_dlcs_command(
|
async fn install_cream_with_dlcs_command(
|
||||||
game_id: String,
|
game_id: String,
|
||||||
selected_dlcs: Vec<DlcInfoWithState>,
|
selected_dlcs: Vec<DlcInfoWithState>,
|
||||||
app_handle: tauri::AppHandle,
|
app_handle: tauri::AppHandle,
|
||||||
) -> Result<Game, String> {
|
) -> Result<Game, String> {
|
||||||
info!(
|
info!(
|
||||||
"Installing CreamLinux with selected DLCs for game: {}",
|
"Installing CreamLinux with selected DLCs for game: {}",
|
||||||
game_id
|
game_id
|
||||||
);
|
);
|
||||||
|
|
||||||
// Clone selected_dlcs for later use
|
// Clone selected_dlcs for later use
|
||||||
let selected_dlcs_clone = selected_dlcs.clone();
|
let selected_dlcs_clone = selected_dlcs.clone();
|
||||||
|
|
||||||
// Install CreamLinux with the selected DLCs
|
// Install CreamLinux with the selected DLCs
|
||||||
match dlc_manager::install_cream_with_dlcs(game_id.clone(), app_handle.clone(), selected_dlcs)
|
match dlc_manager::install_cream_with_dlcs(game_id.clone(), app_handle.clone(), selected_dlcs)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
// Return updated game info
|
// Return updated game info
|
||||||
let state = app_handle.state::<AppState>();
|
let state = app_handle.state::<AppState>();
|
||||||
|
|
||||||
// Get a mutable reference and update the game
|
// Get a mutable reference and update the game
|
||||||
let game = {
|
let game = {
|
||||||
let mut games_map = state.games.lock();
|
let mut games_map = state.games.lock();
|
||||||
let game = games_map.get_mut(&game_id).ok_or_else(|| {
|
let game = games_map.get_mut(&game_id).ok_or_else(|| {
|
||||||
format!("Game with ID {} not found after installation", game_id)
|
format!("Game with ID {} not found after installation", game_id)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Update installation status
|
// Update installation status
|
||||||
game.cream_installed = true;
|
game.cream_installed = true;
|
||||||
game.installing = false;
|
game.installing = false;
|
||||||
|
|
||||||
// Clone the game for returning later
|
// Clone the game for returning later
|
||||||
game.clone()
|
game.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Emit an event to update the UI
|
// Emit an event to update the UI
|
||||||
if let Err(e) = app_handle.emit("game-updated", &game) {
|
if let Err(e) = app_handle.emit("game-updated", &game) {
|
||||||
warn!("Failed to emit game-updated event: {}", e);
|
warn!("Failed to emit game-updated event: {}", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show installation complete dialog with instructions
|
// Show installation complete dialog with instructions
|
||||||
let instructions = installer::InstallationInstructions {
|
let instructions = installer::InstallationInstructions {
|
||||||
type_: "cream_install".to_string(),
|
type_: "cream_install".to_string(),
|
||||||
command: "sh ./cream.sh %command%".to_string(),
|
command: "sh ./cream.sh %command%".to_string(),
|
||||||
game_title: game.title.clone(),
|
game_title: game.title.clone(),
|
||||||
dlc_count: Some(selected_dlcs_clone.iter().filter(|dlc| dlc.enabled).count()),
|
dlc_count: Some(selected_dlcs_clone.iter().filter(|dlc| dlc.enabled).count()),
|
||||||
};
|
};
|
||||||
|
|
||||||
installer::emit_progress(
|
installer::emit_progress(
|
||||||
&app_handle,
|
&app_handle,
|
||||||
&format!("Installation Completed: {}", game.title),
|
&format!("Installation Completed: {}", game.title),
|
||||||
"CreamLinux has been installed successfully!",
|
"CreamLinux has been installed successfully!",
|
||||||
100.0,
|
100.0,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
Some(instructions),
|
Some(instructions),
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(game)
|
Ok(game)
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to install CreamLinux with selected DLCs: {}", e);
|
error!("Failed to install CreamLinux with selected DLCs: {}", e);
|
||||||
Err(format!(
|
Err(format!(
|
||||||
"Failed to install CreamLinux with selected DLCs: {}",
|
"Failed to install CreamLinux with selected DLCs: {}",
|
||||||
e
|
e
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup logging
|
// Setup logging
|
||||||
fn setup_logging() -> Result<(), Box<dyn std::error::Error>> {
|
fn setup_logging() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
use log::LevelFilter;
|
use log::LevelFilter;
|
||||||
use log4rs::append::file::FileAppender;
|
use log4rs::append::file::FileAppender;
|
||||||
use log4rs::config::{Appender, Config, Root};
|
use log4rs::config::{Appender, Config, Root};
|
||||||
use log4rs::encode::pattern::PatternEncoder;
|
use log4rs::encode::pattern::PatternEncoder;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
// Get XDG cache directory
|
// Get XDG cache directory
|
||||||
let xdg_dirs = xdg::BaseDirectories::with_prefix("creamlinux")?;
|
let xdg_dirs = xdg::BaseDirectories::with_prefix("creamlinux")?;
|
||||||
let log_path = xdg_dirs.place_cache_file("creamlinux.log")?;
|
let log_path = xdg_dirs.place_cache_file("creamlinux.log")?;
|
||||||
|
|
||||||
// Clear the log file on startup
|
// Clear the log file on startup
|
||||||
if log_path.exists() {
|
if log_path.exists() {
|
||||||
if let Err(e) = fs::write(&log_path, "") {
|
if let Err(e) = fs::write(&log_path, "") {
|
||||||
eprintln!("Warning: Failed to clear log file: {}", e);
|
eprintln!("Warning: Failed to clear log file: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a file appender
|
// Create a file appender
|
||||||
let file = FileAppender::builder()
|
let file = FileAppender::builder()
|
||||||
.encoder(Box::new(PatternEncoder::new(
|
.encoder(Box::new(PatternEncoder::new(
|
||||||
"[{d(%Y-%m-%d %H:%M:%S)}] {l}: {m}\n",
|
"[{d(%Y-%m-%d %H:%M:%S)}] {l}: {m}\n",
|
||||||
)))
|
)))
|
||||||
.build(log_path)?;
|
.build(log_path)?;
|
||||||
|
|
||||||
// Build the config
|
// Build the config
|
||||||
let config = Config::builder()
|
let config = Config::builder()
|
||||||
.appender(Appender::builder().build("file", Box::new(file)))
|
.appender(Appender::builder().build("file", Box::new(file)))
|
||||||
.build(Root::builder().appender("file").build(LevelFilter::Info))?;
|
.build(Root::builder().appender("file").build(LevelFilter::Info))?;
|
||||||
|
|
||||||
// Initialize log4rs with this config
|
// Initialize log4rs with this config
|
||||||
log4rs::init_config(config)?;
|
log4rs::init_config(config)?;
|
||||||
|
|
||||||
info!("CreamLinux started with a clean log file");
|
info!("CreamLinux started with a clean log file");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
// Set up logging first
|
// Set up logging first
|
||||||
if let Err(e) = setup_logging() {
|
if let Err(e) = setup_logging() {
|
||||||
eprintln!("Warning: Failed to initialize logging: {}", e);
|
eprintln!("Warning: Failed to initialize logging: {}", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("Initializing CreamLinux application");
|
info!("Initializing CreamLinux application");
|
||||||
|
|
||||||
let app_state = AppState {
|
let app_state = AppState {
|
||||||
games: Mutex::new(HashMap::new()),
|
games: Mutex::new(HashMap::new()),
|
||||||
dlc_cache: Mutex::new(HashMap::new()),
|
dlc_cache: Mutex::new(HashMap::new()),
|
||||||
fetch_cancellation: Arc::new(AtomicBool::new(false)),
|
fetch_cancellation: Arc::new(AtomicBool::new(false)),
|
||||||
};
|
};
|
||||||
|
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_process::init())
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||||
.plugin(tauri_plugin_fs::init())
|
.plugin(tauri_plugin_shell::init())
|
||||||
.manage(app_state)
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.invoke_handler(tauri::generate_handler![
|
.plugin(tauri_plugin_fs::init())
|
||||||
scan_steam_games,
|
.manage(app_state)
|
||||||
get_game_info,
|
.invoke_handler(tauri::generate_handler![
|
||||||
process_game_action,
|
scan_steam_games,
|
||||||
fetch_game_dlcs,
|
get_game_info,
|
||||||
stream_game_dlcs,
|
process_game_action,
|
||||||
get_enabled_dlcs_command,
|
fetch_game_dlcs,
|
||||||
update_dlc_configuration_command,
|
stream_game_dlcs,
|
||||||
install_cream_with_dlcs_command,
|
get_enabled_dlcs_command,
|
||||||
get_all_dlcs_command,
|
update_dlc_configuration_command,
|
||||||
clear_caches,
|
install_cream_with_dlcs_command,
|
||||||
abort_dlc_fetch,
|
get_all_dlcs_command,
|
||||||
])
|
clear_caches,
|
||||||
.setup(|app| {
|
abort_dlc_fetch,
|
||||||
// Add a setup handler to do any initialization work
|
])
|
||||||
info!("Tauri application setup");
|
.setup(|app| {
|
||||||
|
// Add a setup handler to do any initialization work
|
||||||
|
info!("Tauri application setup");
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
{
|
{
|
||||||
if std::env::var("OPEN_DEVTOOLS").ok().as_deref() == Some("1") {
|
if std::env::var("OPEN_DEVTOOLS").ok().as_deref() == Some("1") {
|
||||||
if let Some(window) = app.get_webview_window("main") {
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
window.open_devtools();
|
window.open_devtools();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,19 +4,25 @@
|
|||||||
"frontendDist": "../dist",
|
"frontendDist": "../dist",
|
||||||
"devUrl": "http://localhost:1420",
|
"devUrl": "http://localhost:1420",
|
||||||
"beforeDevCommand": "npm run dev",
|
"beforeDevCommand": "npm run dev",
|
||||||
"beforeBuildCommand": "npm run build"
|
"beforeBuildCommand": "npm run sync-version && npm run build"
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
"active": true,
|
"active": true,
|
||||||
"targets": "all",
|
"targets": "all",
|
||||||
"category": "Utility",
|
"category": "Utility",
|
||||||
|
"createUpdaterArtifacts": true,
|
||||||
"icon": ["icons/128x128.png", "icons/128x128@2x.png", "icons/icon.png"]
|
"icon": ["icons/128x128.png", "icons/128x128@2x.png", "icons/icon.png"]
|
||||||
},
|
},
|
||||||
"productName": "Creamlinux",
|
"productName": "Creamlinux",
|
||||||
"mainBinaryName": "creamlinux",
|
"mainBinaryName": "creamlinux",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"identifier": "com.creamlinux.dev",
|
"identifier": "com.creamlinux.dev",
|
||||||
"plugins": {},
|
"plugins": {
|
||||||
|
"updater": {
|
||||||
|
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDJDNEI1NzBBRDUxODQ3RjEKUldUeFJ4alZDbGRMTE5Vc241NG5yL080UklnaW1iUGdUWElPRXloRGtKZ3M2SWkzK0RGSDh3Q2kK",
|
||||||
|
"endpoints": ["https://github.com/Novattz/rust-gui-dev/releases/latest/download/latest.json"]
|
||||||
|
}
|
||||||
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"withGlobalTauri": false,
|
"withGlobalTauri": false,
|
||||||
"windows": [
|
"windows": [
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useAppContext } from '@/contexts/useAppContext'
|
import { useAppContext } from '@/contexts/useAppContext'
|
||||||
|
import { UpdateChecker } from '@/components/updater'
|
||||||
import { useAppLogic } from '@/hooks'
|
import { useAppLogic } from '@/hooks'
|
||||||
import './styles/main.scss'
|
import './styles/main.scss'
|
||||||
|
|
||||||
@@ -104,6 +105,7 @@ function App() {
|
|||||||
onClose={handleDlcDialogClose}
|
onClose={handleDlcDialogClose}
|
||||||
onConfirm={handleDlcConfirm}
|
onConfirm={handleDlcConfirm}
|
||||||
/>
|
/>
|
||||||
|
<UpdateChecker />
|
||||||
</div>
|
</div>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
)
|
)
|
||||||
|
|||||||
147
src/components/updater/UpdateChecker.tsx
Normal file
147
src/components/updater/UpdateChecker.tsx
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { check, type Update, type DownloadEvent } from '@tauri-apps/plugin-updater'
|
||||||
|
import { relaunch } from '@tauri-apps/plugin-process'
|
||||||
|
import { Button } from '@/components/buttons'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* React component that checks for updates and provides
|
||||||
|
* UI for downloading and installing them
|
||||||
|
*/
|
||||||
|
const UpdateChecker = () => {
|
||||||
|
const [updateAvailable, setUpdateAvailable] = useState(false)
|
||||||
|
const [updateInfo, setUpdateInfo] = useState<Update | null>(null)
|
||||||
|
const [isChecking, setIsChecking] = useState(false)
|
||||||
|
const [isDownloading, setIsDownloading] = useState(false)
|
||||||
|
const [downloadProgress, setDownloadProgress] = useState(0)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
// Check for updates on component mount
|
||||||
|
useEffect(() => {
|
||||||
|
checkForUpdates()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const checkForUpdates = async () => {
|
||||||
|
try {
|
||||||
|
setIsChecking(true)
|
||||||
|
setError(null)
|
||||||
|
|
||||||
|
// Check for updates
|
||||||
|
const update = await check()
|
||||||
|
|
||||||
|
if (update) {
|
||||||
|
console.log(`Update available: ${update.version}`)
|
||||||
|
setUpdateAvailable(true)
|
||||||
|
setUpdateInfo(update)
|
||||||
|
} else {
|
||||||
|
console.log('No updates available')
|
||||||
|
setUpdateAvailable(false)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to check for updates:', err)
|
||||||
|
setError(`Failed to check for updates: ${err instanceof Error ? err.message : String(err)}`)
|
||||||
|
} finally {
|
||||||
|
setIsChecking(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadAndInstallUpdate = async () => {
|
||||||
|
if (!updateInfo) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsDownloading(true)
|
||||||
|
setError(null)
|
||||||
|
|
||||||
|
let downloaded = 0
|
||||||
|
let contentLength = 0
|
||||||
|
|
||||||
|
// Download and install update
|
||||||
|
await updateInfo.downloadAndInstall((event: DownloadEvent) => {
|
||||||
|
switch (event.event) {
|
||||||
|
case 'Started':
|
||||||
|
// Started event includes contentLength
|
||||||
|
if ('contentLength' in event.data && typeof event.data.contentLength === 'number') {
|
||||||
|
contentLength = event.data.contentLength
|
||||||
|
console.log(`Started downloading ${contentLength} bytes`)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'Progress':
|
||||||
|
// Progress event includes chunkLength
|
||||||
|
if ('chunkLength' in event.data && typeof event.data.chunkLength === 'number' && contentLength > 0) {
|
||||||
|
downloaded += event.data.chunkLength
|
||||||
|
const progress = (downloaded / contentLength) * 100
|
||||||
|
setDownloadProgress(progress)
|
||||||
|
console.log(`Downloaded ${downloaded} from ${contentLength}`)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'Finished':
|
||||||
|
console.log('Download finished')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('Update installed, relaunching application')
|
||||||
|
await relaunch()
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to download and install update:', err)
|
||||||
|
setError(`Failed to download and install update: ${err instanceof Error ? err.message : String(err)}`)
|
||||||
|
setIsDownloading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isChecking) {
|
||||||
|
return <div className="update-checker">Checking for updates...</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="update-checker error">
|
||||||
|
<p>{error}</p>
|
||||||
|
<Button variant="primary" onClick={checkForUpdates}>Try Again</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!updateAvailable || !updateInfo) {
|
||||||
|
return null // Don't show anything if there's no update
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="update-checker">
|
||||||
|
<div className="update-info">
|
||||||
|
<h3>Update Available</h3>
|
||||||
|
<p>Version {updateInfo.version} is available to download.</p>
|
||||||
|
{updateInfo.body && <p className="update-notes">{updateInfo.body}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isDownloading ? (
|
||||||
|
<div className="update-progress">
|
||||||
|
<div className="progress-bar-container">
|
||||||
|
<div
|
||||||
|
className="progress-bar"
|
||||||
|
style={{ width: `${downloadProgress}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p>Downloading: {Math.round(downloadProgress)}%</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="update-actions">
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={downloadAndInstallUpdate}
|
||||||
|
disabled={isDownloading}
|
||||||
|
>
|
||||||
|
Download & Install
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => setUpdateAvailable(false)}
|
||||||
|
>
|
||||||
|
Later
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UpdateChecker
|
||||||
1
src/components/updater/index.ts
Normal file
1
src/components/updater/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default as UpdateChecker } from './UpdateChecker'
|
||||||
@@ -1 +1,2 @@
|
|||||||
@forward './loading';
|
@forward './loading';
|
||||||
|
@forward './updater';
|
||||||
|
|||||||
85
src/styles/components/common/_updater.scss
Normal file
85
src/styles/components/common/_updater.scss
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
@use '../../themes/index' as *;
|
||||||
|
@use '../../abstracts/index' as *;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Update checker component styles
|
||||||
|
*/
|
||||||
|
.update-checker {
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background-color: var(--elevated-bg);
|
||||||
|
padding: 1.25rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
border: 1px solid var(--border-soft);
|
||||||
|
box-shadow: var(--shadow-standard);
|
||||||
|
max-width: 500px;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: var(--z-modal) - 1;
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
border-color: var(--danger);
|
||||||
|
background-color: var(--danger-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-info {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: var(--primary-color);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-weight: var(--bold);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-notes {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-soft);
|
||||||
|
max-height: 120px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0.5rem;
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
white-space: pre-line;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
@include custom-scrollbar;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-progress {
|
||||||
|
margin-top: 1rem;
|
||||||
|
|
||||||
|
.progress-bar-container {
|
||||||
|
height: 6px;
|
||||||
|
background-color: var(--border-soft);
|
||||||
|
border-radius: 3px;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user