diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md deleted file mode 100644 index 4b6fcbcc..00000000 --- a/.claude/CLAUDE.md +++ /dev/null @@ -1,5 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -@../AGENTS.md diff --git a/.cursor/mcp.json b/.cursor/mcp.json deleted file mode 100644 index 0127976e..00000000 --- a/.cursor/mcp.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "mcpServers": { - "dart": { - "command": "dart", - "args": ["mcp-server", "--force-roots-fallback"] - }, - "next-devtools": { - "command": "node", - "args": ["web/node_modules/next-devtools-mcp/dist/index.js"] - } - } -} diff --git a/.env.example b/.env.example deleted file mode 100644 index 4d4439a4..00000000 --- a/.env.example +++ /dev/null @@ -1,2 +0,0 @@ -CLIENT_ID= -SECRET= diff --git a/.forgejo/workflows/actions-probe.yml b/.forgejo/workflows/actions-probe.yml deleted file mode 100644 index a9e9414d..00000000 --- a/.forgejo/workflows/actions-probe.yml +++ /dev/null @@ -1,14 +0,0 @@ -# Temporary probe: does this repo schedule Actions at all? -name: Actions probe - -on: - push: - branches: [main] - workflow_dispatch: - -jobs: - probe: - runs-on: desktop-release - steps: - - name: ping - run: echo "frosty actions probe ok" diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml deleted file mode 100644 index 8e8ac8fd..00000000 --- a/.forgejo/workflows/release.yml +++ /dev/null @@ -1,304 +0,0 @@ -# Builds a signed Frosty release APK and publishes a Forgejo release for Obtainium. -# Pattern: finamp-fork / jellyfin-android-fork on host runner `desktop-release`. -# -# Required repository secrets: -# RELEASE_KEYSTORE_BASE64, STORE_PASSWORD, KEY_ALIAS, KEY_PASSWORD -# CLIENT_ID, SECRET (Twitch Helix app credentials — --dart-define) -# -# Obtainium: -# Source: Forgejo -# URL: https://forgejo.fsociety00.cc/api/v1/repos/copilot/frosty/releases -# APK filter: frosty-arm64-v8a-v.*\.apk -# Trim version: v?([0-9]+\.[0-9]+\.[0-9]+) -# Match group: 1 -# Private repo → Forgejo API token in Obtainium. - -name: Release APK - -on: - push: - tags: - - 'v*' - workflow_dispatch: - -permissions: - contents: write - -concurrency: - group: frosty-release - cancel-in-progress: true - -jobs: - build-and-release: - name: Build & publish release - runs-on: desktop-release - - steps: - - name: Clone repository - shell: bash - env: - FORGEJO_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -euo pipefail - rm -rf .git - git init . - origin_url="${GITHUB_SERVER_URL#https://}" - origin_url="${origin_url#http://}" - git remote add origin "https://copilot:${FORGEJO_TOKEN}@${origin_url}/${GITHUB_REPOSITORY}.git" - git -c http.sslVerify=false fetch --depth=1 origin "${GITHUB_SHA}" - git checkout --force FETCH_HEAD - test -f pubspec.yaml - test -f android/app/build.gradle.kts - echo "Checked out ${GITHUB_REPOSITORY}@${GITHUB_SHA}" - - - name: Set up JDK - shell: bash - run: | - set -euo pipefail - pkg_install() { - if [ "$(id -u)" -eq 0 ]; then "$@" - elif command -v sudo >/dev/null 2>&1; then sudo "$@" - else "$@"; fi - } - if ! command -v java >/dev/null 2>&1; then - if command -v dnf >/dev/null 2>&1; then pkg_install dnf install -y java-21-openjdk-devel - elif command -v apt-get >/dev/null 2>&1; then - pkg_install apt-get update - pkg_install apt-get install -y openjdk-21-jdk - elif command -v apk >/dev/null 2>&1; then pkg_install apk add --no-cache openjdk21 - fi - fi - java -version - if command -v javac >/dev/null 2>&1; then - java_home="$(dirname "$(dirname "$(readlink -f "$(command -v javac)")")")" - echo "JAVA_HOME=$java_home" >> "$GITHUB_ENV" - fi - - - name: Set up Android SDK - shell: bash - run: | - set -euo pipefail - sdk_candidates=( - "${ANDROID_HOME:-}" - "${ANDROID_SDK_ROOT:-}" - "/home/yuna/Android/Sdk" - "${HOME}/Android/Sdk" - "/opt/android-sdk-linux" - "/root/Android/Sdk" - ) - sdk_dir="" - for candidate in "${sdk_candidates[@]}"; do - if [ -n "$candidate" ] && [ -d "$candidate/platforms" ] && [ -d "$candidate/build-tools" ]; then - sdk_dir="$candidate" - break - fi - done - if [ -z "$sdk_dir" ]; then - echo "Android SDK not found on desktop-release host." - exit 1 - fi - echo "ANDROID_HOME=$sdk_dir" >> "$GITHUB_ENV" - echo "ANDROID_SDK_ROOT=$sdk_dir" >> "$GITHUB_ENV" - printf 'sdk.dir=%s\n' "$sdk_dir" > android/local.properties - echo "Using Android SDK: $sdk_dir" - - - name: Set up Flutter - shell: bash - run: | - set -euo pipefail - pkg_install() { - if [ "$(id -u)" -eq 0 ]; then "$@" - elif command -v sudo >/dev/null 2>&1; then sudo "$@" - else "$@"; fi - } - if ! command -v curl >/dev/null 2>&1; then - if command -v apk >/dev/null 2>&1; then pkg_install apk add --no-cache curl bash git unzip xz - elif command -v dnf >/dev/null 2>&1; then pkg_install dnf install -y curl git unzip - elif command -v apt-get >/dev/null 2>&1; then - pkg_install apt-get update - pkg_install apt-get install -y curl git unzip - fi - fi - # Prefer pinned SDK already on the desktop-release host. - flutter_version="$(awk '/^[[:space:]]*flutter:/{print $2; exit}' pubspec.yaml)" - flutter_version="${flutter_version:-3.47.0}" - candidates=( - "/home/yuna/.local/flutter-${flutter_version}" - "${HOME}/.cache/frosty-ci-flutter-${flutter_version}" - "/home/yuna/.local/flutter" - ) - flutter_home="" - for c in "${candidates[@]}"; do - if [ -x "${c}/bin/flutter" ]; then - flutter_home="$c" - break - fi - done - if [ -z "$flutter_home" ]; then - flutter_home="${HOME}/.cache/frosty-ci-flutter-${flutter_version}" - rm -rf "${flutter_home}" - git clone --depth 1 --branch "${flutter_version}" https://github.com/flutter/flutter.git "${flutter_home}" - fi - echo "${flutter_home}/bin" >> "$GITHUB_PATH" - echo "PATH=${flutter_home}/bin:${PATH}" >> "$GITHUB_ENV" - export PATH="${flutter_home}/bin:${PATH}" - flutter --version - flutter precache --android - - - name: Decode keystore - shell: bash - env: - RELEASE_KEYSTORE_BASE64: ${{ secrets.RELEASE_KEYSTORE_BASE64 }} - STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }} - KEY_ALIAS: ${{ secrets.KEY_ALIAS }} - KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} - run: | - set -euo pipefail - if [ -z "${RELEASE_KEYSTORE_BASE64:-}" ] || [ -z "${STORE_PASSWORD:-}" ] || [ -z "${KEY_ALIAS:-}" ] || [ -z "${KEY_PASSWORD:-}" ]; then - echo "Missing signing secrets: RELEASE_KEYSTORE_BASE64, STORE_PASSWORD, KEY_ALIAS, KEY_PASSWORD" - exit 1 - fi - printf '%s' "$RELEASE_KEYSTORE_BASE64" | base64 -d > android/upload-keystore.jks - test -s android/upload-keystore.jks - { - echo "storePassword=${STORE_PASSWORD}" - echo "keyPassword=${KEY_PASSWORD}" - echo "keyAlias=${KEY_ALIAS}" - echo "storeFile=${GITHUB_WORKSPACE}/android/upload-keystore.jks" - } > android/key.properties - echo "Keystore ready ($(wc -c < android/upload-keystore.jks) bytes)." - - - name: Resolve version metadata - id: version - shell: bash - run: | - set -euo pipefail - raw="$(grep -E '^version:' pubspec.yaml | head -1 | awk '{print $2}')" - VERSION_NAME="${raw%%+*}" - VERSION_CODE="${raw##*+}" - [ "$VERSION_CODE" != "$raw" ] || VERSION_CODE=1 - SHORT_SHA="${GITHUB_SHA::7}" - if [[ "${GITHUB_REF}" == refs/tags/* ]]; then - TAG="${GITHUB_REF#refs/tags/}" - else - TAG="build-${VERSION_NAME}-${VERSION_CODE}-${SHORT_SHA}" - fi - echo "version_name=${VERSION_NAME}" >> "$GITHUB_OUTPUT" - echo "version_code=${VERSION_CODE}" >> "$GITHUB_OUTPUT" - echo "short_sha=${SHORT_SHA}" >> "$GITHUB_OUTPUT" - echo "tag=${TAG}" >> "$GITHUB_OUTPUT" - echo "tag=${TAG} version=${VERSION_NAME}+${VERSION_CODE}" - - - name: Build signed release APK - id: build - shell: bash - env: - CLIENT_ID: ${{ secrets.CLIENT_ID }} - SECRET: ${{ secrets.SECRET }} - VERSION_NAME: ${{ steps.version.outputs.version_name }} - VERSION_CODE: ${{ steps.version.outputs.version_code }} - run: | - set -euo pipefail - if [ -z "${CLIENT_ID:-}" ] || [ -z "${SECRET:-}" ]; then - echo "Missing Twitch secrets CLIENT_ID / SECRET (Helix OAuth app)." - exit 1 - fi - flutter pub get - flutter build apk --release \ - --build-name="${VERSION_NAME}" \ - --build-number="${VERSION_CODE}" \ - --dart-define="CLIENT_ID=${CLIENT_ID}" \ - --dart-define="SECRET=${SECRET}" - mkdir -p dist - cp build/app/outputs/flutter-apk/app-release.apk \ - "dist/frosty-v${VERSION_NAME}.apk" - flutter build apk --release --split-per-abi \ - --build-name="${VERSION_NAME}" \ - --build-number="${VERSION_CODE}" \ - --dart-define="CLIENT_ID=${CLIENT_ID}" \ - --dart-define="SECRET=${SECRET}" || true - for abi in arm64-v8a armeabi-v7a x86_64; do - src="build/app/outputs/flutter-apk/app-${abi}-release.apk" - if [ -f "$src" ]; then - cp "$src" "dist/frosty-${abi}-v${VERSION_NAME}.apk" - fi - done - ls -lh dist/ - - - name: Publish Forgejo release - shell: bash - env: - FORGEJO_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TAG: ${{ steps.version.outputs.tag }} - VERSION_NAME: ${{ steps.version.outputs.version_name }} - VERSION_CODE: ${{ steps.version.outputs.version_code }} - SHORT_SHA: ${{ steps.version.outputs.short_sha }} - run: | - set -euo pipefail - pkg_install() { - if [ "$(id -u)" -eq 0 ]; then "$@" - elif command -v sudo >/dev/null 2>&1; then sudo "$@" - else return 1; fi - } - for pkg in curl jq; do - command -v "$pkg" >/dev/null 2>&1 && continue - if command -v apk >/dev/null 2>&1; then pkg_install apk add --no-cache "$pkg" - elif command -v dnf >/dev/null 2>&1; then pkg_install dnf install -y "$pkg" - elif command -v apt-get >/dev/null 2>&1; then - pkg_install apt-get update - pkg_install apt-get install -y "$pkg" - fi - done - - API="${GITHUB_API_URL:-${GITHUB_SERVER_URL}/api/v1}/repos/${GITHUB_REPOSITORY}" - AUTH="Authorization: token ${FORGEJO_TOKEN}" - - BODY_FILE="$(mktemp)" - cat > "${BODY_FILE}" </dev/null - fi - test -n "$release_id" && test "$release_id" != "null" - - for apk in dist/*.apk; do - [ -f "$apk" ] || continue - name="$(basename "$apk")" - echo "Uploading ${name}…" - curl -sS -X POST -H "$AUTH" \ - -H "Content-Type: application/octet-stream" \ - --data-binary @"${apk}" \ - "${API}/releases/${release_id}/assets?name=${name}" \ - | jq -r '.name // .message' - done - echo "Published ${TAG} as release ${release_id}" - rm -f "$BODY_FILE" diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..520fe380 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,208 @@ +# Copilot Instructions for Frosty + +## Project Overview + +Frosty is a cross-platform Flutter app for Twitch that supports third-party emotes (7TV, BTTV, FFZ). The architecture is built around **MobX state management** with strict code generation requirements and a **shared HTTP client pattern** for API efficiency. + +## Critical Development Workflow + +### Essential Commands + +**Dependencies and Development:** + +- `flutter pub get` - Install dependencies +- `flutter run` - Run the app on connected device/emulator +- `flutter build apk` - Build Android APK +- `flutter build ios` - Build iOS app +- `flutter analyze` - Run static analysis and lint checks + +### Code Generation (REQUIRED) + +All MobX stores and JSON models require code generation. **Always run after any MobX or model changes:** + +```bash +flutter packages pub run build_runner build --delete-conflicting-outputs +``` + +**Alternative commands:** + +- `flutter packages pub run build_runner build` - Generate code once +- `flutter packages pub run build_runner watch` - Watch and regenerate code on changes + +Generated `.g.dart` files are excluded from linting but must be committed to source control. + +### Environment Setup + +Twitch API requires client credentials via `--dart-define`: + +```bash +flutter run --dart-define=clientId=YOUR_TWITCH_CLIENT_ID --dart-define=secret=YOUR_TWITCH_CLIENT_SECRET +``` + +## Architecture Patterns + +### State Management (MobX) + +- **Store Pattern**: Each feature has stores in `lib/screens/{feature}/stores/` (not at screen root) +- **Naming**: All stores end with `Store` suffix and have corresponding `.g.dart` files +- **Global Stores**: `AuthStore` and `SettingsStore` in `lib/screens/settings/stores/` +- **Dependency Injection**: Stores are provided via Provider in `main.dart` with shared HTTP client + +### API Service Architecture + +- **Shared HTTP Client**: Single Dio instance configured in `DioClient.createClient()` with: + - Connection pooling and keep-alive headers for efficiency + - Optimized timeouts: 8s connect, 15s receive, 10s send + - Global Twitch User-Agent header + - Simple logging in debug mode (not verbose to avoid log spam) +- **API Services**: `TwitchApi`, `BTTVApi`, `FFZApi`, `SevenTVApi` in `lib/apis/` +- **Authentication Interceptor**: `TwitchAuthInterceptor` automatically injects auth headers: + - Detects Twitch API URLs and adds Authorization + Client-Id headers + - Updates automatically when user authentication changes + - Eliminates manual header passing throughout the codebase +- **Error Handling**: `UnauthorizedInterceptor` catches 401 errors for token refresh +- **Service Pattern**: All API services extend `BaseApiClient` for consistent error handling + +### Project Structure + +**Key Directories:** + +- `lib/screens/` - UI screens organized by feature (home, channel, settings, onboarding) + - Each screen has its own MobX stores in a `stores/` subdirectory +- `lib/models/` - Data models with JSON serialization (uses `.g.dart` files) +- `lib/apis/` - API services for Twitch, BTTV, FFZ, and 7TV +- `lib/widgets/` - Reusable UI components +- `lib/cache_manager.dart` - Custom cache management for images/media +- `lib/utils.dart` - Utility functions and helpers +- `lib/utils/` - Additional utility modules including context extensions + +### Screen Organization + +``` +lib/screens/{feature}/ +├── {feature}.dart # Main UI +├── stores/ # Feature stores directory +│ ├── {feature}_store.dart +│ └── {feature}_store.g.dart +└── subdirectories/ # Feature sub-components +``` + +**Main Application Flow:** + +1. App starts in `main.dart` with Firebase initialization and dependency injection via Provider +2. Authentication handled by `AuthStore` using Twitch OAuth +3. Settings persisted via `SettingsStore` with SharedPreferences +4. API services inject common HTTP client for efficient connection reuse + +### Model Generation + +- **JSON Serialization**: Models use `@JsonSerializable()` with `.g.dart` companions +- **Emote Architecture**: Base `Emote` class with platform-specific factories (`Emote.fromTwitch()`, `Emote.fromBTTV()`) + +## Key Integration Points + +### Firebase Services + +Initialized in `main.dart` with error handling: + +- Crashlytics for uncaught errors +- Performance monitoring +- Analytics + +### Chat System + +- **IRC Connection**: WebSocket to Twitch IRC for real-time chat +- **Emote Loading**: Asynchronous third-party emote fetching (BTTV/FFZ/7TV) +- **Message Parsing**: Custom rendering with emote/badge support +- **Real-time IRC**: WebSocket connection to Twitch IRC with custom `IRCMessage` parsing +- **Third-party Emotes**: Asynchronous loading of BTTV, FFZ, and 7TV assets via dedicated APIs +- **Message Management**: 5000 message limit with 20% batch removal optimization for performance +- **Assets Store**: Separate `ChatAssetsStore` manages emotes, badges, and chat-related data +- **User interaction features**: Blocking, reporting, moderation capabilities + +### Authentication Flow + +- **OAuth**: Twitch OAuth via WebView in `AuthStore` +- **Token Storage**: FlutterSecureStorage for secure token persistence +- **Auto-refresh**: Token validation and refresh logic in `AuthStore` +- **Two-tier Token System**: + - Default app token for unauthenticated requests + - Optional user token stored in Flutter Secure Storage + - Automatic token validation on startup with fallback +- **WebView OAuth Flow**: Custom JavaScript injection in WebView for seamless Twitch login experience without opening external browsers +- **Secure Storage Cleanup**: First-run detection clears secure storage to handle Android/iOS uninstall scenarios where secure storage persists + +## Code Style Enforcement + +Analysis rules in `analysis_options.yaml`: + +- Single quotes: `prefer_single_quotes` +- Package imports: `always_use_package_imports` +- Trailing commas: `require_trailing_commas` +- Final locals: `prefer_final_locals` +- Additional rules: `directives_ordering`, `avoid_void_async`, `always_declare_return_types` + +**Important**: `.g.dart` files are excluded from analysis but must be committed to source control. + +## Common Patterns + +### MobX Reactions + +```dart +late final ReactionDisposer _disposeReaction; +_disposeReaction = reaction( + (_) => authStore.isLoggedIn, + (_) => _selectedIndex = 0, +); +``` + +### Settings Persistence + +Auto-saving pattern in `main.dart`: + +```dart +autorun((_) => prefs.setString('settings', jsonEncode(settingsStore))); +``` + +### Error Handling + +Use `Future.error()` for API failures, not exceptions. + +## MobX Store Implementation + +### Store Pattern + +All stores follow `StoreBase with _$StoreName` pattern: + +- Generate `.g.dart` files with `flutter packages pub run build_runner build` +- Use `@observable`, `@action`, and `@computed` annotations +- Settings store uses `@JsonSerializable()` with automatic persistence via `autorun()` + +### Authentication Headers + +Generated as `@computed` properties that automatically update when tokens change. + +### State Persistence + +Settings automatically saved to SharedPreferences via MobX `autorun()` reaction. + +## Custom Cache Management + +- **Orphaned File Cleanup**: `CustomCacheManager.removeOrphanedCacheFiles()` runs on startup to remove files not in database +- **Optimized Settings**: 30-day stale period, 10,000 max objects for efficient image caching across the app + +## Code Style Guidelines + +- **Prefer modern Flutter/Dart features and type-safety** +- **Use consistent naming, comments, and styles matching the existing codebase** +- **Single quotes, trailing commas, and final locals per lint rules** +- **Package imports over relative imports** +- **Observer widgets only wrap necessary UI components for MobX optimization** + +## Platform Support + +- **iOS/Android**: Native platform features via plugins +- **Custom Cache**: `CustomCacheManager` for media caching +- **PiP Mode**: Custom implementation via git dependency + +When working with this codebase, always consider MobX code generation requirements and the shared HTTP client pattern for API efficiency. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d6c5c96b..d49bfab7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,57 +3,18 @@ on: push: branches: [main] jobs: - # Linux runner: analyze + test once (both are OS-independent) and compile a - # release APK to catch release-only breakage. Debug-signed via the build.gradle - # fallback (ALLOW_DEBUG_SIGNED_RELEASE) — no signing secrets needed. - android: - name: Analyze, test, and build Android - runs-on: ubuntu-latest + test: + name: Check and test on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] steps: - uses: actions/checkout@v6 - # Match the JDK the release workflows build with, so this compile check - # faithfully catches what would break a real release. - - uses: actions/setup-java@v5 - with: - distribution: temurin - java-version: 21 - uses: subosito/flutter-action@v2 with: - flutter-version-file: pubspec.yaml cache: true - run: flutter doctor -v - run: flutter pub get - # Fail if committed generated files (*.g.dart) are stale — CI never runs - # build_runner otherwise, so forgetting to regenerate after editing a MobX - # store or @JsonSerializable model slips past analyze/test/build. - - run: dart run build_runner build --delete-conflicting-outputs - - name: Verify generated files are up to date - run: | - if [ -n "$(git status --porcelain)" ]; then - echo '::error::Generated files are out of date. Run `dart run build_runner build --delete-conflicting-outputs` and commit the result.' - git status --porcelain - git diff - exit 1 - fi - run: flutter analyze - run: flutter test - - run: flutter build apk --release - env: - ALLOW_DEBUG_SIGNED_RELEASE: '1' - # macOS runner: release-compile iOS (no codesign, no secrets) — the only thing - # that must run on macOS. analyze/test already covered on the Linux job. - ios: - name: Build iOS - runs-on: macos-latest - steps: - - uses: actions/checkout@v6 - # Match release workflows and avoid GitHub's lagging default Xcode. - - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: latest-stable - - uses: subosito/flutter-action@v2 - with: - flutter-version-file: pubspec.yaml - cache: true - - run: flutter pub get - - run: flutter build ios --release --no-codesign diff --git a/.github/workflows/gh-release.yml b/.github/workflows/gh-release.yml index a0d23fd7..ee9a2edd 100644 --- a/.github/workflows/gh-release.yml +++ b/.github/workflows/gh-release.yml @@ -32,7 +32,6 @@ jobs: - name: Set up Flutter uses: subosito/flutter-action@v2 with: - flutter-version-file: pubspec.yaml cache: true # Decode and create the upload-keystore.jks file, used for signing the app. @@ -52,54 +51,11 @@ jobs: run: | flutter build apk --dart-define CLIENT_ID=$CLIENT_ID --dart-define SECRET=$SECRET mv build/app/outputs/flutter-apk/app-release.apk frosty-${{ github.ref_name }}.apk - - # Extract release notes from assets/release-notes.md for the current version. - - name: Extract release notes - id: release_notes - run: | - VERSION="${{ github.ref_name }}" - RELEASE_NOTES_FILE="assets/release-notes.md" - - # Extract the section for this version using awk - # Starts at "## vX.X.X", skips date line, stops at next "---" or "## v" header - NOTES=$(awk -v version="$VERSION" ' - BEGIN { found=0; output="" } - $0 ~ "^## " version "$" { found=1; next } - found && /^[A-Z][a-z]+ [0-9]+, [0-9]+$/ { next } - found && /^---$/ { exit } - found && /^## v[0-9]+\.[0-9]+\.[0-9]+$/ { exit } - found { output = output $0 "\n" } - END { print output } - ' "$RELEASE_NOTES_FILE") - - # Clean up "on GitHub" suffix (redundant since we're on GitHub) - NOTES=$(echo "$NOTES" | sed 's/ on GitHub)/)/') - - # Trim leading/trailing whitespace - NOTES=$(echo "$NOTES" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') - - if [ -n "$NOTES" ]; then - # Get the previous tag for the Full Changelog link - PREV_TAG=$(git describe --tags --abbrev=0 "$VERSION^" 2>/dev/null || echo "") - - echo "found=true" >> $GITHUB_OUTPUT - echo "body<> $GITHUB_OUTPUT - echo "$NOTES" >> $GITHUB_OUTPUT - if [ -n "$PREV_TAG" ]; then - echo "" >> $GITHUB_OUTPUT - echo "**Full Changelog**: https://github.com/${{ github.repository }}/compare/${PREV_TAG}...${VERSION}" >> $GITHUB_OUTPUT - fi - echo "EOF" >> $GITHUB_OUTPUT - else - echo "found=false" >> $GITHUB_OUTPUT - echo "::warning::No release notes found for $VERSION in $RELEASE_NOTES_FILE" - fi - + # Create a draft release on GitHub. - name: Release - uses: softprops/action-gh-release@v3 + uses: softprops/action-gh-release@v2 with: draft: true - generate_release_notes: ${{ steps.release_notes.outputs.found != 'true' }} - body: ${{ steps.release_notes.outputs.found == 'true' && steps.release_notes.outputs.body || '' }} + generate_release_notes: true files: frosty-${{ github.ref_name }}.apk diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 1c3e9897..5a454846 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -8,36 +8,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - # Match the JDK the release workflows build with, so the release-mode APK - # check below faithfully catches what would break a real release. - - uses: actions/setup-java@v5 - with: - distribution: temurin - java-version: 21 - uses: subosito/flutter-action@v2 with: - flutter-version-file: pubspec.yaml cache: true - run: flutter doctor -v - run: flutter pub get - # Fail if committed generated files (*.g.dart) are stale — CI never runs - # build_runner otherwise, so forgetting to regenerate after editing a MobX - # store or @JsonSerializable model slips past analyze/test/build. - - run: dart run build_runner build --delete-conflicting-outputs - - name: Verify generated files are up to date - run: | - if [ -n "$(git status --porcelain)" ]; then - echo '::error::Generated files are out of date. Run `dart run build_runner build --delete-conflicting-outputs` and commit the result.' - git status --porcelain - git diff - exit 1 - fi - run: flutter analyze - run: flutter test - # Release-mode compile check (no signing secrets needed — Android falls back - # to debug signing via ALLOW_DEBUG_SIGNED_RELEASE). Catches release-only - # breakage that debug builds and tests miss (AOT compile, icon tree-shaking, - # plugin/Gradle release incompatibility). - - run: flutter build apk --release - env: - ALLOW_DEBUG_SIGNED_RELEASE: '1' diff --git a/.github/workflows/release-dryrun.yml b/.github/workflows/release-dryrun.yml deleted file mode 100644 index 0b905936..00000000 --- a/.github/workflows/release-dryrun.yml +++ /dev/null @@ -1,126 +0,0 @@ -# Dry-runs the full store release pipeline without publishing, to catch -# credential, signing, dependency, and store-validation breakage before an -# actual release. Mirrors store-release.yml but stops short of publishing: -# Android validates the AAB against Google Play (validate_only); iOS builds a -# fully signed archive via match (readonly) and skips the App Store upload. -name: Release dry-run -on: - # Run on demand before cutting a release. - workflow_dispatch: - # Monthly heartbeat so cert expiry, secret rotation, and dependency drift - # surface on their own between releases. - schedule: - - cron: '0 9 1 * *' # 1st of the month, 09:00 UTC -# Setup environment variables for use in all jobs. -env: - # Gemfile path used for caching dependencies. - BUNDLE_GEMFILE: ${{ github.workspace }}/android/Gemfile - CLIENT_ID: ${{ secrets.CLIENT_ID }} - SECRET: ${{ secrets.SECRET }} - LC_ALL: en_US.UTF-8 - LANG: en_US.UTF-8 -jobs: - android: - name: Dry-run Android release - runs-on: ubuntu-latest - environment: release-android - env: - UPLOAD_KEYSTORE: ${{ secrets.UPLOAD_KEYSTORE }} - STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }} - KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} - KEY_FILE_PATH: android/key.properties - PLAY_STORE_CREDENTIALS: ${{ secrets.PLAY_STORE_CREDENTIALS }} - - steps: - - name: Checkout Frosty repo - uses: actions/checkout@v6 - - # Install Ruby and install/cache dependencies with bundler in order to use fastlane. - - name: Set up Ruby and Bundler - uses: ruby/setup-ruby@v1 - with: - ruby-version: 3.3 - bundler-cache: true - - # Setup Java so that we can build the appbundle. - - name: Set up Java - uses: actions/setup-java@v5 - with: - distribution: temurin - java-version: 21 - - # Setup and cache Flutter to install packages and build. - - name: Set up Flutter - uses: subosito/flutter-action@v2 - with: - flutter-version-file: pubspec.yaml - cache: true - - # Decode and create the upload-keystore.jks file, used for signing the app. - - name: Decode and create upload-keystore.jks - run: echo $UPLOAD_KEYSTORE | base64 --decode > $GITHUB_WORKSPACE/android/upload-keystore.jks - - # Create the key.properties file, used for signing the app. - - name: Create key.properties - run: | - echo storePassword=$STORE_PASSWORD >> $KEY_FILE_PATH - echo keyPassword=$KEY_PASSWORD >> $KEY_FILE_PATH - echo keyAlias=upload >> $KEY_FILE_PATH - echo storeFile=$GITHUB_WORKSPACE/android/upload-keystore.jks >> $KEY_FILE_PATH - - # Build the Android app bundle (AAB) and pass the environment variables. - # - # The version code is overridden because Play rejects one it has already - # seen, and pubspec's code is by definition already published between - # releases — so the dry-run would fail on every scheduled run and never - # reach the validation it exists to perform. Epoch seconds is always - # unused, always increasing, and well under Play's 2100000000 cap. The - # bundle is only ever validated, never published, so the value is thrown - # away. versionName still comes from pubspec. - - name: Build the Android AAB - run: flutter build appbundle --build-number=$(date +%s) --dart-define CLIENT_ID=$CLIENT_ID --dart-define SECRET=$SECRET - - # Validate the AAB and credentials against Google Play without publishing. - - name: Validate release with fastlane (no publish) - run: cd android && bundle exec fastlane deploy dry_run:true - ios: - name: Dry-run iOS release - runs-on: macos-latest - environment: release-ios - env: - APP_STORE_KEY_ID: ${{ secrets.APP_STORE_KEY_ID }} - APP_STORE_ISSUER_ID: ${{ secrets.APP_STORE_ISSUER_ID }} - APP_STORE_CREDENTIALS: ${{ secrets.APP_STORE_CREDENTIALS }} - MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }} - MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} - steps: - - name: Checkout Frosty repo - uses: actions/checkout@v6 - - # Always pick the newest production Xcode on the runner. - - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: latest-stable - - # Install Ruby and install/cache dependencies with bundler in order to use fastlane. - - name: Set up Ruby and Bundler - uses: ruby/setup-ruby@v1 - with: - ruby-version: 3.3 - bundler-cache: true - - # Setup and cache Flutter to install packages and build. - - name: Set up Flutter - uses: subosito/flutter-action@v2 - with: - flutter-version-file: pubspec.yaml - cache: true - - # Setup the iOS project with environment variables without signing (building and signing will happen in fastlane). - - name: Config the iOS build - run: flutter build ios --no-codesign --config-only --dart-define CLIENT_ID=$CLIENT_ID --dart-define SECRET=$SECRET - - # Authenticate ASC API key, resolve signing via match (readonly), and build a - # fully signed archive without uploading to the App Store. - - name: Sign and build with fastlane (no publish) - run: cd ios && bundle exec fastlane release dry_run:true diff --git a/.github/workflows/store-release.yml b/.github/workflows/store-release.yml index 53365a9e..75afb083 100644 --- a/.github/workflows/store-release.yml +++ b/.github/workflows/store-release.yml @@ -35,7 +35,7 @@ jobs: - name: Set up Ruby and Bundler uses: ruby/setup-ruby@v1 with: - ruby-version: 3.3 + ruby-version: 3.2 bundler-cache: true # Setup Java so that we can build the appbundle. @@ -49,7 +49,6 @@ jobs: - name: Set up Flutter uses: subosito/flutter-action@v2 with: - flutter-version-file: pubspec.yaml cache: true # Decode and create the upload-keystore.jks file, used for signing the app. @@ -85,7 +84,7 @@ jobs: - name: Checkout Frosty repo uses: actions/checkout@v6 - # Always pick the newest production Xcode on the runner. + # Always pick the newest production Xcode on the runner - uses: maxim-lobanov/setup-xcode@v1 with: xcode-version: latest-stable @@ -94,14 +93,13 @@ jobs: - name: Set up Ruby and Bundler uses: ruby/setup-ruby@v1 with: - ruby-version: 3.3 + ruby-version: 3.2 bundler-cache: true # Setup and cache Flutter to install packages and build. - name: Set up Flutter uses: subosito/flutter-action@v2 with: - flutter-version-file: pubspec.yaml cache: true # Setup the iOS project with environment variables without signing (building and signing will happen in fastlane). diff --git a/.gitignore b/.gitignore index dfee3eb8..702a84bc 100644 --- a/.gitignore +++ b/.gitignore @@ -21,7 +21,7 @@ migrate_working_dir/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. -# .vscode/ +.vscode/ # Flutter/Dart/Pub related **/doc/api/ @@ -44,8 +44,6 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release -# AGP 9 writes a problems report here even when the Gradle build dir is redirected -/android/build/ # Folder containing random testing files random/ @@ -66,8 +64,6 @@ random/ # potentially sensitive review information **/fastlane/metadata/review_information -# secrets -.env - # ai -.claude/settings.local.json \ No newline at end of file +.claude +.cursor \ No newline at end of file diff --git a/.mcp.json b/.mcp.json deleted file mode 100644 index 0127976e..00000000 --- a/.mcp.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "mcpServers": { - "dart": { - "command": "dart", - "args": ["mcp-server", "--force-roots-fallback"] - }, - "next-devtools": { - "command": "node", - "args": ["web/node_modules/next-devtools-mcp/dist/index.js"] - } - } -} diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index dad4cfce..00000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "name": "frosty", - "request": "launch", - "type": "dart", - "args": ["--dart-define-from-file=.env"] - }, - { - "name": "frosty (profile mode)", - "request": "launch", - "type": "dart", - "flutterMode": "profile", - "args": ["--dart-define-from-file=.env"] - }, - { - "name": "frosty (release mode)", - "request": "launch", - "type": "dart", - "flutterMode": "release", - "args": ["--dart-define-from-file=.env"] - } - ] -} diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 4495eb73..00000000 --- a/AGENTS.md +++ /dev/null @@ -1,46 +0,0 @@ -# AGENTS.md - -Flutter Twitch client for iOS and Android. MobX with `build_runner`. The landing page is `web/` (own `AGENTS.md`). Flutter is pinned in `pubspec.yaml` `environment.flutter` — bump that one field; CI reads it via `flutter-version-file`. - -## Commands - -```bash -flutter pub get -flutter run --dart-define-from-file=.env # local; .env is gitignored -flutter run --dart-define=CLIENT_ID=ID --dart-define=SECRET=SECRET -flutter analyze -flutter test -flutter test test/path/to/file.dart -dart run build_runner build # after MobX / @JsonSerializable changes -``` - -`--delete-conflicting-outputs` is ignored as of build_runner 2.15. Don't add it. - -## Layout - -- `lib/apis/` — API clients extend `BaseApiClient` -- `lib/models/` — data models; `@JsonSerializable` ones commit `.g.dart` -- `lib/screens/{feature}/` — screens; feature stores in co-located `stores/` -- `lib/stores/` — global stores only (`global_assets_store.dart`) -- `test/` mirrors `lib/`; shared fixtures in `test/fixtures/` - -Don't add top-level store files or standalone API classes. Canonical MobX store: `lib/screens/settings/stores/auth_store.dart`. Extract state, reactions, and actions into stores when it keeps widgets on rendering. - -New or changed logic (stores, API clients, parsing, utils) ships with tests; extend existing tests when touching tested code. Pure UI or config may skip. HTTP: `http_mock_adapter` (`DioAdapter`) with full URLs. Otherwise `mocktail`. - -## Gotchas - -- Commit regenerated `.g.dart` files. Never edit them. CI fails the PR if they're stale. -- `main.dart` secure-storage `deleteAll()` looks redundant; it handles Android/iOS storage surviving uninstall. Don't remove it. -- `flutter_secure_storage` stays on v9 (v10 shipped in 5.0.0 and was reverted in 5.0.1 over an Android 14 startup freeze). That also holds `package_info_plus` v9 and `device_info_plus` v12 (win32). Don't jump them in a drive-by upgrade. -- Git-pinned forks in `pubspec.yaml` (`extended_text_field`, `better_native_video_player`, `simple_pip_mode`) carry patches. Don't swap them back to pub.dev. -- `android/gradle.properties`: keep `android.builtInKotlin=false` and `android.newDsl=false` so plugins that still apply KGP build on AGP 9. -- `ios/Runner/AppDelegate.swift` is customized (`CookieExtractorPlugin` + `FlutterImplicitEngineDelegate`). Flutter will not auto-migrate UIScene; don't replace the delegate with the template. -- `android/.../MainActivity.kt` extends `PipCallbackHelperActivityWrapper` and registers `CookieExtractorPlugin` by hand. Don't revert it to `FlutterActivity`. -- Helix OAuth (`Bearer`) and the Twitch web GQL cookie are different tokens. Native playback uses the cookie; don't send the Helix token to GQL/Usher. -- `syncedChatDelay` is runtime-only (`includeFromJson/toJson: false`). Don't persist it with the other settings. -- Don't remove the `ALLOW_DEBUG_SIGNED_RELEASE` branch in `android/app/build.gradle.kts`. Real releases must still fail if `key.properties` is missing. - -## Releases - -When cutting a store or GitHub release, read `docs/release.md` and follow it. That playbook is repo-local, not a global skill. `assets/release-notes.md` keeps about three consecutive versions; drop from the oldest end, never skip. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..883cbe35 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,132 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Development Workflow + +**Plan Mode - In-Depth Planning**: When in plan mode, interview thoroughly before coding: + +- Ask detailed questions about technical implementation, UI/UX concerns, tradeoffs, and edge cases +- Continue asking questions until all requirements and non-obvious decisions are clear +- Don't begin implementation until all important details are resolved + +**Pattern Consistency**: When implementing new code: + +- Search the codebase to find existing usages and implementations +- Prefer following established patterns, styles, and practices for consistency + +## Development Commands + +**Build and Development** + +- `flutter pub get` - Install dependencies +- `flutter run` - Run the app on connected device/emulator +- `flutter build apk` - Build Android APK +- `flutter build ios` - Build iOS app +- `flutter analyze` - Run static analysis and lint checks +- `flutter test` - Run tests (note: no test suite currently exists) + +**Code Generation (Required for MobX stores and JSON serialization)** + +- `flutter packages pub run build_runner build` - Generate code once +- `flutter packages pub run build_runner watch` - Watch and regenerate code on changes +- `flutter packages pub run build_runner build --delete-conflicting-outputs` - Force rebuild all generated files + +**Asset Generation** + +- `flutter pub run flutter_native_splash:create` - Generate native splash screens +- `flutter pub run flutter_launcher_icons:main` - Generate app launcher icons + +**Environment Variables** +Use `--dart-define` to set environment variables (required for Twitch API access): + +```bash +flutter run --dart-define=clientId=YOUR_TWITCH_CLIENT_ID --dart-define=secret=YOUR_TWITCH_CLIENT_SECRET +``` + +## Architecture Overview + +**State Management**: MobX with code generation. All stores end with `Store` and have corresponding `.g.dart` generated files. + +**Key Directories**: + +- `lib/screens/` - UI screens organized by feature (home, channel, settings, onboarding) + - Each screen has its own MobX stores in a `stores/` subdirectory +- `lib/models/` - Data models with JSON serialization (uses .g.dart files) +- `lib/apis/` - API services for Twitch, BTTV, FFZ, and 7TV +- `lib/widgets/` - Reusable UI components +- `lib/utils/` - Utility modules including context extensions + +**Main Application Flow**: + +1. App starts in `main.dart` with Firebase initialization and dependency injection via Provider +2. Authentication handled by `AuthStore` using Twitch OAuth +3. Settings persisted via `SettingsStore` with SharedPreferences and MobX `autorun()` +4. API services share a common Dio HTTP client for efficient connection reuse + +**Global Stores** (provided at app root in `main.dart`): + +- `AuthStore` - Authentication state and token management +- `SettingsStore` - User preferences with automatic persistence + +## HTTP Client Architecture + +**Centralized Dio Setup**: All API services share a single Dio instance configured in `DioClient.createClient()` with connection pooling, keep-alive headers, and optimized timeouts (8s connect, 15s receive, 10s send). + +**Authentication Interceptor**: `TwitchAuthInterceptor` automatically injects auth headers for Twitch API URLs. Updates automatically when user authentication changes. + +**API Service Pattern**: All API services extend `BaseApiClient` for consistent error handling. Services: `TwitchApi`, `BTTVApi`, `FFZApi`, `SevenTVApi`. + +## Authentication & Token Management + +**Two-tier Token System**: + +- Default app token for unauthenticated requests +- Optional user token stored in Flutter Secure Storage +- Automatic token validation on startup with fallback + +**Secure Storage Cleanup**: First-run detection clears secure storage to handle Android/iOS uninstall scenarios where secure storage persists. + +## MobX Store Implementation + +**Store Pattern**: All stores follow `StoreBase with _$StoreName` pattern: + +```dart +class SomeStore = SomeStoreBase with _$SomeStore; + +abstract class SomeStoreBase with Store { + @observable + var someValue = ''; + + @action + void updateValue(String value) => someValue = value; + + @computed + String get derivedValue => someValue.toUpperCase(); +} +``` + +After any changes to MobX stores or `@JsonSerializable()` models, run the build_runner to regenerate `.g.dart` files. + +## Chat System Architecture + +- **Real-time IRC**: WebSocket connection to Twitch IRC with custom `IRCMessage` parsing +- **Third-party Emotes**: Asynchronous loading of BTTV, FFZ, and 7TV assets via dedicated APIs +- **Message Management**: 5000 message limit with 20% batch removal optimization +- **Assets Store**: Separate `ChatAssetsStore` manages emotes, badges, and chat-related data + +## Code Style + +Analysis rules enforced via `analysis_options.yaml`: + +- `prefer_single_quotes` - Use single quotes for strings +- `always_use_package_imports` - Package imports over relative imports +- `require_trailing_commas` - Trailing commas on multi-line constructs +- `prefer_final_locals` - Final for local variables +- `directives_ordering`, `avoid_void_async`, `always_declare_return_types` + +Generated `.g.dart` files are excluded from analysis but must be committed. + +## Commit Convention + +Commits follow conventional format: `refactor:`, `feat:`, `fix:`, `format:`, etc. Keep commits tightly scoped. Include generated `.g.dart` outputs in the same commit as their source changes. diff --git a/DESIGN.md b/DESIGN.md deleted file mode 100644 index 07a0fe02..00000000 --- a/DESIGN.md +++ /dev/null @@ -1,61 +0,0 @@ -# Frosty fork — Twitch ad blocking (DESIGN) - -Personal Forgejo fork of [tommyxchow/frosty](https://github.com/tommyxchow/frosty) that adds **TwitchAdSolutions (vaft)**-style live ad blocking. - -Canonical living plan: session `active-plan.md` for conversation `37b527bf-e540-4753-be1f-39906fe2ba28`. ADR: **ADR-026**. - -## Problem - -Upstream Frosty plays Twitch HLS (native) or `player.twitch.tv` (WebView). Twitch may stitch ad pods into the media playlist (`stitched` / `EXT-X-DATERANGE`). Upstream only **detects** native ad breaks (to avoid bogus latency recovery); it does **not** block ads. Turbo/sub `hide_ads` via linked web session is separate and not a client blocker. - -## Approach (vaft-equivalent) - -Browser vaft hooks `Worker`/`fetch` and rewrites m3u8. Frosty’s native player cannot do that from Dart (`loadUrl` + headers only). - -| Layer | Behavior | -| --- | --- | -| Primary token | GQL `PlaybackAccessToken` with `playerType=popout` (vaft `ForceAccessTokenPlayerType`) when Settings `blockTwitchAds` is on | -| Ad break | Native `adBreakStream` → probe backup types `embed` → `popout` → `autoplay`; accept first Usher multivariant whose sampled media playlist lacks `stitched`; `loadUrl` that URL | -| Ad end | Restore primary popout Usher URL | -| WebView | Best-effort inject of vendored `assets/js/vaft.user.js` (not true document-start on Android) | -| UI | Settings toggle (default on; enabling forces native player); overlay “Blocking ads” | - -## Key code - -- `lib/apis/twitch_ad_solutions.dart` — constants + `TwitchAdFreePlayback` -- `lib/apis/twitch_gql_api.dart` — `playerType` / `platform`, `fetchPlaylistText` -- `lib/screens/channel/video/native_video_store.dart` — load + swap -- `lib/screens/channel/video/video_store.dart` — WebView inject -- `lib/screens/settings/stores/settings_store.dart` — `blockTwitchAds` -- `assets/js/vaft.user.js` — upstream TwitchAdSolutions script - -## Non-goals (v1) - -- Upstream PR -- Full Worker prototype cleaning -- Third-party proxies -- HLS segment stripping inside `better_native_video_player` (unless smoke tests demand it) - -## Tags + viewer sort (v5.2.2+) - -Helix cannot filter by freeform tags or sort ascending. GQL covers both: - -- Search: `SearchFreeformTags` → tap → `TagStreams` (`streams` + `freeformTags`) -- Sort: Top (asc), category (`game.streams`), tag — `VIEWER_COUNT` / `VIEWER_COUNT_ASC` via `StreamSortButton` - -Key code: `lib/apis/twitch_gql_api.dart`, `lib/screens/home/search/`, `lib/widgets/stream_sort_button.dart`, `ListStore` / `ListType.tag`. - -## Stream languages (v5.2.4+) - -Settings → General → Stream languages (multi-select chips). Default `de`. Applied to Top, category, and tag lists. - -- Helix `language=` when one language; multi-language / tags via Helix over-fetch + client filter -- Viewer sort is persisted (`streamViewerSort`) -- **Tag lists (asc + desc):** GQL `streams` with `freeformTags` + `sort` + cursor. Page-2+ uses Client-Integrity (same mint/retry as follow via `UserStore.runWithGqlIntegrity`). Helix + client tag filter only if integrity fails / no web session. -- **Follow / integrity:** Dio `POST /integrity` is almost always `is_bad_bot`. Follow retries via `IntegrityWebViewDialog` (capture `Client-Integrity` + `X-Device-Id` from Twitch's own WebView GQL traffic), then `UserStore.applyWebIntegrity`. -- **Top / category most viewers (desc):** Helix directory order (paginates) -- **Top / category fewest (asc):** GQL `VIEWER_COUNT_ASC` with the same integrity pagination; Helix fill + client re-sort as fallback - -## Build - -Flutter **3.47.0** (pinned in `pubspec.yaml`). Local SDK: `/home/yuna/.local/flutter-3.47.0`. diff --git a/README.md b/README.md index 834bbc44..6741da8d 100644 --- a/README.md +++ b/README.md @@ -85,17 +85,11 @@ For a more detailed overview, visit [frostyapp.io](https://www.frostyapp.io/). 3. Go to the [Twitch dev console](https://dev.twitch.tv/login) and register a new application to retrieve a **Client ID** and **Client Secret**. -4. Copy `.env.example` to `.env` and fill in your credentials: - - ```bash - cp .env.example .env - ``` +4. Use [`--dart-define`](https://dartcode.org/docs/using-dart-define-in-flutter/) to set the `clientId` and `secret` environment variables with your **Client ID** and **Client Secret**. 5. Run `flutter pub get` to fetch all the dependencies. -6. Choose an emulator or device and run the app! The included VS Code launch configurations will automatically read your `.env` file. - -7. (Optional) Run `dart run scripts/get_twitch_token.dart` to quickly get a user auth token. Long-press the **Anonymous** account tile in the app to log in. +6. Choose an emulator or device and run the app! > [!IMPORTANT] > Frosty uses [MobX](https://mobx.netlify.app/) for state management. Please refer to the documentation about code generation, otherwise your changes within MobX stores may not be applied. diff --git a/analysis_options.yaml b/analysis_options.yaml index 3b60af70..de45531d 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -16,9 +16,6 @@ analyzer: - ios/** - android/** - web/** - - windows/** - - macos/** - - linux/** linter: # The lint rules applied to this project can be customized in the diff --git a/android/Gemfile.lock b/android/Gemfile.lock index 41b69f5c..d27ecafb 100644 --- a/android/Gemfile.lock +++ b/android/Gemfile.lock @@ -3,13 +3,13 @@ GEM specs: CFPropertyList (3.0.8) abbrev (0.1.2) - addressable (2.9.0) + addressable (2.8.8) public_suffix (>= 2.0.2, < 8.0) artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.4.0) - aws-partitions (1.1260.0) - aws-sdk-core (3.252.0) + aws-partitions (1.1200.0) + aws-sdk-core (3.240.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -17,19 +17,18 @@ GEM bigdecimal jmespath (~> 1, >= 1.6.1) logger - aws-sdk-kms (1.129.0) - aws-sdk-core (~> 3, >= 3.248.0) + aws-sdk-kms (1.118.0) + aws-sdk-core (~> 3, >= 3.239.1) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.225.1) - aws-sdk-core (~> 3, >= 3.248.0) + aws-sdk-s3 (1.209.0) + aws-sdk-core (~> 3, >= 3.234.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) aws-sigv4 (1.12.1) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) - base64 (0.3.0) - benchmark (0.5.0) - bigdecimal (4.1.2) + base64 (0.2.0) + bigdecimal (4.0.1) claide (1.1.0) colored (1.2) colored2 (3.1.2) @@ -43,7 +42,7 @@ GEM dotenv (2.8.1) emoji_regex (3.2.3) excon (0.112.0) - faraday (1.10.5) + faraday (1.10.4) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) faraday-excon (~> 1.1) @@ -68,20 +67,19 @@ GEM faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) faraday-rack (1.0.0) - faraday-retry (1.0.4) + faraday-retry (1.0.3) faraday_middleware (1.2.1) faraday (~> 1.0) - fastimage (2.4.1) - fastlane (2.234.0) - CFPropertyList (>= 2.3, < 5.0.0) - abbrev (~> 0.1) + fastimage (2.4.0) + fastlane (2.230.0) + CFPropertyList (>= 2.3, < 4.0.0) + abbrev (~> 0.1.2) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) - aws-sdk-s3 (~> 1.197) + aws-sdk-s3 (~> 1.0) babosa (>= 1.0.3, < 2.0.0) - base64 (~> 0.2) - benchmark (>= 0.1.0) - bundler (>= 1.17.3, < 5.0.0) + base64 (~> 0.2.0) + bundler (>= 1.12.0, < 3.0.0) colored (~> 1.2) commander (~> 4.6) csv (~> 3.3) @@ -92,11 +90,11 @@ GEM faraday-cookie_jar (~> 0.0.6) faraday_middleware (~> 1.0) fastimage (>= 2.1.0, < 3.0.0) - fastlane-sirp (>= 1.1.0) + fastlane-sirp (>= 1.0.0) gh_inspector (>= 1.1.2, < 2.0.0) google-apis-androidpublisher_v3 (~> 0.3) google-apis-playcustomapp_v1 (~> 0.1) - google-cloud-env (>= 1.6.0, <= 2.1.1) + google-cloud-env (>= 1.6.0, < 2.0.0) google-cloud-storage (~> 1.31) highline (~> 2.0) http-cookie (~> 1.0.5) @@ -105,11 +103,10 @@ GEM logger (>= 1.6, < 2.0) mini_magick (>= 4.9.4, < 5.0.0) multipart-post (>= 2.0.0, < 3.0.0) - mutex_m (~> 0.3) + mutex_m (~> 0.3.0) naturally (~> 2.2) - nkf (~> 0.2) + nkf (~> 0.2.0) optparse (>= 0.1.1, < 1.0.0) - ostruct (>= 0.1.0) plist (>= 3.1.0, < 4.0.0) rubyzip (>= 2.0.0, < 3.0.0) security (= 0.1.5) @@ -122,42 +119,41 @@ GEM xcodeproj (>= 1.13.0, < 2.0.0) xcpretty (~> 0.4.1) xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) - fastlane-sirp (1.1.0) + fastlane-sirp (1.0.0) + sysrandom (~> 1.0) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.102.0) - google-apis-core (>= 0.15.0, < 2.a) - google-apis-core (0.18.0) + google-apis-androidpublisher_v3 (0.54.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-core (0.11.3) addressable (~> 2.5, >= 2.5.1) - googleauth (~> 1.9) - httpclient (>= 2.8.3, < 3.a) + googleauth (>= 0.16.2, < 2.a) + httpclient (>= 2.8.1, < 3.a) mini_mime (~> 1.0) - mutex_m representable (~> 3.0) retriable (>= 2.0, < 4.a) - google-apis-iamcredentials_v1 (0.27.0) - google-apis-core (>= 0.15.0, < 2.a) - google-apis-playcustomapp_v1 (0.17.0) - google-apis-core (>= 0.15.0, < 2.a) - google-apis-storage_v1 (0.63.0) - google-apis-core (>= 0.15.0, < 2.a) - google-cloud-core (1.9.0) + rexml + google-apis-iamcredentials_v1 (0.17.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-playcustomapp_v1 (0.13.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-storage_v1 (0.31.0) + google-apis-core (>= 0.11.0, < 2.a) + google-cloud-core (1.8.0) google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) - google-cloud-env (2.1.1) - faraday (>= 1.0, < 3.a) - google-cloud-errors (1.6.0) - google-cloud-storage (1.61.0) + google-cloud-env (1.6.0) + faraday (>= 0.17.3, < 3.0) + google-cloud-errors (1.5.0) + google-cloud-storage (1.47.0) addressable (~> 2.8) digest-crc (~> 0.4) - google-apis-core (>= 0.18, < 2) - google-apis-iamcredentials_v1 (~> 0.18) - google-apis-storage_v1 (>= 0.42) + google-apis-iamcredentials_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.31.0) google-cloud-core (~> 1.6) - googleauth (~> 1.9) + googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) - googleauth (1.11.2) - faraday (>= 1.0, < 3.a) - google-cloud-env (~> 2.1) + googleauth (1.8.1) + faraday (>= 0.17.3, < 3.a) jwt (>= 1.4, < 3.0) multi_json (~> 1.11) os (>= 0.9, < 2.0) @@ -168,41 +164,42 @@ GEM httpclient (2.9.0) mutex_m jmespath (1.6.2) - json (2.19.9) - jwt (2.10.3) + json (2.18.0) + jwt (2.10.2) base64 logger (1.7.0) mini_magick (4.13.2) mini_mime (1.1.5) - multi_json (1.21.1) + multi_json (1.19.1) multipart-post (2.4.1) mutex_m (0.3.0) nanaimo (0.4.0) naturally (2.3.0) - nkf (0.3.0) + nkf (0.2.0) optparse (0.8.1) os (1.1.4) - ostruct (0.6.3) plist (3.7.2) - public_suffix (7.0.5) - rake (13.4.2) + public_suffix (7.0.0) + rake (13.3.1) representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) - retriable (3.8.0) + retriable (3.1.2) rexml (3.4.4) rouge (3.28.0) ruby2_keywords (0.0.5) rubyzip (2.4.1) security (0.1.5) - signet (0.22.0) + signet (0.21.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) jwt (>= 1.5, < 4.0) + multi_json (~> 1.10) simctl (1.6.10) CFPropertyList naturally + sysrandom (1.0.5) terminal-notifier (2.0.0) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 00000000..d2a897d0 --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,60 @@ +plugins { + id "com.android.application" + // START: FlutterFire Configuration + id 'com.google.gms.google-services' + id 'com.google.firebase.firebase-perf' + id 'com.google.firebase.crashlytics' + // END: FlutterFire Configuration + id "kotlin-android" + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id "dev.flutter.flutter-gradle-plugin" +} + +def keystoreProperties = new Properties() +def keystorePropertiesFile = rootProject.file("key.properties") +if (keystorePropertiesFile.exists()) { + keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) +} + +android { + namespace = "com.tommychow.frosty" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8 + } + + defaultConfig { + applicationId = "com.tommychow.frosty" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + signingConfigs { + release { + keyAlias keystoreProperties["keyAlias"] + keyPassword keystoreProperties["keyPassword"] + storeFile keystoreProperties["storeFile"] ? file(keystoreProperties["storeFile"]) : null + storePassword keystoreProperties["storePassword"] + } + } + buildTypes { + release { + signingConfig signingConfigs.release + } + } +} + +flutter { + source = "../.." +} diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts deleted file mode 100644 index 998334f3..00000000 --- a/android/app/build.gradle.kts +++ /dev/null @@ -1,70 +0,0 @@ -import java.io.FileInputStream -import java.util.Properties - -plugins { - id("com.android.application") - id("com.google.gms.google-services") - id("com.google.firebase.firebase-perf") - id("com.google.firebase.crashlytics") - // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. - id("dev.flutter.flutter-gradle-plugin") -} - -val keystoreProperties = Properties() -val keystorePropertiesFile = rootProject.file("key.properties") -if (keystorePropertiesFile.exists()) { - keystoreProperties.load(FileInputStream(keystorePropertiesFile)) -} - -android { - namespace = "com.tommychow.frosty" - compileSdk = flutter.compileSdkVersion - ndkVersion = flutter.ndkVersion - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - } - - defaultConfig { - applicationId = "com.tommychow.frosty" - minSdk = flutter.minSdkVersion - targetSdk = flutter.targetSdkVersion - versionCode = flutter.versionCode - versionName = flutter.versionName - } - - signingConfigs { - create("release") { - keyAlias = keystoreProperties["keyAlias"] as String? - keyPassword = keystoreProperties["keyPassword"] as String? - storeFile = keystoreProperties["storeFile"]?.let { file(it) } - storePassword = keystoreProperties["storePassword"] as String? - } - } - - buildTypes { - release { - // Use the real upload key when key.properties is present (release - // workflows create it from secrets). Allow debug signing only when a CI - // compile-only check explicitly opts in via ALLOW_DEBUG_SIGNED_RELEASE; - // otherwise fail loudly so a real release can never be silently - // debug-signed if the keystore step is ever missing. - signingConfig = when { - keystorePropertiesFile.exists() -> signingConfigs.getByName("release") - System.getenv("ALLOW_DEBUG_SIGNED_RELEASE") != null -> signingConfigs.getByName("debug") - else -> error("key.properties missing — set ALLOW_DEBUG_SIGNED_RELEASE=1 for compile-only builds") - } - } - } -} - -kotlin { - compilerOptions { - jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 - } -} - -flutter { - source = "../.." -} diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 4beaec33..36beb448 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -13,8 +13,7 @@ android:label="Frosty" android:name="${applicationName}" android:icon="@mipmap/ic_launcher" - android:allowBackup="false" - android:dataExtractionRules="@xml/data_extraction_rules"> + android:allowBackup="false"> + false false - true + false shortEdges