fix(android): resolve icon generation and build script errors #165
imagemagick-anrdoid
into master
@ -0,0 +1,32 @@ |
|||
--- |
|||
alwaysApply: true |
|||
--- |
|||
# Asset Configuration Directive |
|||
*Scope: Assets Only (icons, splashes, image pipelines) — not overall build orchestration* |
|||
|
|||
## Intent |
|||
- Version **asset configuration files** (optionally dev-time generated). |
|||
- **Do not** version platform asset outputs (Android/iOS/Electron); generate them **at build-time** with standard tools. |
|||
- Keep existing per-platform build scripts unchanged. |
|||
|
|||
## Source of Truth |
|||
- **Preferred (Capacitor default):** `resources/` as the single master source. |
|||
- **Alternative:** `assets/` is acceptable **only** if `capacitor-assets` is explicitly configured to read from it. |
|||
- **Never** maintain both `resources/` and `assets/` as parallel sources. Migrate and delete the redundant folder. |
|||
|
|||
## Config Files |
|||
- Live under: `config/assets/` (committed). |
|||
- Examples: |
|||
- `config/assets/capacitor-assets.config.json` (or the path the tool expects) |
|||
- `config/assets/android.assets.json` |
|||
- `config/assets/ios.assets.json` |
|||
- `config/assets/common.assets.yaml` (optional shared layer) |
|||
- **Dev-time generation allowed** for these configs; **build-time generation is forbidden**. |
|||
|
|||
## Build-Time Behavior |
|||
- Build generates platform assets (not configs) using the standard chain: |
|||
```bash |
|||
npm run build:capacitor # web build via Vite (.mts) |
|||
npx cap sync |
|||
npx capacitor-assets generate # produces platform assets; not committed |
|||
# then platform-specific build steps |
@ -0,0 +1,142 @@ |
|||
name: Asset Validation & CI Safeguards |
|||
|
|||
on: |
|||
pull_request: |
|||
paths: |
|||
- 'resources/**' |
|||
- 'config/assets/**' |
|||
- 'capacitor-assets.config.json' |
|||
- 'capacitor.config.ts' |
|||
- 'capacitor.config.json' |
|||
push: |
|||
branches: [main, develop] |
|||
paths: |
|||
- 'resources/**' |
|||
- 'config/assets/**' |
|||
- 'capacitor-assets.config.json' |
|||
- 'capacitor.config.ts' |
|||
- 'capacitor.config.json' |
|||
|
|||
jobs: |
|||
asset-validation: |
|||
runs-on: ubuntu-latest |
|||
steps: |
|||
- name: Checkout code |
|||
uses: actions/checkout@v4 |
|||
|
|||
- name: Setup Node.js |
|||
uses: actions/setup-node@v4 |
|||
with: |
|||
node-version-file: '.nvmrc' |
|||
cache: 'npm' |
|||
|
|||
- name: Install dependencies |
|||
run: npm ci |
|||
|
|||
- name: Validate asset configuration |
|||
run: npm run assets:validate |
|||
|
|||
- name: Check for committed platform assets (Android) |
|||
run: | |
|||
if git ls-files -z android/app/src/main/res | grep -E '(AppIcon.*\.png|Splash.*\.png|mipmap-.*/ic_launcher.*\.png)' > /dev/null; then |
|||
echo "❌ Android platform assets found in VCS - these should be generated at build-time" |
|||
git ls-files -z android/app/src/main/res | grep -E '(AppIcon.*\.png|Splash.*\.png|mipmap-.*/ic_launcher.*\.png)' |
|||
exit 1 |
|||
fi |
|||
echo "✅ No Android platform assets committed" |
|||
|
|||
- name: Check for committed platform assets (iOS) |
|||
run: | |
|||
if git ls-files -z ios/App/App/Assets.xcassets | grep -E '(AppIcon.*\.png|Splash.*\.png)' > /dev/null; then |
|||
echo "❌ iOS platform assets found in VCS - these should be generated at build-time" |
|||
git ls-files -z ios/App/App/Assets.xcassets | grep -E '(AppIcon.*\.png|Splash.*\.png)' |
|||
exit 1 |
|||
fi |
|||
echo "✅ No iOS platform assets committed" |
|||
|
|||
- name: Test asset generation |
|||
run: | |
|||
echo "🧪 Testing asset generation workflow..." |
|||
npm run build:capacitor |
|||
npx cap sync |
|||
npx capacitor-assets generate --dry-run || npx capacitor-assets generate |
|||
echo "✅ Asset generation test completed" |
|||
|
|||
- name: Verify clean tree after build |
|||
run: | |
|||
if [ -n "$(git status --porcelain)" ]; then |
|||
echo "❌ Dirty tree after build - asset configs were modified" |
|||
git status |
|||
git diff |
|||
exit 1 |
|||
fi |
|||
echo "✅ Build completed with clean tree" |
|||
|
|||
schema-validation: |
|||
runs-on: ubuntu-latest |
|||
steps: |
|||
- name: Checkout code |
|||
uses: actions/checkout@v4 |
|||
|
|||
- name: Setup Node.js |
|||
uses: actions/setup-node@v4 |
|||
with: |
|||
node-version-file: '.nvmrc' |
|||
cache: 'npm' |
|||
|
|||
- name: Install dependencies |
|||
run: npm ci |
|||
|
|||
- name: Validate schema compliance |
|||
run: | |
|||
echo "🔍 Validating schema compliance..." |
|||
node -e " |
|||
const fs = require('fs'); |
|||
const config = JSON.parse(fs.readFileSync('capacitor-assets.config.json', 'utf8')); |
|||
const schema = JSON.parse(fs.readFileSync('config/assets/schema.json', 'utf8')); |
|||
|
|||
// Basic schema validation |
|||
if (!config.icon || !config.splash) { |
|||
throw new Error('Missing required sections: icon and splash'); |
|||
} |
|||
|
|||
if (!config.icon.source || !config.splash.source) { |
|||
throw new Error('Missing required source fields'); |
|||
} |
|||
|
|||
if (!/^resources\/.*\.(png|svg)$/.test(config.icon.source)) { |
|||
throw new Error('Icon source must be in resources/ directory'); |
|||
} |
|||
|
|||
if (!/^resources\/.*\.(png|svg)$/.test(config.splash.source)) { |
|||
throw new Error('Splash source must be in resources/ directory'); |
|||
} |
|||
|
|||
console.log('✅ Schema validation passed'); |
|||
" |
|||
|
|||
- name: Check source file existence |
|||
run: | |
|||
echo "📁 Checking source file existence..." |
|||
node -e " |
|||
const fs = require('fs'); |
|||
const config = JSON.parse(fs.readFileSync('capacitor-assets.config.json', 'utf8')); |
|||
|
|||
const requiredFiles = [ |
|||
config.icon.source, |
|||
config.splash.source |
|||
]; |
|||
|
|||
if (config.splash.darkSource) { |
|||
requiredFiles.push(config.splash.darkSource); |
|||
} |
|||
|
|||
const missingFiles = requiredFiles.filter(file => !fs.existsSync(file)); |
|||
|
|||
if (missingFiles.length > 0) { |
|||
console.error('❌ Missing source files:', missingFiles); |
|||
process.exit(1); |
|||
} |
|||
|
|||
console.log('✅ All source files exist'); |
|||
" |
@ -0,0 +1 @@ |
|||
18.19.0 |
@ -0,0 +1 @@ |
|||
18.19.0 |
@ -0,0 +1,7 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<resources> |
|||
<color name="colorPrimary">#3F51B5</color> |
|||
<color name="colorPrimaryDark">#303F9F</color> |
|||
<color name="colorAccent">#FF4081</color> |
|||
<color name="ic_launcher_background">#FFFFFF</color> |
|||
</resources> |
@ -1,2 +0,0 @@ |
|||
|
|||
Application icons are here. They are processed for android & ios by the `capacitor-assets` command, as indicated in the BUILDING.md file. |
@ -1,36 +1,32 @@ |
|||
{ |
|||
"icon": { |
|||
"ios": { |
|||
"source": "resources/ios/icon/icon.png", |
|||
"target": "ios/App/App/Assets.xcassets/AppIcon.appiconset" |
|||
}, |
|||
"android": { |
|||
"source": "resources/android/icon/icon.png", |
|||
"adaptive": { |
|||
"background": "#121212", |
|||
"foreground": "resources/icon.png", |
|||
"monochrome": "resources/icon.png" |
|||
}, |
|||
"target": "android/app/src/main/res" |
|||
}, |
|||
"ios": { |
|||
"padding": 0, |
|||
"target": "ios/App/App/Assets.xcassets/AppIcon.appiconset" |
|||
}, |
|||
"source": "resources/icon.png", |
|||
"web": { |
|||
"source": "resources/web/icon/icon.png", |
|||
"target": "public/img/icons" |
|||
} |
|||
}, |
|||
"splash": { |
|||
"ios": { |
|||
"source": "resources/ios/splash/splash.png", |
|||
"target": "ios/App/App/Assets.xcassets/Splash.imageset" |
|||
}, |
|||
"android": { |
|||
"source": "resources/android/splash/splash.png", |
|||
"scale": "cover", |
|||
"target": "android/app/src/main/res" |
|||
} |
|||
}, |
|||
"splashDark": { |
|||
"darkSource": "resources/splash_dark.png", |
|||
"ios": { |
|||
"source": "resources/ios/splash/splash_dark.png", |
|||
"target": "ios/App/App/Assets.xcassets/SplashDark.imageset" |
|||
"target": "ios/App/App/Assets.xcassets", |
|||
"useStoryBoard": true |
|||
}, |
|||
"android": { |
|||
"source": "resources/android/splash/splash_dark.png", |
|||
"target": "android/app/src/main/res" |
|||
} |
|||
"source": "resources/splash.png" |
|||
} |
|||
} |
@ -0,0 +1,116 @@ |
|||
import { CapacitorConfig } from '@capacitor/cli'; |
|||
|
|||
const config: CapacitorConfig = { |
|||
appId: 'app.timesafari', |
|||
appName: 'TimeSafari', |
|||
webDir: 'dist', |
|||
server: { |
|||
cleartext: true |
|||
}, |
|||
plugins: { |
|||
App: { |
|||
appUrlOpen: { |
|||
handlers: [ |
|||
{ |
|||
url: 'timesafari://*', |
|||
autoVerify: true |
|||
} |
|||
] |
|||
} |
|||
}, |
|||
SplashScreen: { |
|||
launchShowDuration: 3000, |
|||
launchAutoHide: true, |
|||
backgroundColor: '#ffffff', |
|||
androidSplashResourceName: 'splash', |
|||
androidScaleType: 'CENTER_CROP', |
|||
showSpinner: false, |
|||
androidSpinnerStyle: 'large', |
|||
iosSpinnerStyle: 'small', |
|||
spinnerColor: '#999999', |
|||
splashFullScreen: true, |
|||
splashImmersive: true |
|||
}, |
|||
CapSQLite: { |
|||
iosDatabaseLocation: 'Library/CapacitorDatabase', |
|||
iosIsEncryption: false, |
|||
iosBiometric: { |
|||
biometricAuth: false, |
|||
biometricTitle: 'Biometric login for TimeSafari' |
|||
}, |
|||
androidIsEncryption: false, |
|||
androidBiometric: { |
|||
biometricAuth: false, |
|||
biometricTitle: 'Biometric login for TimeSafari' |
|||
}, |
|||
electronIsEncryption: false |
|||
} |
|||
}, |
|||
ios: { |
|||
contentInset: 'never', |
|||
allowsLinkPreview: true, |
|||
scrollEnabled: true, |
|||
limitsNavigationsToAppBoundDomains: true, |
|||
backgroundColor: '#ffffff', |
|||
allowNavigation: [ |
|||
'*.timesafari.app', |
|||
'*.jsdelivr.net', |
|||
'api.endorser.ch' |
|||
] |
|||
}, |
|||
android: { |
|||
allowMixedContent: true, |
|||
captureInput: true, |
|||
webContentsDebuggingEnabled: false, |
|||
allowNavigation: [ |
|||
'*.timesafari.app', |
|||
'*.jsdelivr.net', |
|||
'api.endorser.ch', |
|||
'10.0.2.2:3000' |
|||
] |
|||
}, |
|||
electron: { |
|||
deepLinking: { |
|||
schemes: ['timesafari'] |
|||
}, |
|||
buildOptions: { |
|||
appId: 'app.timesafari', |
|||
productName: 'TimeSafari', |
|||
directories: { |
|||
output: 'dist-electron-packages' |
|||
}, |
|||
files: [ |
|||
'dist/**/*', |
|||
'electron/**/*' |
|||
], |
|||
mac: { |
|||
category: 'public.app-category.productivity', |
|||
target: [ |
|||
{ |
|||
target: 'dmg', |
|||
arch: ['x64', 'arm64'] |
|||
} |
|||
] |
|||
}, |
|||
win: { |
|||
target: [ |
|||
{ |
|||
target: 'nsis', |
|||
arch: ['x64'] |
|||
} |
|||
] |
|||
}, |
|||
linux: { |
|||
target: [ |
|||
{ |
|||
target: 'AppImage', |
|||
arch: ['x64'] |
|||
} |
|||
], |
|||
category: 'Utility' |
|||
} |
|||
} |
|||
} |
|||
}; |
|||
|
|||
export default config; |
@ -0,0 +1,32 @@ |
|||
{ |
|||
"icon": { |
|||
"android": { |
|||
"adaptive": { |
|||
"background": "#121212", |
|||
"foreground": "resources/icon.png", |
|||
"monochrome": "resources/icon.png" |
|||
}, |
|||
"target": "android/app/src/main/res" |
|||
}, |
|||
"ios": { |
|||
"padding": 0, |
|||
"target": "ios/App/App/Assets.xcassets/AppIcon.appiconset" |
|||
}, |
|||
"source": "resources/icon.png", |
|||
"web": { |
|||
"target": "public/img/icons" |
|||
} |
|||
}, |
|||
"splash": { |
|||
"android": { |
|||
"scale": "cover", |
|||
"target": "android/app/src/main/res" |
|||
}, |
|||
"darkSource": "resources/splash_dark.png", |
|||
"ios": { |
|||
"target": "ios/App/App/Assets.xcassets", |
|||
"useStoryBoard": true |
|||
}, |
|||
"source": "resources/splash.png" |
|||
} |
|||
} |
@ -0,0 +1,119 @@ |
|||
{ |
|||
"$schema": "http://json-schema.org/draft-07/schema#", |
|||
"title": "Capacitor Assets Configuration Schema", |
|||
"description": "Schema for validating capacitor-assets configuration files", |
|||
"type": "object", |
|||
"properties": { |
|||
"icon": { |
|||
"type": "object", |
|||
"properties": { |
|||
"source": { |
|||
"type": "string", |
|||
"pattern": "^resources/.*\\.(png|svg)$", |
|||
"description": "Source icon file path relative to project root" |
|||
}, |
|||
"android": { |
|||
"type": "object", |
|||
"properties": { |
|||
"adaptive": { |
|||
"type": "object", |
|||
"properties": { |
|||
"foreground": { |
|||
"type": "string", |
|||
"pattern": "^resources/.*\\.(png|svg)$", |
|||
"description": "Foreground icon for Android adaptive icons" |
|||
}, |
|||
"background": { |
|||
"type": ["string", "object"], |
|||
"description": "Background color or image for adaptive icons" |
|||
}, |
|||
"monochrome": { |
|||
"type": "string", |
|||
"pattern": "^resources/.*\\.(png|svg)$", |
|||
"description": "Monochrome icon for Android 13+" |
|||
} |
|||
}, |
|||
"required": ["foreground", "background"] |
|||
}, |
|||
"target": { |
|||
"type": "string", |
|||
"description": "Android target directory for generated icons" |
|||
} |
|||
} |
|||
}, |
|||
"ios": { |
|||
"type": "object", |
|||
"properties": { |
|||
"padding": { |
|||
"type": "number", |
|||
"minimum": 0, |
|||
"maximum": 1, |
|||
"description": "Padding ratio for iOS icons" |
|||
}, |
|||
"target": { |
|||
"type": "string", |
|||
"description": "iOS target directory for generated icons" |
|||
} |
|||
} |
|||
}, |
|||
"web": { |
|||
"type": "object", |
|||
"properties": { |
|||
"target": { |
|||
"type": "string", |
|||
"description": "Web target directory for generated icons" |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
"required": ["source"], |
|||
"additionalProperties": false |
|||
}, |
|||
"splash": { |
|||
"type": "object", |
|||
"properties": { |
|||
"source": { |
|||
"type": "string", |
|||
"pattern": "^resources/.*\\.(png|svg)$", |
|||
"description": "Source splash screen file" |
|||
}, |
|||
"darkSource": { |
|||
"type": "string", |
|||
"pattern": "^resources/.*\\.(png|svg)$", |
|||
"description": "Dark mode splash screen file" |
|||
}, |
|||
"android": { |
|||
"type": "object", |
|||
"properties": { |
|||
"scale": { |
|||
"type": "string", |
|||
"enum": ["cover", "contain", "fill"], |
|||
"description": "Android splash screen scaling mode" |
|||
}, |
|||
"target": { |
|||
"type": "string", |
|||
"description": "Android target directory for splash screens" |
|||
} |
|||
} |
|||
}, |
|||
"ios": { |
|||
"type": "object", |
|||
"properties": { |
|||
"useStoryBoard": { |
|||
"type": "boolean", |
|||
"description": "Use LaunchScreen storyboard instead of splash assets" |
|||
}, |
|||
"target": { |
|||
"type": "string", |
|||
"description": "iOS target directory for splash screens" |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
"required": ["source"], |
|||
"additionalProperties": false |
|||
} |
|||
}, |
|||
"required": ["icon", "splash"], |
|||
"additionalProperties": false |
|||
} |
@ -0,0 +1,214 @@ |
|||
# TimeSafari Asset Configuration Migration Plan |
|||
|
|||
**Author**: Matthew Raymer |
|||
**Date**: 2025-08-14 |
|||
**Status**: 🎯 **IMPLEMENTATION** - Ready for Execution |
|||
|
|||
## Overview |
|||
|
|||
This document outlines the migration from the current mixed asset management |
|||
system to a standardized, single-source asset configuration approach using |
|||
`capacitor-assets` as the standard generator. |
|||
|
|||
## Current State Analysis |
|||
|
|||
### Asset Sources (Duplicated) |
|||
|
|||
- **`assets/` directory**: Contains `icon.png`, `splash.png`, `splash_dark.png` |
|||
- **`resources/` directory**: Contains identical files in platform-specific subdirectories |
|||
- **Result**: Duplicate storage, confusion about source of truth |
|||
|
|||
### Asset Generation (Manual) |
|||
|
|||
- **Custom scripts**: `generate-icons.sh`, `generate-ios-assets.sh`, `generate-android-icons.sh` |
|||
- **Bypass capacitor-assets**: Manual ImageMagick-based generation |
|||
- **Inconsistent outputs**: Different generation methods for each platform |
|||
|
|||
### Configuration (Scattered) |
|||
|
|||
- **`capacitor-assets.config.json`**: Basic configuration at root |
|||
- **Platform-specific configs**: Mixed in various build scripts |
|||
- **No validation**: No schema or consistency checks |
|||
|
|||
## Target State |
|||
|
|||
### Single Source of Truth |
|||
|
|||
- **`resources/` directory**: Capacitor default location for source assets |
|||
- **Eliminate duplication**: Remove `assets/` directory after migration |
|||
- **Standardized paths**: All tools read from `resources/` |
|||
|
|||
### Standardized Generation |
|||
|
|||
- **`capacitor-assets`**: Single tool for all platform asset generation |
|||
- **Build-time generation**: Assets generated during build, not committed |
|||
- **Deterministic outputs**: Same inputs → same outputs every time |
|||
|
|||
### Centralized Configuration |
|||
|
|||
- **`config/assets/`**: All asset-related configuration files |
|||
- **Schema validation**: JSON schema for configuration validation |
|||
- **CI safeguards**: Automated validation and compliance checks |
|||
|
|||
## Migration Steps |
|||
|
|||
### Phase 1: Foundation Setup ✅ |
|||
|
|||
- [x] Create `config/assets/` directory structure |
|||
- [x] Create asset configuration schema (`schema.json`) |
|||
- [x] Create enhanced capacitor-assets configuration |
|||
- [x] Convert `capacitor.config.json` to `capacitor.config.ts` |
|||
- [x] Pin Node.js version (`.nvmrc`, `.node-version`) |
|||
- [x] Create dev-time asset configuration generator |
|||
- [x] Create asset configuration validator |
|||
- [x] Add npm scripts for asset management |
|||
- [x] Update `.gitignore` with proper asset exclusions |
|||
- [x] Create CI workflow for asset validation |
|||
|
|||
### Phase 2: Validation & Testing |
|||
|
|||
- [ ] Run `npm run assets:config` to generate new configuration |
|||
- [ ] Run `npm run assets:validate` to verify configuration |
|||
- [ ] Test `npm run build:native` workflow |
|||
- [ ] Verify CI workflow passes all checks |
|||
- [ ] Confirm no platform assets are committed to VCS |
|||
|
|||
### Phase 3: Cleanup & Removal |
|||
|
|||
- [ ] Remove `assets/` directory (after validation) |
|||
- [ ] Remove manual asset generation scripts |
|||
- [ ] Remove asset checking scripts |
|||
- [ ] Update documentation references |
|||
- [ ] Final validation of clean state |
|||
|
|||
## Implementation Details |
|||
|
|||
### File Structure |
|||
|
|||
``` |
|||
resources/ # Image sources ONLY |
|||
icon.png |
|||
splash.png |
|||
splash_dark.png |
|||
|
|||
config/assets/ # Versioned config & schema |
|||
capacitor-assets.config.json |
|||
schema.json |
|||
|
|||
scripts/ |
|||
assets-config.js # Dev-time config generator |
|||
assets-validator.js # Schema validator |
|||
``` |
|||
|
|||
### Configuration Schema |
|||
|
|||
The schema enforces: |
|||
- Source files must be in `resources/` directory |
|||
- Required fields for icon and splash sections |
|||
- Android adaptive icon support (foreground/background/monochrome) |
|||
- iOS LaunchScreen preferences |
|||
- Target directory validation |
|||
|
|||
### CI Safeguards |
|||
|
|||
- **Schema validation**: Configuration must comply with schema |
|||
- **Source file validation**: All referenced files must exist |
|||
- **Platform asset denial**: Reject commits with generated assets |
|||
- **Clean tree enforcement**: Build must not modify committed configs |
|||
|
|||
## Testing Strategy |
|||
|
|||
### Local Validation |
|||
|
|||
```bash |
|||
# Generate configuration |
|||
npm run assets:config |
|||
|
|||
# Validate configuration |
|||
npm run assets:validate |
|||
|
|||
# Test build workflow |
|||
npm run build:native |
|||
|
|||
# Clean generated assets |
|||
npm run assets:clean |
|||
``` |
|||
|
|||
### CI Validation |
|||
|
|||
- **Asset validation workflow**: Runs on asset-related changes |
|||
- **Schema compliance**: Ensures configuration follows schema |
|||
- **Source file existence**: Verifies all referenced files exist |
|||
- **Platform asset detection**: Prevents committed generated assets |
|||
- **Build tree verification**: Ensures clean tree after build |
|||
|
|||
## Risk Mitigation |
|||
|
|||
### Data Loss Prevention |
|||
|
|||
- **Backup branch**: Create backup before removing `assets/` |
|||
- **Validation checks**: Multiple validation steps before removal |
|||
- **Gradual migration**: Phase-by-phase approach with rollback capability |
|||
|
|||
### Build Continuity |
|||
|
|||
- **Per-platform scripts unchanged**: All existing build orchestration preserved |
|||
- **Standard toolchain**: Uses capacitor-assets, not custom scripts |
|||
- **Fallback support**: Manual scripts remain until migration complete |
|||
|
|||
### Configuration Consistency |
|||
|
|||
- **Schema enforcement**: JSON schema prevents invalid configurations |
|||
- **CI validation**: Automated checks catch configuration issues |
|||
- **Documentation updates**: Clear guidance for future changes |
|||
|
|||
## Success Criteria |
|||
|
|||
### Technical Requirements |
|||
|
|||
- [ ] Single source of truth in `resources/` directory |
|||
- [ ] All platform assets generated via `capacitor-assets` |
|||
- [ ] No manual asset generation scripts |
|||
- [ ] Configuration validation passes all checks |
|||
- [ ] CI workflow enforces asset policies |
|||
|
|||
### Quality Metrics |
|||
|
|||
- [ ] Zero duplicate asset sources |
|||
- [ ] 100% configuration schema compliance |
|||
- [ ] No platform assets committed to VCS |
|||
- [ ] Clean build tree after asset generation |
|||
- [ ] Deterministic asset outputs |
|||
|
|||
### User Experience |
|||
|
|||
- [ ] Clear asset management documentation |
|||
- [ ] Simple development commands |
|||
- [ ] Consistent asset generation across platforms |
|||
- [ ] Reduced confusion about asset sources |
|||
|
|||
## Next Steps |
|||
|
|||
1. **Execute Phase 2**: Run validation and testing steps |
|||
2. **Verify CI workflow**: Ensure all checks pass |
|||
3. **Execute Phase 3**: Remove duplicate assets and scripts |
|||
4. **Update documentation**: Finalize README and BUILDING.md |
|||
5. **Team training**: Ensure all developers understand new workflow |
|||
|
|||
## Rollback Plan |
|||
|
|||
If issues arise during migration: |
|||
|
|||
1. **Restore backup branch**: `git checkout backup-before-asset-migration` |
|||
2. **Revert configuration changes**: Remove new config files |
|||
3. **Restore manual scripts**: Re-enable previous asset generation |
|||
4. **Investigate issues**: Identify and resolve root causes |
|||
5. **Plan revised migration**: Adjust approach based on lessons learned |
|||
|
|||
--- |
|||
|
|||
**Status**: Ready for Phase 2 execution |
|||
**Priority**: High |
|||
**Estimated Effort**: 2-3 hours |
|||
**Dependencies**: CI workflow validation |
|||
**Stakeholders**: Development team |
@ -0,0 +1,23 @@ |
|||
{ |
|||
"images" : [ |
|||
{ |
|||
"filename" : "splash@1x.png", |
|||
"idiom" : "universal", |
|||
"scale" : "1x" |
|||
}, |
|||
{ |
|||
"filename" : "splash@2x.png", |
|||
"idiom" : "universal", |
|||
"scale" : "2x" |
|||
}, |
|||
{ |
|||
"filename" : "splash@3x.png", |
|||
"idiom" : "universal", |
|||
"scale" : "3x" |
|||
} |
|||
], |
|||
"info" : { |
|||
"author" : "xcode", |
|||
"version" : 1 |
|||
} |
|||
} |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 62 KiB |
After Width: | Height: | Size: 156 KiB |
Before Width: | Height: | Size: 279 KiB After Width: | Height: | Size: 279 KiB |
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.9 MiB |
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.9 MiB |
@ -0,0 +1,224 @@ |
|||
#!/usr/bin/env tsx |
|||
|
|||
/** |
|||
* TimeSafari Asset Configuration Generator |
|||
* Generates capacitor-assets configuration files with deterministic outputs |
|||
* Author: Matthew Raymer |
|||
* |
|||
* Usage: tsx scripts/assets-config.ts |
|||
*/ |
|||
|
|||
import fs from 'fs'; |
|||
import path from 'path'; |
|||
import { fileURLToPath } from 'url'; |
|||
|
|||
const __filename = fileURLToPath(import.meta.url); |
|||
const __dirname = path.dirname(__filename); |
|||
const PROJECT_ROOT = path.dirname(__dirname); |
|||
|
|||
// TypeScript interfaces for asset configuration
|
|||
interface AdaptiveIconConfig { |
|||
foreground: string; |
|||
background: string; |
|||
monochrome: string; |
|||
} |
|||
|
|||
interface AndroidIconConfig { |
|||
adaptive: AdaptiveIconConfig; |
|||
target: string; |
|||
} |
|||
|
|||
interface IOSIconConfig { |
|||
padding: number; |
|||
target: string; |
|||
} |
|||
|
|||
interface WebIconConfig { |
|||
target: string; |
|||
} |
|||
|
|||
interface IconConfig { |
|||
source: string; |
|||
android: AndroidIconConfig; |
|||
ios: IOSIconConfig; |
|||
web: WebIconConfig; |
|||
} |
|||
|
|||
interface AndroidSplashConfig { |
|||
scale: string; |
|||
target: string; |
|||
} |
|||
|
|||
interface IOSSplashConfig { |
|||
useStoryBoard: boolean; |
|||
target: string; |
|||
} |
|||
|
|||
interface SplashConfig { |
|||
source: string; |
|||
darkSource: string; |
|||
android: AndroidSplashConfig; |
|||
ios: IOSSplashConfig; |
|||
} |
|||
|
|||
interface AssetConfig { |
|||
icon: IconConfig; |
|||
splash: SplashConfig; |
|||
} |
|||
|
|||
/** |
|||
* Generate deterministic capacitor-assets configuration |
|||
* @returns Sorted, stable configuration object |
|||
*/ |
|||
function generateAssetConfig(): AssetConfig { |
|||
const config: AssetConfig = { |
|||
icon: { |
|||
source: "resources/icon.png", |
|||
android: { |
|||
adaptive: { |
|||
foreground: "resources/icon.png", |
|||
background: "#121212", |
|||
monochrome: "resources/icon.png" |
|||
}, |
|||
target: "android/app/src/main/res" |
|||
}, |
|||
ios: { |
|||
padding: 0, |
|||
target: "ios/App/App/Assets.xcassets/AppIcon.appiconset" |
|||
}, |
|||
web: { |
|||
target: "public/img/icons" |
|||
} |
|||
}, |
|||
splash: { |
|||
source: "resources/splash.png", |
|||
darkSource: "resources/splash_dark.png", |
|||
android: { |
|||
scale: "cover", |
|||
target: "android/app/src/main/res" |
|||
}, |
|||
ios: { |
|||
useStoryBoard: true, |
|||
target: "ios/App/App/Assets.xcassets" |
|||
} |
|||
} |
|||
}; |
|||
|
|||
return sortObjectKeys(config); |
|||
} |
|||
|
|||
/** |
|||
* Sort object keys recursively for deterministic output |
|||
* @param obj - Object to sort |
|||
* @returns Object with sorted keys |
|||
*/ |
|||
function sortObjectKeys(obj: any): any { |
|||
if (obj === null || typeof obj !== 'object') { |
|||
return obj; |
|||
} |
|||
|
|||
if (Array.isArray(obj)) { |
|||
return obj.map(sortObjectKeys); |
|||
} |
|||
|
|||
const sorted: any = {}; |
|||
Object.keys(obj) |
|||
.sort() |
|||
.forEach(key => { |
|||
sorted[key] = sortObjectKeys(obj[key]); |
|||
}); |
|||
|
|||
return sorted; |
|||
} |
|||
|
|||
/** |
|||
* Validate that required source files exist |
|||
*/ |
|||
function validateSourceFiles(): void { |
|||
const requiredFiles = [ |
|||
'resources/icon.png', |
|||
'resources/splash.png', |
|||
'resources/splash_dark.png' |
|||
]; |
|||
|
|||
const missingFiles = requiredFiles.filter(file => { |
|||
const filePath = path.join(PROJECT_ROOT, file); |
|||
return !fs.existsSync(filePath); |
|||
}); |
|||
|
|||
if (missingFiles.length > 0) { |
|||
console.error('❌ Missing required source files:'); |
|||
missingFiles.forEach(file => console.error(` ${file}`)); |
|||
process.exit(1); |
|||
} |
|||
|
|||
console.log('✅ All required source files found'); |
|||
} |
|||
|
|||
/** |
|||
* Write configuration to file with consistent formatting |
|||
* @param config - Configuration object |
|||
* @param outputPath - Output file path |
|||
*/ |
|||
function writeConfig(config: AssetConfig, outputPath: string): void { |
|||
const jsonString = JSON.stringify(config, null, 2); |
|||
|
|||
// Ensure consistent line endings and no trailing whitespace
|
|||
const cleanJson = jsonString |
|||
.split('\n') |
|||
.map(line => line.trimEnd()) |
|||
.join('\n') + '\n'; |
|||
|
|||
fs.writeFileSync(outputPath, cleanJson, 'utf8'); |
|||
console.log(`✅ Configuration written to: ${outputPath}`); |
|||
} |
|||
|
|||
/** |
|||
* Main execution function |
|||
*/ |
|||
function main(): void { |
|||
console.log('🔄 Generating TimeSafari asset configuration...'); |
|||
console.log(`📁 Project root: ${PROJECT_ROOT}`); |
|||
console.log(`📅 Generated: ${new Date().toISOString()}`); |
|||
|
|||
try { |
|||
// Validate source files exist
|
|||
validateSourceFiles(); |
|||
|
|||
// Generate configuration
|
|||
const config = generateAssetConfig(); |
|||
|
|||
// Ensure config directory exists
|
|||
const configDir = path.join(PROJECT_ROOT, 'config', 'assets'); |
|||
if (!fs.existsSync(configDir)) { |
|||
fs.mkdirSync(configDir, { recursive: true }); |
|||
} |
|||
|
|||
// Write configuration files
|
|||
const capacitorAssetsConfigPath = path.join(configDir, 'capacitor-assets.config.json'); |
|||
writeConfig(config, capacitorAssetsConfigPath); |
|||
|
|||
// Copy to root for capacitor-assets discovery
|
|||
const rootConfigPath = path.join(PROJECT_ROOT, 'capacitor-assets.config.json'); |
|||
writeConfig(config, rootConfigPath); |
|||
|
|||
console.log('🎉 Asset configuration generation completed successfully!'); |
|||
console.log(''); |
|||
console.log('📋 Next steps:'); |
|||
console.log(' 1. Review the generated configuration'); |
|||
console.log(' 2. Commit the configuration files'); |
|||
console.log(' 3. Run "npm run assets:validate" to verify'); |
|||
console.log(' 4. Use "npm run build:native" for builds'); |
|||
|
|||
} catch (error) { |
|||
console.error('❌ Configuration generation failed:', error instanceof Error ? error.message : String(error)); |
|||
process.exit(1); |
|||
} |
|||
} |
|||
|
|||
// Run if called directly
|
|||
if (import.meta.url === `file://${process.argv[1]}`) { |
|||
main(); |
|||
} |
|||
|
|||
export { generateAssetConfig, sortObjectKeys, validateSourceFiles, AssetConfig }; |
@ -0,0 +1,239 @@ |
|||
#!/usr/bin/env tsx |
|||
|
|||
/** |
|||
* TimeSafari Asset Configuration Validator |
|||
* Validates capacitor-assets configuration against schema and source files |
|||
* Author: Matthew Raymer |
|||
* |
|||
* Usage: tsx scripts/assets-validator.ts [config-path] |
|||
*/ |
|||
|
|||
import fs from 'fs'; |
|||
import path from 'path'; |
|||
import { fileURLToPath } from 'url'; |
|||
|
|||
const __filename = fileURLToPath(import.meta.url); |
|||
const __dirname = path.dirname(__filename); |
|||
const PROJECT_ROOT = path.dirname(__dirname); |
|||
|
|||
// TypeScript interfaces for validation
|
|||
interface ValidationError { |
|||
message: string; |
|||
} |
|||
|
|||
interface AssetConfig { |
|||
icon?: { |
|||
source?: string; |
|||
android?: { |
|||
adaptive?: { |
|||
foreground?: string; |
|||
background?: string; |
|||
monochrome?: string; |
|||
}; |
|||
}; |
|||
}; |
|||
splash?: { |
|||
source?: string; |
|||
darkSource?: string; |
|||
}; |
|||
} |
|||
|
|||
/** |
|||
* Load and parse JSON file |
|||
* @param filePath - Path to JSON file |
|||
* @returns Parsed JSON object |
|||
*/ |
|||
function loadJsonFile(filePath: string): AssetConfig { |
|||
try { |
|||
const content = fs.readFileSync(filePath, 'utf8'); |
|||
return JSON.parse(content); |
|||
} catch (error) { |
|||
throw new Error(`Failed to load ${filePath}: ${error instanceof Error ? error.message : String(error)}`); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Validate configuration against schema |
|||
* @param config - Configuration object to validate |
|||
* @returns Array of validation errors |
|||
*/ |
|||
function validateAgainstSchema(config: AssetConfig): ValidationError[] { |
|||
const errors: ValidationError[] = []; |
|||
|
|||
// Basic structure validation
|
|||
if (!config.icon || !config.splash) { |
|||
errors.push({ message: 'Configuration must contain both "icon" and "splash" sections' }); |
|||
} |
|||
|
|||
// Icon validation
|
|||
if (config.icon) { |
|||
if (!config.icon.source) { |
|||
errors.push({ message: 'Icon section must contain "source" field' }); |
|||
} else if (!/^resources\/.*\.(png|svg)$/.test(config.icon.source)) { |
|||
errors.push({ message: 'Icon source must be a PNG or SVG file in resources/ directory' }); |
|||
} |
|||
|
|||
// Android adaptive icon validation
|
|||
if (config.icon.android?.adaptive) { |
|||
const adaptive = config.icon.android.adaptive; |
|||
if (!adaptive.foreground || !adaptive.background) { |
|||
errors.push({ message: 'Android adaptive icon must have both foreground and background' }); |
|||
} |
|||
if (adaptive.foreground && !/^resources\/.*\.(png|svg)$/.test(adaptive.foreground)) { |
|||
errors.push({ message: 'Android adaptive foreground must be a PNG or SVG file in resources/ directory' }); |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Splash validation
|
|||
if (config.splash) { |
|||
if (!config.splash.source) { |
|||
errors.push({ message: 'Splash section must contain "source" field' }); |
|||
} else if (!/^resources\/.*\.(png|svg)$/.test(config.splash.source)) { |
|||
errors.push({ message: 'Splash source must be a PNG or SVG file in resources/ directory' }); |
|||
} |
|||
|
|||
if (config.splash.darkSource && !/^resources\/.*\.(png|svg)$/.test(config.splash.darkSource)) { |
|||
errors.push({ message: 'Dark splash source must be a PNG or SVG file in resources/ directory' }); |
|||
} |
|||
} |
|||
|
|||
return errors; |
|||
} |
|||
|
|||
/** |
|||
* Validate that source files exist |
|||
* @param config - Configuration object |
|||
* @returns Array of missing file errors |
|||
*/ |
|||
function validateSourceFiles(config: AssetConfig): ValidationError[] { |
|||
const errors: ValidationError[] = []; |
|||
const requiredFiles = new Set<string>(); |
|||
|
|||
// Collect all required source files
|
|||
if (config.icon?.source) requiredFiles.add(config.icon.source); |
|||
if (config.icon?.android?.adaptive?.foreground) requiredFiles.add(config.icon.android.adaptive.foreground); |
|||
if (config.icon?.android?.adaptive?.monochrome) requiredFiles.add(config.icon.android.adaptive.monochrome); |
|||
if (config.splash?.source) requiredFiles.add(config.splash.source); |
|||
if (config.splash?.darkSource) requiredFiles.add(config.splash.darkSource); |
|||
|
|||
// Check each file exists
|
|||
requiredFiles.forEach(file => { |
|||
const filePath = path.join(PROJECT_ROOT, file); |
|||
if (!fs.existsSync(filePath)) { |
|||
errors.push({ message: `Source file not found: ${file}` }); |
|||
} |
|||
}); |
|||
|
|||
return errors; |
|||
} |
|||
|
|||
/** |
|||
* Validate target directories are writable |
|||
* @param config - Configuration object |
|||
* @returns Array of directory validation errors |
|||
*/ |
|||
function validateTargetDirectories(config: AssetConfig): ValidationError[] { |
|||
const errors: ValidationError[] = []; |
|||
const targetDirs = new Set<string>(); |
|||
|
|||
// Collect all target directories
|
|||
if (config.icon?.android?.target) targetDirs.add(config.icon.android.target); |
|||
if (config.icon?.ios?.target) targetDirs.add(config.icon.ios.target); |
|||
if (config.icon?.web?.target) targetDirs.add(config.icon.web.target); |
|||
if (config.splash?.android?.target) targetDirs.add(config.splash.android.target); |
|||
if (config.splash?.ios?.target) targetDirs.add(config.splash.ios.target); |
|||
|
|||
// Check each target directory
|
|||
targetDirs.forEach(dir => { |
|||
const dirPath = path.join(PROJECT_ROOT, dir); |
|||
const parentDir = path.dirname(dirPath); |
|||
|
|||
if (!fs.existsSync(parentDir)) { |
|||
errors.push({ message: `Parent directory does not exist: ${parentDir}` }); |
|||
} else if (!fs.statSync(parentDir).isDirectory()) { |
|||
errors.push({ message: `Parent path is not a directory: ${parentDir}` }); |
|||
} |
|||
}); |
|||
|
|||
return errors; |
|||
} |
|||
|
|||
/** |
|||
* Main validation function |
|||
* @param configPath - Path to configuration file |
|||
* @returns True if validation passes |
|||
*/ |
|||
function validateConfiguration(configPath: string): boolean { |
|||
console.log('🔍 Validating TimeSafari asset configuration...'); |
|||
console.log(`📁 Config file: ${configPath}`); |
|||
console.log(`📁 Project root: ${PROJECT_ROOT}`); |
|||
|
|||
try { |
|||
// Load configuration
|
|||
const config = loadJsonFile(configPath); |
|||
console.log('✅ Configuration file loaded successfully'); |
|||
|
|||
// Load schema
|
|||
const schemaPath = path.join(PROJECT_ROOT, 'config', 'assets', 'schema.json'); |
|||
const schema = loadJsonFile(schemaPath); |
|||
console.log('✅ Schema file loaded successfully'); |
|||
|
|||
// Perform validations
|
|||
const schemaErrors = validateAgainstSchema(config); |
|||
const fileErrors = validateSourceFiles(config); |
|||
const dirErrors = validateTargetDirectories(config); |
|||
|
|||
// Report results
|
|||
const allErrors = [...schemaErrors, ...fileErrors, ...dirErrors]; |
|||
|
|||
if (allErrors.length === 0) { |
|||
console.log('🎉 All validations passed successfully!'); |
|||
console.log(''); |
|||
console.log('📋 Configuration summary:'); |
|||
console.log(` Icon source: ${config.icon?.source || 'NOT SET'}`); |
|||
console.log(` Splash source: ${config.splash?.source || 'NOT SET'}`); |
|||
console.log(` Dark splash: ${config.splash?.darkSource || 'NOT SET'}`); |
|||
console.log(` Android adaptive: ${config.icon?.android?.adaptive ? 'ENABLED' : 'DISABLED'}`); |
|||
console.log(` iOS LaunchScreen: ${config.splash?.ios?.useStoryBoard ? 'ENABLED' : 'DISABLED'}`); |
|||
return true; |
|||
} else { |
|||
console.error('❌ Validation failed with the following errors:'); |
|||
allErrors.forEach((error, index) => { |
|||
console.error(` ${index + 1}. ${error.message}`); |
|||
}); |
|||
return false; |
|||
} |
|||
|
|||
} catch (error) { |
|||
console.error('❌ Validation failed:', error instanceof Error ? error.message : String(error)); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Main execution function |
|||
*/ |
|||
function main(): void { |
|||
const configPath = process.argv[2] || path.join(PROJECT_ROOT, 'capacitor-assets.config.json'); |
|||
|
|||
if (!fs.existsSync(configPath)) { |
|||
console.error(`❌ Configuration file not found: ${configPath}`); |
|||
console.log(''); |
|||
console.log('💡 Available options:'); |
|||
console.log(' - Use default: capacitor-assets.config.json'); |
|||
console.log(' - Specify path: tsx scripts/assets-validator.ts path/to/config.json'); |
|||
console.log(' - Generate config: npm run assets:config'); |
|||
process.exit(1); |
|||
} |
|||
|
|||
const success = validateConfiguration(configPath); |
|||
process.exit(success ? 0 : 1); |
|||
} |
|||
|
|||
// Run if called directly
|
|||
if (import.meta.url === `file://${process.argv[1]}`) { |
|||
main(); |
|||
} |
|||
|
|||
export { validateConfiguration, validateAgainstSchema, validateSourceFiles, validateTargetDirectories, AssetConfig }; |
@ -1,142 +0,0 @@ |
|||
#!/bin/bash |
|||
|
|||
# TimeSafari Android Resource Check Script |
|||
# Checks for missing Android resources and automatically fixes common issues |
|||
# Author: Matthew Raymer |
|||
|
|||
set -e |
|||
|
|||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
|||
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" |
|||
ANDROID_RES_DIR="$PROJECT_ROOT/android/app/src/main/res" |
|||
ASSETS_DIR="$PROJECT_ROOT/assets" |
|||
|
|||
echo "=== TimeSafari Android Resource Check ===" |
|||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] Checking Android resources" |
|||
|
|||
# Function to check if a file exists |
|||
check_file() { |
|||
local file="$1" |
|||
local description="$2" |
|||
if [ -f "$file" ]; then |
|||
echo "[✓] $description: $file" |
|||
return 0 |
|||
else |
|||
echo "[✗] $description: $file (MISSING)" |
|||
return 1 |
|||
fi |
|||
} |
|||
|
|||
# Function to check if a directory exists and has files |
|||
check_directory() { |
|||
local dir="$1" |
|||
local description="$2" |
|||
if [ -d "$dir" ] && [ "$(ls -A "$dir" 2>/dev/null)" ]; then |
|||
echo "[✓] $description: $dir" |
|||
return 0 |
|||
else |
|||
echo "[✗] $description: $dir (MISSING OR EMPTY)" |
|||
return 1 |
|||
fi |
|||
} |
|||
|
|||
# Track issues |
|||
issues_found=0 |
|||
fixes_applied=0 |
|||
|
|||
echo "[INFO] Checking splash screen resources..." |
|||
# Check splash screen resources |
|||
if ! check_file "$ANDROID_RES_DIR/drawable/splash.png" "Splash screen (light)"; then |
|||
if [ -f "$ASSETS_DIR/splash.png" ]; then |
|||
echo "[FIX] Copying splash.png to Android resources..." |
|||
cp "$ASSETS_DIR/splash.png" "$ANDROID_RES_DIR/drawable/splash.png" |
|||
fixes_applied=$((fixes_applied + 1)) |
|||
else |
|||
issues_found=$((issues_found + 1)) |
|||
fi |
|||
fi |
|||
|
|||
if ! check_file "$ANDROID_RES_DIR/drawable/splash_dark.png" "Splash screen (dark)"; then |
|||
if [ -f "$ASSETS_DIR/splash_dark.png" ]; then |
|||
echo "[FIX] Copying splash_dark.png to Android resources..." |
|||
cp "$ASSETS_DIR/splash_dark.png" "$ANDROID_RES_DIR/drawable/splash_dark.png" |
|||
fixes_applied=$((fixes_applied + 1)) |
|||
else |
|||
issues_found=$((issues_found + 1)) |
|||
fi |
|||
fi |
|||
|
|||
echo "[INFO] Checking launcher icon resources..." |
|||
# Check launcher icon resources |
|||
required_icons=( |
|||
"mipmap-mdpi/ic_launcher.png" |
|||
"mipmap-hdpi/ic_launcher.png" |
|||
"mipmap-xhdpi/ic_launcher.png" |
|||
"mipmap-xxhdpi/ic_launcher.png" |
|||
"mipmap-xxxhdpi/ic_launcher.png" |
|||
"mipmap-anydpi-v26/ic_launcher.xml" |
|||
"mipmap-anydpi-v26/ic_launcher_round.xml" |
|||
) |
|||
|
|||
missing_icons=0 |
|||
for icon in "${required_icons[@]}"; do |
|||
if ! check_file "$ANDROID_RES_DIR/$icon" "Launcher icon: $icon"; then |
|||
missing_icons=$((missing_icons + 1)) |
|||
fi |
|||
done |
|||
|
|||
if [ $missing_icons -gt 0 ]; then |
|||
echo "[FIX] Missing launcher icons detected. Running icon generation script..." |
|||
if [ -f "$SCRIPT_DIR/generate-android-icons.sh" ]; then |
|||
"$SCRIPT_DIR/generate-android-icons.sh" |
|||
fixes_applied=$((fixes_applied + 1)) |
|||
else |
|||
echo "[ERROR] Icon generation script not found: $SCRIPT_DIR/generate-android-icons.sh" |
|||
issues_found=$((issues_found + 1)) |
|||
fi |
|||
fi |
|||
|
|||
echo "[INFO] Checking Capacitor platform status..." |
|||
# Check if Android platform is properly initialized |
|||
if [ ! -d "$PROJECT_ROOT/android" ]; then |
|||
echo "[ERROR] Android platform directory not found" |
|||
issues_found=$((issues_found + 1)) |
|||
elif [ ! -f "$PROJECT_ROOT/android/app/src/main/AndroidManifest.xml" ]; then |
|||
echo "[ERROR] AndroidManifest.xml not found - platform may be corrupted" |
|||
issues_found=$((issues_found + 1)) |
|||
else |
|||
echo "[✓] Android platform appears to be properly initialized" |
|||
fi |
|||
|
|||
# Check for common build issues |
|||
echo "[INFO] Checking for common build issues..." |
|||
|
|||
# Check for invalid resource names (dashes in filenames) |
|||
invalid_resources=$(find "$ANDROID_RES_DIR" -name "*-*" -type f 2>/dev/null | grep -E '\.(png|jpg|jpeg|gif|xml)$' || true) |
|||
if [ -n "$invalid_resources" ]; then |
|||
echo "[WARNING] Found resources with invalid names (containing dashes):" |
|||
echo "$invalid_resources" | while read -r file; do |
|||
echo " - $file" |
|||
done |
|||
echo "[INFO] Android resource names must contain only lowercase a-z, 0-9, or underscore" |
|||
issues_found=$((issues_found + 1)) |
|||
fi |
|||
|
|||
# Summary |
|||
echo "" |
|||
echo "=== Resource Check Summary ===" |
|||
if [ $issues_found -eq 0 ] && [ $fixes_applied -eq 0 ]; then |
|||
echo "[SUCCESS] All Android resources are present and valid" |
|||
exit 0 |
|||
elif [ $fixes_applied -gt 0 ]; then |
|||
echo "[SUCCESS] Fixed $fixes_applied resource issues automatically" |
|||
if [ $issues_found -gt 0 ]; then |
|||
echo "[WARNING] $issues_found issues remain that require manual attention" |
|||
exit 1 |
|||
else |
|||
exit 0 |
|||
fi |
|||
else |
|||
echo "[ERROR] Found $issues_found resource issues that require manual attention" |
|||
exit 1 |
|||
fi |
@ -1,96 +0,0 @@ |
|||
#!/bin/bash |
|||
|
|||
# TimeSafari Android Icon Generation Script |
|||
# Generates all required Android launcher icon sizes from assets/icon.png |
|||
# Author: Matthew Raymer |
|||
|
|||
set -e |
|||
|
|||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
|||
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" |
|||
ASSETS_DIR="$PROJECT_ROOT/assets" |
|||
ANDROID_RES_DIR="$PROJECT_ROOT/android/app/src/main/res" |
|||
|
|||
echo "=== TimeSafari Android Icon Generation ===" |
|||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] Starting Android icon generation" |
|||
|
|||
# Check if source icon exists |
|||
if [ ! -f "$ASSETS_DIR/icon.png" ]; then |
|||
echo "[ERROR] Source icon not found: $ASSETS_DIR/icon.png" |
|||
exit 1 |
|||
fi |
|||
|
|||
# Check if ImageMagick is available |
|||
if ! command -v convert &> /dev/null; then |
|||
echo "[ERROR] ImageMagick (convert) not found. Please install ImageMagick." |
|||
echo " Arch: sudo pacman -S imagemagick" |
|||
echo " Ubuntu: sudo apt-get install imagemagick" |
|||
echo " macOS: brew install imagemagick" |
|||
exit 1 |
|||
fi |
|||
|
|||
# Create mipmap directories if they don't exist |
|||
mkdir -p "$ANDROID_RES_DIR/mipmap-hdpi" |
|||
mkdir -p "$ANDROID_RES_DIR/mipmap-mdpi" |
|||
mkdir -p "$ANDROID_RES_DIR/mipmap-xhdpi" |
|||
mkdir -p "$ANDROID_RES_DIR/mipmap-xxhdpi" |
|||
mkdir -p "$ANDROID_RES_DIR/mipmap-xxxhdpi" |
|||
|
|||
echo "[INFO] Generating launcher icons..." |
|||
|
|||
# Generate launcher icons for different densities |
|||
# Android launcher icon sizes: mdpi=48, hdpi=72, xhdpi=96, xxhdpi=144, xxxhdpi=192 |
|||
convert "$ASSETS_DIR/icon.png" -resize 48x48 "$ANDROID_RES_DIR/mipmap-mdpi/ic_launcher.png" |
|||
convert "$ASSETS_DIR/icon.png" -resize 72x72 "$ANDROID_RES_DIR/mipmap-hdpi/ic_launcher.png" |
|||
convert "$ASSETS_DIR/icon.png" -resize 96x96 "$ANDROID_RES_DIR/mipmap-xhdpi/ic_launcher.png" |
|||
convert "$ASSETS_DIR/icon.png" -resize 144x144 "$ANDROID_RES_DIR/mipmap-xxhdpi/ic_launcher.png" |
|||
convert "$ASSETS_DIR/icon.png" -resize 192x192 "$ANDROID_RES_DIR/mipmap-xxxhdpi/ic_launcher.png" |
|||
|
|||
# Generate round launcher icons |
|||
convert "$ASSETS_DIR/icon.png" -resize 48x48 "$ANDROID_RES_DIR/mipmap-mdpi/ic_launcher_round.png" |
|||
convert "$ASSETS_DIR/icon.png" -resize 72x72 "$ANDROID_RES_DIR/mipmap-hdpi/ic_launcher_round.png" |
|||
convert "$ASSETS_DIR/icon.png" -resize 96x96 "$ANDROID_RES_DIR/mipmap-xhdpi/ic_launcher_round.png" |
|||
convert "$ASSETS_DIR/icon.png" -resize 144x144 "$ANDROID_RES_DIR/mipmap-xxhdpi/ic_launcher_round.png" |
|||
convert "$ASSETS_DIR/icon.png" -resize 192x192 "$ANDROID_RES_DIR/mipmap-xxxhdpi/ic_launcher_round.png" |
|||
|
|||
# Create background and foreground mipmap files |
|||
# These reference the existing drawable files |
|||
cat > "$ANDROID_RES_DIR/mipmap-anydpi-v26/ic_launcher_background.xml" << 'EOF' |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> |
|||
<background android:drawable="@drawable/ic_launcher_background"/> |
|||
</adaptive-icon> |
|||
EOF |
|||
|
|||
cat > "$ANDROID_RES_DIR/mipmap-anydpi-v26/ic_launcher_foreground.xml" << 'EOF' |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> |
|||
<foreground android:drawable="@drawable/ic_launcher_foreground"/> |
|||
</adaptive-icon> |
|||
EOF |
|||
|
|||
# Update the existing launcher XML files to reference the correct resources |
|||
cat > "$ANDROID_RES_DIR/mipmap-anydpi-v26/ic_launcher.xml" << 'EOF' |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> |
|||
<background android:drawable="@drawable/ic_launcher_background"/> |
|||
<foreground android:drawable="@drawable/ic_launcher_foreground"/> |
|||
</adaptive-icon> |
|||
EOF |
|||
|
|||
cat > "$ANDROID_RES_DIR/mipmap-anydpi-v26/ic_launcher_round.xml" << 'EOF' |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> |
|||
<background android:drawable="@drawable/ic_launcher_background"/> |
|||
<foreground android:drawable="@drawable/ic_launcher_foreground"/> |
|||
</adaptive-icon> |
|||
EOF |
|||
|
|||
echo "[SUCCESS] Generated Android launcher icons:" |
|||
echo " - mipmap-mdpi/ic_launcher.png (48x48)" |
|||
echo " - mipmap-hdpi/ic_launcher.png (72x72)" |
|||
echo " - mipmap-xhdpi/ic_launcher.png (96x96)" |
|||
echo " - mipmap-xxhdpi/ic_launcher.png (144x144)" |
|||
echo " - mipmap-xxxhdpi/ic_launcher.png (192x192)" |
|||
echo " - Updated mipmap-anydpi-v26 XML files" |
|||
echo "[SUCCESS] Android icon generation completed successfully!" |
@ -1,22 +0,0 @@ |
|||
#!/bin/bash |
|||
|
|||
# Create directories if they don't exist |
|||
mkdir -p android/app/src/main/res/mipmap-mdpi |
|||
mkdir -p android/app/src/main/res/mipmap-hdpi |
|||
mkdir -p android/app/src/main/res/mipmap-xhdpi |
|||
mkdir -p android/app/src/main/res/mipmap-xxhdpi |
|||
mkdir -p android/app/src/main/res/mipmap-xxxhdpi |
|||
|
|||
# Generate placeholder icons using ImageMagick |
|||
convert -size 48x48 xc:blue -gravity center -pointsize 20 -fill white -annotate 0 "TS" android/app/src/main/res/mipmap-mdpi/ic_launcher.png |
|||
convert -size 72x72 xc:blue -gravity center -pointsize 30 -fill white -annotate 0 "TS" android/app/src/main/res/mipmap-hdpi/ic_launcher.png |
|||
convert -size 96x96 xc:blue -gravity center -pointsize 40 -fill white -annotate 0 "TS" android/app/src/main/res/mipmap-xhdpi/ic_launcher.png |
|||
convert -size 144x144 xc:blue -gravity center -pointsize 60 -fill white -annotate 0 "TS" android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png |
|||
convert -size 192x192 xc:blue -gravity center -pointsize 80 -fill white -annotate 0 "TS" android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png |
|||
|
|||
# Copy to round versions |
|||
cp android/app/src/main/res/mipmap-mdpi/ic_launcher.png android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png |
|||
cp android/app/src/main/res/mipmap-hdpi/ic_launcher.png android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png |
|||
cp android/app/src/main/res/mipmap-xhdpi/ic_launcher.png android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png |
|||
cp android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png |
|||
cp android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png |
@ -1,67 +0,0 @@ |
|||
#!/bin/bash |
|||
|
|||
# TimeSafari Generated Assets Purge Script |
|||
# Removes generated Android assets and resources from git history |
|||
# Author: Matthew Raymer |
|||
|
|||
set -e |
|||
|
|||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
|||
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" |
|||
|
|||
echo "=== TimeSafari Generated Assets Purge ===" |
|||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] Starting git history cleanup" |
|||
|
|||
# Check if we're in a git repository |
|||
if [ ! -d ".git" ]; then |
|||
echo "[ERROR] Not in a git repository. Please run this script from the project root." |
|||
exit 1 |
|||
fi |
|||
|
|||
# Check if we have uncommitted changes |
|||
if [ -n "$(git status --porcelain)" ]; then |
|||
echo "[ERROR] You have uncommitted changes. Please commit or stash them first." |
|||
echo "Current status:" |
|||
git status --short |
|||
exit 1 |
|||
fi |
|||
|
|||
# Create backup branch |
|||
BACKUP_BRANCH="backup-before-asset-purge-$(date +%Y%m%d-%H%M%S)" |
|||
echo "[INFO] Creating backup branch: $BACKUP_BRANCH" |
|||
git branch "$BACKUP_BRANCH" |
|||
|
|||
echo "[INFO] Starting git filter-branch to remove generated assets..." |
|||
|
|||
# Use git filter-branch to remove the directories from history |
|||
git filter-branch --force --index-filter ' |
|||
# Remove generated Android assets directory |
|||
git rm -rf --cached --ignore-unmatch android/app/src/main/assets/public/ 2>/dev/null || true |
|||
|
|||
# Remove generated Android resources (but keep config files) |
|||
git rm -rf --cached --ignore-unmatch android/app/src/main/res/drawable*/ 2>/dev/null || true |
|||
git rm -rf --cached --ignore-unmatch android/app/src/main/res/mipmap*/ 2>/dev/null || true |
|||
git rm -rf --cached --ignore-unmatch android/app/src/main/res/values/ic_launcher_background.xml 2>/dev/null || true |
|||
|
|||
# Keep configuration files |
|||
git add android/app/src/main/res/values/strings.xml 2>/dev/null || true |
|||
git add android/app/src/main/res/values/styles.xml 2>/dev/null || true |
|||
git add android/app/src/main/res/layout/activity_main.xml 2>/dev/null || true |
|||
git add android/app/src/main/res/xml/config.xml 2>/dev/null || true |
|||
git add android/app/src/main/res/xml/file_paths.xml 2>/dev/null || true |
|||
' --prune-empty --tag-name-filter cat -- --all |
|||
|
|||
echo "[INFO] Cleaning up git filter-branch temporary files..." |
|||
rm -rf .git/refs/original/ |
|||
git reflog expire --expire=now --all |
|||
git gc --prune=now --aggressive |
|||
|
|||
echo "[SUCCESS] Generated assets purged from git history!" |
|||
echo "[INFO] Backup branch created: $BACKUP_BRANCH" |
|||
echo "[INFO] Repository size should be significantly reduced" |
|||
echo "" |
|||
echo "Next steps:" |
|||
echo "1. Test that the repository works correctly" |
|||
echo "2. Force push to remote: git push --force-with-lease origin <branch>" |
|||
echo "3. Inform team members to re-clone or reset their local repositories" |
|||
echo "4. Delete backup branch when confident: git branch -D $BACKUP_BRANCH" |