diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 3ffc5a6b3..9e9c594ba 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -37,21 +37,23 @@ jobs: - name: Lint run: swiftlint --strict --reporter github-actions-logging - - name: Cache Swift packages + # Only the Git mirrors: a cached checkout missing a binary artifact (Sparkle.xcframework) is never + # downloaded again and fails the build, while checkouts and artifacts are quick to recreate + - name: Cache Swift package repositories uses: actions/cache@v6 with: - path: .build/SourcePackages - key: spm-${{ hashFiles('Whisky.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved', 'WhiskyKit/Package.swift') }} + path: .build/SourcePackages/repositories + key: spm-repositories-${{ hashFiles('Whisky.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved', 'WhiskyKit/Package.swift') }} - name: Test WhiskyKit run: swift test --package-path WhiskyKit - name: Build Whisky run: xcodebuild build -project Whisky.xcodeproj -scheme Whisky -configuration Release - -clonedSourcePackagesDirPath .build/SourcePackages -disableAutomaticPackageResolution + -clonedSourcePackagesDirPath .build/SourcePackages -onlyUsePackageVersionsFromResolvedFile CODE_SIGNING_ALLOWED=NO CODE_SIGN_IDENTITY="" - name: Build WhiskyCmd run: xcodebuild build -project Whisky.xcodeproj -scheme WhiskyCmd -configuration Release - -clonedSourcePackagesDirPath .build/SourcePackages -disableAutomaticPackageResolution + -clonedSourcePackagesDirPath .build/SourcePackages -onlyUsePackageVersionsFromResolvedFile CODE_SIGNING_ALLOWED=NO CODE_SIGN_IDENTITY="" diff --git a/README.md b/README.md index ce6dfed00..a5d35216b 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ *Windows games on your Mac, set up in a few clicks* - ![](https://img.shields.io/github/actions/workflow/status/iStorry/Whisky/CI.yml?branch=main&style=for-the-badge) + ![](https://img.shields.io/github/actions/workflow/status/iStorry/Brandy/CI.yml?branch=main&style=for-the-badge) --- diff --git a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_128x128.png b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_128x128.png index d97747d70..dbcc27a86 100644 Binary files a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_128x128.png and b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_128x128.png differ diff --git a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png index 543bb52a4..514959dab 100644 Binary files a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png and b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png differ diff --git a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_16x16.png b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_16x16.png index 90d063fba..6dda6a2b6 100644 Binary files a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_16x16.png and b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_16x16.png differ diff --git a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png index 25f96f32e..74fa038f5 100644 Binary files a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png and b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png differ diff --git a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_256x256.png b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_256x256.png index 543bb52a4..514959dab 100644 Binary files a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_256x256.png and b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_256x256.png differ diff --git a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png index 5f13526ea..a6d366772 100644 Binary files a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png and b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png differ diff --git a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_32x32.png b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_32x32.png index 25f96f32e..74fa038f5 100644 Binary files a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_32x32.png and b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_32x32.png differ diff --git a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png index 1c28a9e48..d97cbc7c1 100644 Binary files a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png and b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png differ diff --git a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_512x512.png b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_512x512.png index 5f13526ea..a6d366772 100644 Binary files a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_512x512.png and b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_512x512.png differ diff --git a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png index f4f67562d..3fc9e80bd 100644 Binary files a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png and b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png differ diff --git a/Whisky/Info.plist b/Whisky/Info.plist index cd0b8f8f7..6c0f14084 100644 --- a/Whisky/Info.plist +++ b/Whisky/Info.plist @@ -54,7 +54,7 @@ SUFeedURL - https://github.com/iStorry/Whisky/releases/latest/download/appcast.xml + https://github.com/iStorry/Brandy/releases/latest/download/appcast.xml UTExportedTypeDeclarations diff --git a/Whisky/Views/WhiskyApp.swift b/Whisky/Views/WhiskyApp.swift index d54319088..8ff59ffdd 100644 --- a/Whisky/Views/WhiskyApp.swift +++ b/Whisky/Views/WhiskyApp.swift @@ -124,7 +124,7 @@ struct WhiskyApp: App { } CommandGroup(replacing: .help) { Button("help.github") { - if let url = URL(string: "https://github.com/iStorry/Whisky") { + if let url = URL(string: "https://github.com/iStorry/Brandy") { openURL(url) } } diff --git a/WhiskyKit/Sources/WhiskyKit/WhiskyWine/WhiskyWineInstaller.swift b/WhiskyKit/Sources/WhiskyKit/WhiskyWine/WhiskyWineInstaller.swift index 7c4446135..5be539bbb 100644 --- a/WhiskyKit/Sources/WhiskyKit/WhiskyWine/WhiskyWineInstaller.swift +++ b/WhiskyKit/Sources/WhiskyKit/WhiskyWine/WhiskyWineInstaller.swift @@ -48,7 +48,7 @@ public class WhiskyWineInstaller { /// Assets of the release for the supported major version, which `scripts/publish-whiskywine.sh` publishes private static let distributionURL: URL = { - let release = "https://github.com/iStorry/Whisky/releases/download/whiskywine-\(supportedMajorVersion)/" + let release = "https://github.com/iStorry/Brandy/releases/download/whiskywine-\(supportedMajorVersion)/" guard let url = URL(string: release) else { preconditionFailure("Invalid WhiskyWine distribution URL") } diff --git a/WhiskyThumbnail/Icons.xcassets/Icon.imageset/512R512x1.png b/WhiskyThumbnail/Icons.xcassets/Icon.imageset/512R512x1.png index 3793b5fd9..a6d366772 100644 Binary files a/WhiskyThumbnail/Icons.xcassets/Icon.imageset/512R512x1.png and b/WhiskyThumbnail/Icons.xcassets/Icon.imageset/512R512x1.png differ diff --git a/images/brandy-icon.png b/images/brandy-icon.png index f4f67562d..3fc9e80bd 100644 Binary files a/images/brandy-icon.png and b/images/brandy-icon.png differ diff --git a/scripts/generate-app-icon.swift b/scripts/generate-app-icon.swift new file mode 100644 index 000000000..7e9394589 --- /dev/null +++ b/scripts/generate-app-icon.swift @@ -0,0 +1,278 @@ +#!/usr/bin/env swift +// +// generate-app-icon.swift +// Brandy +// +// This file is part of Whisky. +// +// Whisky is free software: you can redistribute it and/or modify it under the terms +// of the GNU General Public License as published by the Free Software Foundation, +// either version 3 of the License, or (at your option) any later version. +// +// Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Whisky. +// If not, see https://www.gnu.org/licenses/. +// + +// Renders the Brandy app icon at every size the asset catalogs need. +// Usage: swift scripts/generate-app-icon.swift (run from the repository root) + +import AppKit +import CoreGraphics + +/// All geometry is authored on Apple's 1024 pt macOS icon grid, y pointing down. +let canvas: CGFloat = 1024 +let tileRect = CGRect(x: 100, y: 100, width: 824, height: 824) +let tileSquircleExponent: CGFloat = 5 + +let sunCenter = CGPoint(x: 640, y: 300) +let sunRadius: CGFloat = 70 + +/// One band of liquid: a sine wave across the tile, filled with a vertical gradient below it. +struct Stratum { + let baseline: CGFloat + let amplitude: CGFloat + let wavelength: CGFloat + let phase: CGFloat + let topColor: UInt32 + let bottomColor: UInt32 + let crestColor: UInt32 +} + +/// Ordered from the top band down, distilling from deep wine at the bottom to gold at the top. +let strata: [Stratum] = [ + Stratum(baseline: 470, amplitude: 34, wavelength: 760, phase: 0.6, + topColor: 0xFFE3A2, bottomColor: 0xF5A947, crestColor: 0xFFF6DA), + Stratum(baseline: 568, amplitude: 38, wavelength: 700, phase: 2.2, + topColor: 0xF6A443, bottomColor: 0xD0621D, crestColor: 0xFFD08A), + Stratum(baseline: 664, amplitude: 40, wavelength: 820, phase: 3.9, + topColor: 0xD8502A, bottomColor: 0x9A2420, crestColor: 0xF38A5A), + Stratum(baseline: 758, amplitude: 36, wavelength: 740, phase: 5.3, + topColor: 0x921A2A, bottomColor: 0x560B18, crestColor: 0xC8455A), + Stratum(baseline: 848, amplitude: 30, wavelength: 680, phase: 1.4, + topColor: 0x5A0C1A, bottomColor: 0x2E040C, crestColor: 0x8E2238) +] + +let colorSpace = CGColorSpace(name: CGColorSpace.displayP3) ?? CGColorSpaceCreateDeviceRGB() + +func color(_ hex: UInt32, _ alpha: CGFloat = 1) -> CGColor { + CGColor( + colorSpace: colorSpace, + components: [ + CGFloat((hex >> 16) & 0xFF) / 255, + CGFloat((hex >> 8) & 0xFF) / 255, + CGFloat(hex & 0xFF) / 255, + alpha + ] + ) ?? CGColor(gray: 0, alpha: alpha) +} + +func gradient(_ stops: [(CGColor, CGFloat)]) -> CGGradient? { + CGGradient(colorsSpace: colorSpace, colors: stops.map(\.0) as CFArray, locations: stops.map(\.1)) +} + +func fill(_ ctx: CGContext, _ path: CGPath, with gradient: CGGradient?, from start: CGPoint, to end: CGPoint) { + guard let gradient else { return } + ctx.saveGState() + ctx.addPath(path) + ctx.clip() + ctx.drawLinearGradient(gradient, start: start, end: end, + options: [.drawsBeforeStartLocation, .drawsAfterEndLocation]) + ctx.restoreGState() +} + +func fillRadial(_ ctx: CGContext, _ path: CGPath, with gradient: CGGradient?, center: CGPoint, radius: CGFloat) { + guard let gradient else { return } + ctx.saveGState() + ctx.addPath(path) + ctx.clip() + ctx.drawRadialGradient(gradient, startCenter: center, startRadius: 0, + endCenter: center, endRadius: radius, options: []) + ctx.restoreGState() +} + +/// Superellipse tile, the continuous-corner shape macOS icons use. +func squirclePath(in rect: CGRect, exponent: CGFloat) -> CGPath { + let path = CGMutablePath() + let steps = 720 + for step in 0...steps { + let angle = CGFloat(step) / CGFloat(steps) * 2 * .pi + let cosine = cos(angle) + let sine = sin(angle) + let unitX = pow(abs(cosine), 2 / exponent) * (cosine < 0 ? -1 : 1) + let unitY = pow(abs(sine), 2 / exponent) * (sine < 0 ? -1 : 1) + let point = CGPoint(x: rect.midX + unitX * rect.width / 2, y: rect.midY + unitY * rect.height / 2) + if step == 0 { + path.move(to: point) + } else { + path.addLine(to: point) + } + } + path.closeSubpath() + return path +} + +extension Stratum { + func surfaceY(atX column: CGFloat) -> CGFloat { + let angle = 2 * .pi * column / wavelength + phase + return baseline + amplitude * sin(angle) + amplitude * 0.3 * sin(angle * 2.1 + 1) + } + + func crest() -> CGPath { + let path = CGMutablePath() + let range = stride(from: tileRect.minX - 20, through: tileRect.maxX + 20, by: 4) + for column in range { + let point = CGPoint(x: column, y: surfaceY(atX: column)) + if path.isEmpty { + path.move(to: point) + } else { + path.addLine(to: point) + } + } + return path + } + + func body() -> CGPath { + let path = CGMutablePath() + path.addPath(crest()) + path.addLine(to: CGPoint(x: tileRect.maxX + 20, y: canvas)) + path.addLine(to: CGPoint(x: tileRect.minX - 20, y: canvas)) + path.closeSubpath() + return path + } +} + +func drawTile(_ ctx: CGContext, _ tile: CGPath) { + ctx.saveGState() + ctx.setShadow(offset: CGSize(width: 0, height: 10), blur: 22, color: color(0x000000, 0.3)) + ctx.addPath(tile) + ctx.setFillColor(color(0x1C080C)) + ctx.fillPath() + ctx.restoreGState() + + fill( + ctx, tile, + with: gradient([(color(0x17070B), 0), (color(0x2E0F14), 0.5), (color(0x5A2218), 1)]), + from: CGPoint(x: 512, y: tileRect.minY), + to: CGPoint(x: 512, y: strata[0].baseline) + ) +} + +func drawSun(_ ctx: CGContext, _ tile: CGPath) { + fillRadial( + ctx, tile, + with: gradient([ + (color(0xFFC36A, 0.55), 0), (color(0xF58A3A, 0.18), 0.45), (color(0xF58A3A, 0), 1) + ]), + center: sunCenter, + radius: 330 + ) + let sun = CGPath( + ellipseIn: CGRect(x: sunCenter.x - sunRadius, y: sunCenter.y - sunRadius, + width: sunRadius * 2, height: sunRadius * 2), + transform: nil + ) + fill( + ctx, sun, + with: gradient([(color(0xFFF7E2), 0), (color(0xFFD892), 0.6), (color(0xFFB862), 1)]), + from: CGPoint(x: sunCenter.x, y: sunCenter.y - sunRadius), + to: CGPoint(x: sunCenter.x, y: sunCenter.y + sunRadius) + ) +} + +func drawStratum(_ ctx: CGContext, _ stratum: Stratum) { + let body = stratum.body() + + ctx.saveGState() + ctx.setShadow(offset: CGSize(width: 0, height: -8), blur: 26, color: color(0x0A0204, 0.55)) + ctx.addPath(body) + ctx.setFillColor(color(stratum.bottomColor)) + ctx.fillPath() + ctx.restoreGState() + + fill( + ctx, body, + with: gradient([(color(stratum.topColor), 0), (color(stratum.bottomColor), 1)]), + from: CGPoint(x: 512, y: stratum.baseline - stratum.amplitude), + to: CGPoint(x: 512, y: stratum.baseline + 150) + ) + + ctx.saveGState() + ctx.addPath(body) + ctx.clip() + ctx.addPath(stratum.crest()) + ctx.setLineWidth(7) + ctx.setStrokeColor(color(stratum.crestColor, 0.75)) + ctx.strokePath() + ctx.restoreGState() +} + +func drawTileEdge(_ ctx: CGContext) { + let edge = squirclePath(in: tileRect.insetBy(dx: 1.5, dy: 1.5), exponent: tileSquircleExponent) + .copy(strokingWithWidth: 3, lineCap: .round, lineJoin: .round, miterLimit: 10) + fill( + ctx, edge, + with: gradient([ + (color(0xFFE2C0, 0.22), 0), (color(0xFFE2C0, 0.03), 0.5), (color(0xFFE2C0, 0.08), 1) + ]), + from: CGPoint(x: 512, y: tileRect.minY), + to: CGPoint(x: 512, y: tileRect.maxY) + ) +} + +func renderIcon(pixels: Int) -> Data? { + guard let ctx = CGContext( + data: nil, width: pixels, height: pixels, bitsPerComponent: 8, bytesPerRow: 0, + space: colorSpace, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue + ) else { return nil } + + let scale = CGFloat(pixels) / canvas + ctx.translateBy(x: 0, y: CGFloat(pixels)) + ctx.scaleBy(x: scale, y: -scale) + + let tile = squirclePath(in: tileRect, exponent: tileSquircleExponent) + drawTile(ctx, tile) + + ctx.saveGState() + ctx.addPath(tile) + ctx.clip() + drawSun(ctx, tile) + strata.forEach { drawStratum(ctx, $0) } + ctx.restoreGState() + + drawTileEdge(ctx) + + guard let image = ctx.makeImage() else { return nil } + return NSBitmapImageRep(cgImage: image).representation(using: .png, properties: [:]) +} + +let outputs: [(path: String, pixels: Int)] = [ + ("Whisky/Assets.xcassets/AppIcon.appiconset/icon_16x16.png", 16), + ("Whisky/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png", 32), + ("Whisky/Assets.xcassets/AppIcon.appiconset/icon_32x32.png", 32), + ("Whisky/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png", 64), + ("Whisky/Assets.xcassets/AppIcon.appiconset/icon_128x128.png", 128), + ("Whisky/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png", 256), + ("Whisky/Assets.xcassets/AppIcon.appiconset/icon_256x256.png", 256), + ("Whisky/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png", 512), + ("Whisky/Assets.xcassets/AppIcon.appiconset/icon_512x512.png", 512), + ("Whisky/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png", 1024), + ("WhiskyThumbnail/Icons.xcassets/Icon.imageset/512R512x1.png", 512), + ("images/brandy-icon.png", 1024) +] + +for output in outputs { + guard let png = renderIcon(pixels: output.pixels) else { + FileHandle.standardError.write(Data("Failed to render \(output.path)\n".utf8)) + exit(1) + } + do { + try png.write(to: URL(fileURLWithPath: output.path)) + } catch { + FileHandle.standardError.write(Data("Failed to write \(output.path): \(error)\n".utf8)) + exit(1) + } +} diff --git a/scripts/publish-whiskywine.sh b/scripts/publish-whiskywine.sh index 7d48ad7aa..2523f9179 100755 --- a/scripts/publish-whiskywine.sh +++ b/scripts/publish-whiskywine.sh @@ -13,7 +13,7 @@ set -euo pipefail # Must match WhiskyWineInstaller.distributionURL; this checkout also has the upstream remote -readonly REPOSITORY="iStorry/Whisky" +readonly REPOSITORY="iStorry/Brandy" readonly ARCHIVE_NAME="Libraries.tar.gz" readonly VERSION_FILE="WhiskyWineVersion.plist"