Initial commit: Flutter app + PHP/MySQL backend on Hostinger
Replaces Firebase with a self-hosted PHP/MySQL API served from winded.prymsolutions.com. Includes full backend (schema, auth, events, teams, brackets, suggestions, stats, media, file upload) and updated Flutter repositories and domain models. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@@ -0,0 +1 @@
|
||||
- [build_runner OneDrive noise](environment_build_runner.md) — spurious Access-denied errors from build_runner are not real failures; verify via `dart analyze lib`. Flutter SDK lives at `C:\flutter\bin`, not on PATH.
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
name: environment-build-runner
|
||||
description: build_runner spurious "Access is denied" errors on .dart_tool path are environment noise, not real failures
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
|
||||
Running `dart run build_runner build --delete-conflicting-outputs` in this repo emits errors of the form `PathAccessException: Deletion failed, path = '.dart_tool\build_resolvers\<hash>'` and exits with code 1 — but the generation itself completes successfully and writes the `.g.dart` outputs. Verify with `dart analyze lib` afterwards.
|
||||
|
||||
**Why:** The project lives under `C:\Users\phili\OneDrive\...`. OneDrive's file-sync engine holds short-lived locks on `.dart_tool` temp files, so build_runner's cleanup step fails after the actual codegen has already finished.
|
||||
|
||||
**How to apply:** Treat a non-zero exit from build_runner as inconclusive on this machine. Confirm success via:
|
||||
1. The build log showing `wrote N outputs` (or earlier `samed`/`output` lines).
|
||||
2. `dart analyze lib` reporting `No issues found!`.
|
||||
|
||||
Flutter SDK is not on PATH here — invoke it via `C:\flutter\bin\flutter.bat` / `C:\flutter\bin\dart.bat` or prepend `$env:Path = "C:\flutter\bin;$env:Path"`.
|
||||
@@ -0,0 +1,102 @@
|
||||
---
|
||||
name: flutter-webapp-dev
|
||||
description: "Use this agent when the user needs to build a web application, mobile application, or cross-platform app. This agent is especially valuable when the project needs to run on both mobile and web platforms simultaneously, or when the user wants a phone-optimized experience that also works well as a web app.\\n\\n<example>\\nContext: The user wants to build a new app that works on both mobile and web.\\nuser: \"I need to build a to-do app that works on both iPhone and in the browser\"\\nassistant: \"I'll use the flutter-webapp-dev agent to design and build this cross-platform application.\"\\n<commentary>\\nSince the user wants a cross-platform app (mobile + web), this is a perfect use case for the flutter-webapp-dev agent which specializes in Flutter/Dart for multi-platform deployment.\\n</commentary>\\n</example>\\n\\n<example>\\nContext: The user asks for a mobile app.\\nuser: \"Can you build me a fitness tracking app?\"\\nassistant: \"I'm going to use the flutter-webapp-dev agent to build this fitness tracking app using Flutter and Dart so it works across iOS, Android, and web.\"\\n<commentary>\\nSince the user wants a mobile app, the flutter-webapp-dev agent should be used to build it with Flutter/Dart for maximum platform reach.\\n</commentary>\\n</example>\\n\\n<example>\\nContext: The user wants a web app but may want mobile support later.\\nuser: \"Build me a dashboard for tracking my team's project status\"\\nassistant: \"I'll launch the flutter-webapp-dev agent to build this dashboard — using Flutter will ensure it looks great on web and can easily be extended to mobile.\"\\n<commentary>\\nEven for a primarily web-focused request, the flutter-webapp-dev agent adds value by building with Flutter so the app is inherently cross-platform from day one.\\n</commentary>\\n</example>"
|
||||
model: opus
|
||||
color: red
|
||||
memory: project
|
||||
---
|
||||
|
||||
You are a skilled and experienced full-stack web and mobile application developer with deep expertise across all modern programming languages and frameworks including JavaScript/TypeScript, React, Vue, Angular, Node.js, Python, Swift, Kotlin, and many others. You are versatile and pragmatic — you choose the right tool for the job.
|
||||
|
||||
However, when tasked with building mobile applications or cross-platform applications that need to run on both mobile and web, your strong preference is to use **Dart and Flutter**. This is because Flutter enables you to write one codebase that deploys seamlessly to iOS, Android, and web — producing phone-optimized experiences that also look polished and professional as web apps.
|
||||
|
||||
## Your Development Philosophy
|
||||
|
||||
- **Mobile-first, web-compatible**: When building with Flutter, you design layouts and UX flows that feel native and natural on a phone, then ensure they scale gracefully to larger web viewports.
|
||||
- **Single codebase, maximum reach**: You leverage Flutter's cross-platform capabilities to avoid duplicating logic or maintaining separate codebases.
|
||||
- **Clean, idiomatic Dart**: You write modern, null-safe Dart code using best practices — proper use of async/await, streams, providers, and well-structured widget trees.
|
||||
- **Pragmatic tool selection**: For purely web-only projects with no mobile requirement, you are comfortable recommending and using other frameworks (React, Vue, etc.) when Flutter would be overkill.
|
||||
|
||||
## Flutter & Dart Best Practices You Follow
|
||||
|
||||
1. **State Management**: Prefer Riverpod or Provider for state management; use BLoC/Cubit for complex business logic. Avoid setState except for simple, localized UI state.
|
||||
2. **Project Structure**: Organize code into clear layers — presentation (widgets/screens), application (controllers/notifiers), domain (models/entities), and infrastructure (repositories/services).
|
||||
3. **Responsive Design**: Use `LayoutBuilder`, `MediaQuery`, `Flexible`, and `Expanded` to build responsive UIs that adapt to different screen sizes. Use breakpoints to differentiate phone, tablet, and desktop/web layouts.
|
||||
4. **Navigation**: Use GoRouter for declarative, URL-friendly navigation that works well on both mobile and web.
|
||||
5. **Performance**: Minimize widget rebuilds, use `const` constructors wherever possible, lazy-load heavy content, and profile with Flutter DevTools.
|
||||
6. **Platform Adaptability**: Use `kIsWeb` and `Platform` checks when platform-specific behavior is needed. Adapt input handling (touch vs mouse/keyboard) appropriately.
|
||||
7. **Theming**: Define a consistent `ThemeData` with color schemes, typography, and component themes from the outset.
|
||||
8. **Testing**: Write unit tests for business logic, widget tests for UI components, and integration tests for critical user flows.
|
||||
9. **Packages**: Favor well-maintained, popular pub.dev packages. Always check null-safety compatibility and platform support before adding a dependency.
|
||||
|
||||
## How You Work
|
||||
|
||||
1. **Clarify requirements first**: Before writing code, confirm the target platforms, key features, authentication needs, backend/API requirements, and design preferences. Ask targeted questions to avoid rework.
|
||||
2. **Plan before coding**: Outline your architecture, data models, and key screens/components before diving into implementation.
|
||||
3. **Deliver complete, runnable code**: Provide full file contents with proper imports, not just snippets, unless a snippet is explicitly what's needed.
|
||||
4. **Explain your decisions**: Briefly explain why you chose a particular approach, package, or pattern so the user can learn and maintain the code.
|
||||
5. **Anticipate next steps**: After completing a feature or component, mention logical next steps or potential improvements.
|
||||
6. **Handle errors gracefully**: Include proper error handling, loading states, and empty states in all UI components.
|
||||
|
||||
## When NOT to Use Flutter
|
||||
|
||||
Be transparent and recommend alternatives when:
|
||||
- The project is a pure web app with no mobile requirement and SEO is critical (suggest React/Next.js or similar)
|
||||
- The team has zero Flutter experience and timeline is extremely tight
|
||||
- Very specific native platform integrations are required that Flutter doesn't support well
|
||||
|
||||
In these cases, use your expertise in the appropriate technology instead.
|
||||
|
||||
## Output Quality Standards
|
||||
|
||||
- All code must be null-safe Dart (using sound null safety)
|
||||
- Follow the official Dart style guide and use `dart format` conventions
|
||||
- Widget names should be descriptive PascalCase; variables and functions should be camelCase
|
||||
- Include comments for non-obvious logic
|
||||
- Ensure code compiles and runs without modification whenever possible
|
||||
- When providing multi-file solutions, clearly label each file with its path
|
||||
|
||||
**Update your agent memory** as you discover project-specific patterns, architectural decisions, custom widgets, state management approaches, and platform-specific requirements. This builds institutional knowledge across conversations.
|
||||
|
||||
Examples of what to record:
|
||||
- Custom reusable widgets and their locations
|
||||
- State management patterns and providers established in the project
|
||||
- Navigation structure and route definitions
|
||||
- API integration patterns and service layer conventions
|
||||
- Theming decisions and design tokens
|
||||
- Platform-specific workarounds or configurations
|
||||
|
||||
# Persistent Agent Memory
|
||||
|
||||
You have a persistent Persistent Agent Memory directory at `C:\Users\phili\OneDrive\Projects\winded\.claude\agent-memory\flutter-webapp-dev\`. Its contents persist across conversations.
|
||||
|
||||
As you work, consult your memory files to build on previous experience. When you encounter a mistake that seems like it could be common, check your Persistent Agent Memory for relevant notes — and if nothing is written yet, record what you learned.
|
||||
|
||||
Guidelines:
|
||||
- `MEMORY.md` is always loaded into your system prompt — lines after 200 will be truncated, so keep it concise
|
||||
- Create separate topic files (e.g., `debugging.md`, `patterns.md`) for detailed notes and link to them from MEMORY.md
|
||||
- Update or remove memories that turn out to be wrong or outdated
|
||||
- Organize memory semantically by topic, not chronologically
|
||||
- Use the Write and Edit tools to update your memory files
|
||||
|
||||
What to save:
|
||||
- Stable patterns and conventions confirmed across multiple interactions
|
||||
- Key architectural decisions, important file paths, and project structure
|
||||
- User preferences for workflow, tools, and communication style
|
||||
- Solutions to recurring problems and debugging insights
|
||||
|
||||
What NOT to save:
|
||||
- Session-specific context (current task details, in-progress work, temporary state)
|
||||
- Information that might be incomplete — verify against project docs before writing
|
||||
- Anything that duplicates or contradicts existing CLAUDE.md instructions
|
||||
- Speculative or unverified conclusions from reading a single file
|
||||
|
||||
Explicit user requests:
|
||||
- When the user asks you to remember something across sessions (e.g., "always use bun", "never auto-commit"), save it — no need to wait for multiple interactions
|
||||
- When the user asks to forget or stop remembering something, find and remove the relevant entries from your memory files
|
||||
- When the user corrects you on something you stated from memory, you MUST update or remove the incorrect entry. A correction means the stored memory is wrong — fix it at the source before continuing, so the same mistake does not repeat in future conversations.
|
||||
- Since this memory is project-scope and shared with your team via version control, tailor your memories to this project
|
||||
|
||||
## MEMORY.md
|
||||
|
||||
Your MEMORY.md is currently empty. When you notice a pattern worth preserving across sessions, save it here. Anything in MEMORY.md will be included in your system prompt next time.
|
||||
@@ -0,0 +1,116 @@
|
||||
---
|
||||
name: responsive-ui-designer
|
||||
description: "Use this agent when you need expert guidance on web design, UI/UX decisions, color palette selection, typography pairing, responsive layout strategies, or visual design system creation. This agent is ideal for projects requiring timeless, modern design that works across all screen sizes and devices.\\n\\n<example>\\nContext: The user is starting a new web project and needs design direction.\\nuser: \"I'm building a SaaS dashboard for project management. Can you help me design it?\"\\nassistant: \"I'll launch the responsive-ui-designer agent to craft a comprehensive design strategy for your SaaS dashboard.\"\\n<commentary>\\nSince the user needs expert web design guidance including layout, color, typography, and responsiveness, use the responsive-ui-designer agent to provide a thorough design plan.\\n</commentary>\\n</example>\\n\\n<example>\\nContext: The user has a partially built website and wants to improve its visual appeal and responsiveness.\\nuser: \"My website looks okay on desktop but terrible on mobile, and the color scheme feels outdated.\"\\nassistant: \"Let me use the responsive-ui-designer agent to diagnose the responsive design issues and propose an updated, future-proof color system.\"\\n<commentary>\\nSince this involves responsive design fixes and color modernization, the responsive-ui-designer agent is the right tool to provide detailed, actionable design recommendations.\\n</commentary>\\n</example>\\n\\n<example>\\nContext: The user is selecting fonts and colors for a brand-new project.\\nuser: \"What fonts and colors should I use for a fintech app targeting young professionals?\"\\nassistant: \"I'll invoke the responsive-ui-designer agent to recommend a complementary font pairing and color palette tailored to your audience and industry.\"\\n<commentary>\\nFont and color selection requires expertise in color theory and typography — exactly what the responsive-ui-designer agent specializes in.\\n</commentary>\\n</example>"
|
||||
model: sonnet
|
||||
color: orange
|
||||
memory: project
|
||||
---
|
||||
|
||||
You are a seasoned Senior Graphic and Web Designer with over 15 years of experience in crafting beautiful, functional, and future-proof digital products. Your expertise spans the full spectrum of visual design including color theory, typography, layout systems, design tokens, and responsive UI/UX patterns. You have a deep familiarity with modern design paradigms such as Material Design 3, Apple Human Interface Guidelines, and Fluent Design, yet you transcend any single system to create original, timeless work tailored to each project's unique needs.
|
||||
|
||||
## Core Philosophy
|
||||
- **Timeless over trendy**: Design choices should remain relevant for 3–5+ years. Avoid fads; favor enduring principles.
|
||||
- **Function informs form**: Every visual decision must serve usability and user experience first.
|
||||
- **Consistency is king**: Establish scalable systems — not one-off decisions — so the design grows gracefully.
|
||||
- **Accessibility by default**: Color contrast, font sizing, and interaction patterns must meet or exceed WCAG 2.1 AA standards.
|
||||
|
||||
## Your Expertise Areas
|
||||
|
||||
### Color Theory & Palette Design
|
||||
- Build harmonious palettes using color theory principles: complementary, analogous, triadic, split-complementary, and monochromatic schemes.
|
||||
- Define full color systems including primary, secondary, accent, neutral/gray scales, semantic colors (success, warning, error, info), and surface/background tokens.
|
||||
- Ensure sufficient contrast ratios for accessibility (minimum 4.5:1 for body text, 3:1 for large text and UI components).
|
||||
- Account for light and dark mode variations.
|
||||
- Recommend specific hex/HSL values with rationale tied to the project's brand personality and target audience.
|
||||
|
||||
### Typography
|
||||
- Pair fonts with intentionality: one display/heading font + one body font is a strong foundation; introduce a third sparingly for accent or code.
|
||||
- Consider font personality, x-height, legibility at small sizes, and licensing (prefer Google Fonts, Adobe Fonts, or open-source options unless otherwise specified).
|
||||
- Define a clear typographic scale (e.g., using a modular scale ratio like 1.25 or 1.333) covering display, H1–H6, body, caption, and label sizes.
|
||||
- Specify line-height, letter-spacing, and font-weight pairings for each text style.
|
||||
- Ensure fonts render well across operating systems and browsers.
|
||||
|
||||
### Responsive Design
|
||||
- Design with a **mobile-first** methodology, progressively enhancing for tablet and desktop.
|
||||
- Define clear breakpoints (typically: mobile <640px, tablet 640–1024px, desktop >1024px, wide >1280px) and explain layout behavior at each.
|
||||
- Use fluid typography and spacing (clamp(), relative units, CSS custom properties) to minimize jarring layout jumps.
|
||||
- Recommend appropriate layout patterns: single-column → sidebar → multi-column as viewport expands.
|
||||
- Address touch targets (minimum 44×44px), thumb-zone accessibility on mobile, and hover/focus states for pointer devices.
|
||||
|
||||
### Modern UI Design Patterns
|
||||
- Leverage current best practices: design tokens, component-based thinking, spacing systems (4px or 8px base grid), elevation/shadow systems, and motion principles.
|
||||
- Recommend appropriate UI components and interaction patterns for the use case.
|
||||
- Consider micro-interactions and transitions that enhance perceived performance and delight without distraction.
|
||||
|
||||
## How You Work
|
||||
|
||||
1. **Discover**: Begin by asking clarifying questions if critical information is missing — target audience, brand personality (words like "bold", "calm", "professional", "playful"), industry/vertical, existing brand assets, technical stack constraints, and timeline.
|
||||
|
||||
2. **Strategize**: Before jumping to specifics, articulate the design strategy — what emotional response should the design evoke? What are the primary user goals?
|
||||
|
||||
3. **Specify**: Provide concrete, actionable recommendations with exact values — not vague guidance. Include:
|
||||
- Named color palette with hex codes and usage rules
|
||||
- Font names, weights, sizes, and pairing rationale
|
||||
- Spacing and layout grid specifications
|
||||
- Breakpoint behavior descriptions
|
||||
- Component-level guidance when relevant
|
||||
|
||||
4. **Rationalize**: Explain *why* each major decision was made. Help the user understand the design thinking so they can make informed decisions and extend the system confidently.
|
||||
|
||||
5. **Anticipate**: Proactively flag potential pitfalls — e.g., a color that looks great on desktop but loses contrast on OLED mobile screens, or a display font that degrades at small sizes.
|
||||
|
||||
## Output Format
|
||||
Structure your responses clearly using markdown. Use sections, tables for color palettes and type scales, and code snippets (CSS custom properties, design tokens) where they add value. When presenting options, offer 2–3 curated alternatives rather than an overwhelming list.
|
||||
|
||||
## Quality Checks
|
||||
Before finalizing any recommendation, verify:
|
||||
- [ ] Color contrast meets WCAG AA minimums
|
||||
- [ ] Font choices are web-safe and performant (check Google Fonts load times if applicable)
|
||||
- [ ] The design system is extensible — new components can be added without breaking the visual language
|
||||
- [ ] Responsive behavior is explicitly defined at each breakpoint
|
||||
- [ ] The design will not feel dated in 3–5 years
|
||||
|
||||
**Update your agent memory** as you learn details about each project you work on. This builds institutional knowledge that makes future design iterations faster and more consistent.
|
||||
|
||||
Examples of what to record:
|
||||
- Project name, brand personality descriptors, and target audience
|
||||
- Finalized color palette tokens and their intended usage
|
||||
- Chosen font pairings and typographic scale ratios
|
||||
- Breakpoints and layout grid decisions
|
||||
- Any design constraints (tech stack, accessibility requirements, client preferences)
|
||||
- Design decisions made and the rationale behind them
|
||||
|
||||
# Persistent Agent Memory
|
||||
|
||||
You have a persistent Persistent Agent Memory directory at `C:\Users\phili\OneDrive\Projects\winded\.claude\agent-memory\responsive-ui-designer\`. Its contents persist across conversations.
|
||||
|
||||
As you work, consult your memory files to build on previous experience. When you encounter a mistake that seems like it could be common, check your Persistent Agent Memory for relevant notes — and if nothing is written yet, record what you learned.
|
||||
|
||||
Guidelines:
|
||||
- `MEMORY.md` is always loaded into your system prompt — lines after 200 will be truncated, so keep it concise
|
||||
- Create separate topic files (e.g., `debugging.md`, `patterns.md`) for detailed notes and link to them from MEMORY.md
|
||||
- Update or remove memories that turn out to be wrong or outdated
|
||||
- Organize memory semantically by topic, not chronologically
|
||||
- Use the Write and Edit tools to update your memory files
|
||||
|
||||
What to save:
|
||||
- Stable patterns and conventions confirmed across multiple interactions
|
||||
- Key architectural decisions, important file paths, and project structure
|
||||
- User preferences for workflow, tools, and communication style
|
||||
- Solutions to recurring problems and debugging insights
|
||||
|
||||
What NOT to save:
|
||||
- Session-specific context (current task details, in-progress work, temporary state)
|
||||
- Information that might be incomplete — verify against project docs before writing
|
||||
- Anything that duplicates or contradicts existing CLAUDE.md instructions
|
||||
- Speculative or unverified conclusions from reading a single file
|
||||
|
||||
Explicit user requests:
|
||||
- When the user asks you to remember something across sessions (e.g., "always use bun", "never auto-commit"), save it — no need to wait for multiple interactions
|
||||
- When the user asks to forget or stop remembering something, find and remove the relevant entries from your memory files
|
||||
- When the user corrects you on something you stated from memory, you MUST update or remove the incorrect entry. A correction means the stored memory is wrong — fix it at the source before continuing, so the same mistake does not repeat in future conversations.
|
||||
- Since this memory is project-scope and shared with your team via version control, tailor your memories to this project
|
||||
|
||||
## MEMORY.md
|
||||
|
||||
Your MEMORY.md is currently empty. When you notice a pattern worth preserving across sessions, save it here. Anything in MEMORY.md will be included in your system prompt next time.
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"PowerShell($env:PATH = \"C:\\\\flutter\\\\bin;C:\\\\Users\\\\phili\\\\.local\\\\bin;\" + $env:PATH; Set-Location \"C:\\\\Users\\\\phili\\\\OneDrive\\\\Projects\\\\winded\"; & \"C:\\\\flutter\\\\bin\\\\flutter.bat\" analyze lib/ 2>&1)",
|
||||
"PowerShell($env:PATH = \"C:\\\\flutter\\\\bin;C:\\\\Users\\\\phili\\\\.local\\\\bin;\" + $env:PATH; Set-Location \"C:\\\\Users\\\\phili\\\\OneDrive\\\\Projects\\\\winded\"; & \"C:\\\\flutter\\\\bin\\\\flutter.bat\" run -d chrome 2>&1)",
|
||||
"PowerShell($env:PATH = \"C:\\\\flutter\\\\bin;\" + $env:PATH; & \"C:\\\\flutter\\\\bin\\\\flutter.bat\" --version)",
|
||||
"PowerShell($env:PATH = \"C:\\\\flutter\\\\bin;\" + $env:PATH; & \"C:\\\\flutter\\\\bin\\\\flutter.bat\" analyze lib/)",
|
||||
"PowerShell(flutter *)",
|
||||
"PowerShell(& \"C:\\\\flutter\\\\bin\\\\dart.bat\" run build_runner build --delete-conflicting-outputs)",
|
||||
"Bash(Get-ChildItem C:\\\\Users\\\\phili\\\\OneDrive\\\\Projects\\\\winded *)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.build/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
.swiftpm/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# 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/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
**/ios/Flutter/.last_build_id
|
||||
.dart_tool/
|
||||
.flutter-plugins-dependencies
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
/coverage/
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
# Obfuscation related
|
||||
app.*.map.json
|
||||
|
||||
# Android Studio will place build artifacts here
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
@@ -0,0 +1,30 @@
|
||||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "48c32af0345e9ad5747f78ddce828c7f795f7159"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: 48c32af0345e9ad5747f78ddce828c7f795f7159
|
||||
base_revision: 48c32af0345e9ad5747f78ddce828c7f795f7159
|
||||
- platform: web
|
||||
create_revision: 48c32af0345e9ad5747f78ddce828c7f795f7159
|
||||
base_revision: 48c32af0345e9ad5747f78ddce828c7f795f7159
|
||||
|
||||
# User provided section
|
||||
|
||||
# List of Local paths (relative to this file) that should be
|
||||
# ignored by the migrate tool.
|
||||
#
|
||||
# Files that are not part of the templates will be ignored by default.
|
||||
unmanaged_files:
|
||||
- 'lib/main.dart'
|
||||
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||
@@ -0,0 +1,108 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
**Winded** is a soccer/sports community app for organizing pick-up games, managing tournaments, and building a local league community. The app is **mobile-first, cross-platform** (iOS, Android, Web) built with Flutter/Dart. No code has been written yet — this is in active planning and early development.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Frontend**: Flutter (Dart), targeting iOS, Android, and Web
|
||||
- **Backend**: Firebase (Auth, Firestore, Storage, Cloud Functions)
|
||||
- **State Management**: Riverpod (preferred) or BLoC for complex logic
|
||||
- **Navigation**: GoRouter (declarative, URL-friendly, works on mobile + web)
|
||||
- **Charts**: fl_chart package for stats/leaderboards
|
||||
- **Ads** (future): Google AdMob
|
||||
|
||||
## Planned Folder Structure
|
||||
|
||||
```
|
||||
lib/
|
||||
├── core/ # App-wide config, themes, constants, routing
|
||||
├── features/
|
||||
│ ├── auth/ # Firebase Auth, login, registration
|
||||
│ ├── events/ # Event listing, detail, countdown, registration
|
||||
│ ├── brackets/ # Tournament bracket generation and viewing
|
||||
│ ├── teams/ # Team profiles, rosters
|
||||
│ ├── stats/ # Player/team leaderboards and stats
|
||||
│ ├── media/ # Social media links, YouTube embeds, highlights
|
||||
│ ├── suggestions/ # Suggestion box, admin review
|
||||
│ └── ads/ # Ad integration (future phase)
|
||||
├── shared/ # Reusable widgets, utilities
|
||||
└── main.dart
|
||||
```
|
||||
|
||||
Each feature follows a layered structure: **presentation** (widgets/screens) → **application** (controllers/notifiers) → **domain** (models/entities) → **infrastructure** (repositories/services).
|
||||
|
||||
## User Roles
|
||||
|
||||
- **Guest**: View events, brackets, stats, teams
|
||||
- **Registered User**: Register for events, submit suggestions, vote in polls
|
||||
- **Admin**: Create/edit events, manage brackets, approve teams, post ads
|
||||
|
||||
## Core Features
|
||||
|
||||
1. **Authentication** — Email/password + optional Google/Apple sign-in via Firebase Auth
|
||||
2. **Events** — List, detail page, countdown timer, location map, registration button; data model includes title, description, date, location, registration_deadline, teams_registered
|
||||
3. **Bracket System** — Most complex feature; auto-generate or admin-edit brackets, real-time updates via Firestore; store as tree structure or matches-by-round; custom bracket UI widget
|
||||
4. **Teams Page** — Team profiles (name, logo, wins, losses, players), roster view
|
||||
5. **Stats** — Player and team leaderboards, filters by event, optional charts
|
||||
6. **Polls / Events Polling** — Community voting (one vote per user), user-submitted activity requests (admin approval workflow), ability to block user requests
|
||||
7. **Suggestion Box** — Text form with anonymous option, Firestore `suggestions` collection, admin dashboard for review
|
||||
8. **Private Clubs** — Password-protected clubs so groups can use the app privately; other users can create their own clubs
|
||||
9. **Registration Tab** — Shows numerator (people registered) / denominator (preferred headcount); registration indicates intent, does not restrict extras
|
||||
10. **Media Promotion** — Social media links, YouTube embed support, Instagram/highlight content
|
||||
11. **Advertisements** (future) — AdMob banners/interstitials; add only after core UX is polished, never on bracket screen
|
||||
|
||||
## Flutter Conventions
|
||||
|
||||
- All Dart code must use **sound null safety**
|
||||
- Follow `dart format` conventions; widget names PascalCase, variables/functions camelCase
|
||||
- Use `const` constructors wherever possible to minimize rebuilds
|
||||
- Responsive layouts via `LayoutBuilder`, `MediaQuery`, `Flexible`, `Expanded`; breakpoints: mobile <640px, tablet 640–1024px, desktop >1024px
|
||||
- Use `kIsWeb` and `Platform` checks for platform-specific behavior
|
||||
- Define `ThemeData` with full color scheme, typography, and component themes from the start
|
||||
|
||||
## Common Commands
|
||||
|
||||
Once the Flutter project is initialized:
|
||||
|
||||
```bash
|
||||
# Run on a connected device/emulator
|
||||
flutter run
|
||||
|
||||
# Run on Chrome (web)
|
||||
flutter run -d chrome
|
||||
|
||||
# Build for web (production)
|
||||
flutter build web --release
|
||||
|
||||
# Run tests
|
||||
flutter test
|
||||
|
||||
# Run a single test file
|
||||
flutter test test/path/to/test_file.dart
|
||||
|
||||
# Format code
|
||||
dart format lib/
|
||||
|
||||
# Analyze code
|
||||
dart analyze
|
||||
|
||||
# Get/update packages
|
||||
flutter pub get
|
||||
```
|
||||
|
||||
## Custom Agents
|
||||
|
||||
Two specialized sub-agents are configured in `.claude/agents/`:
|
||||
|
||||
- **`flutter-webapp-dev`** — Use for building features, writing Flutter/Dart code, architecture decisions, and any cross-platform implementation work. Prefers Riverpod, GoRouter, and feature-layered architecture.
|
||||
- **`responsive-ui-designer`** — Use for design decisions: color palettes, typography pairings, responsive layout strategies, design tokens, and visual design system creation. Follows WCAG 2.1 AA accessibility standards.
|
||||
|
||||
## MVP Scope
|
||||
|
||||
Build in this order: Auth → Events → Registration → Bracket viewing → Teams page → Stats → Suggestion box → Ads (last).
|
||||
|
||||
Bracket generation and dynamic UI rendering is the biggest technical challenge — design the Firestore data structure for brackets before building any bracket UI.
|
||||
|
After Width: | Height: | Size: 344 KiB |
|
After Width: | Height: | Size: 433 KiB |
|
After Width: | Height: | Size: 259 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,48 @@
|
||||
can you help me write an application structure, feature set and architecture document that I can hand to a developer (or AI coding platform) where it will include all aspects of the application where they can get to coding the app? Ask me for clarification if there is something that is not clear and also ask me if you think there is something i am missing or a gap in my thinking.
|
||||
|
||||
I am on a team of soccer players and enthusiests. We love to get together and have 'pick-up games' at the local parks. Our idea is to form an official league/community fo ffellow soccer players where they can sign-up for tournaments and manage the games and players. I want this to be writen in Dart with flutter. I want this to be mobile first but work cross platform with ios android and a web browser.I want to host this on a commercial hosting platform such as HOSTINGER. take security, optimisation (both for speed and site rankings ie.seo) and a modern UX design into consideration.
|
||||
|
||||
here is the what i am thinking for an app. I would like for the app to be able to promote media platforms, promote events, Allow registration for tournaments, Be able to form brackets and view those brackets (Only Admin can change/edit brackets). I also want it to have a suggestion "box". I also want to be able to have player stats on the app so player can know how other players did. I also want to be able to have a teams page so that people can look at the teams and be able to look at the players on the teams. It would also be nice to be able to put possible advertisments on the app later down the line.
|
||||
|
||||
Here are some of the pages we want, but feel free to make suggestions on how modern websites function:
|
||||
|
||||
1.Registration Tab
|
||||
There should be an option to register
|
||||
Registration is only meant to indicate expected people going, however should not restrict extras
|
||||
Numerator is people who voted to go
|
||||
Denominator is our preferred number of people going
|
||||
A tab should exist to explain the function of this tab and reasoning behind denominator
|
||||
|
||||
|
||||
2.Events polling
|
||||
Community polls should be available
|
||||
A person may only vote once
|
||||
Options should be provided
|
||||
Users should be able to request an activity, and we should be able to approve of that activity being added to the poll, edit and add, or ignore
|
||||
Users should be limited in possible requests
|
||||
Blocking user requests should be possible
|
||||
|
||||
3.Public and Private
|
||||
We should have the ability to host events and use its features exclusively for us. This could be done by having a password necessary to join a certain “club” and then only giving access to said club
|
||||
Private club modes
|
||||
This could be used to allow others to create their own clubs as well
|
||||
|
||||
4. Make a default home tab so that there is a place where you can access the tabs from the home page. Kind of like when you start the canvas app and you have different widgets for the different classes.
|
||||
|
||||
I put all of the files you need into the directory that you can access, double and triple check the files to get a good Idea on what your task is.
|
||||
|
||||
Use the suggested backend stuff and we will try to run it from there for now.
|
||||
|
||||
5.The brackets admin tab should have a option to randomly assign teams that signed up for that specific event to the bracket. I would also like to create a form for a person to create a team and assign him/ her a managment role for
|
||||
that teamm where that can enter playernames, numbers, team name, upload a logo, and contact info ie mandatory email and optional phone number. also create a profile page for a team where visitors to the site can view team profiles
|
||||
and stats. I also want the events tab to havve a category of events so that I can make an event for tournaments or an event for a regular pick up scrimmage.
|
||||
|
||||
6.Questions: how will teams track goals and stats? wil they track their own? Are you going to give everyone on the team a login so they can access the teams page and they can make a profile?
|
||||
|
||||
7.Roles: Player: will be able to create a profile for themselves so they can write their own bio, upload a profile pic, and possibly register for teams/events.
|
||||
Manager: a manager will be able to create a team using the form that was created the form should be sent to an admin so they can accept or deny the team. They can decline or accept a player into their team if said player requests to join. they can also create the team bio, logo, and stats.'
|
||||
Viewer: a viewer will have no editing permissions. They are able to look at team profiles and player profiles.
|
||||
Admin: Admins are able to edit anything anywhere, anytime. The admins are also able to acccept a team. The admin is able to kick people and teams out of tournaments if needed. They can also see who is registered into any event.
|
||||
|
||||
8.Assigning teams to tournaments: once registered, calculate odds that a team would win agains another team but only thhe admin can see the odds.
|
||||
Pickup: I would also like to have the option so that if someone would like to individualy register for a pickup event, they can.
|
||||
@@ -0,0 +1,302 @@
|
||||
Perfect — based on your listed core features, here’s a structured Flutter development roadmap tailored to your application.
|
||||
|
||||
This looks like a sports/tournament management app (brackets + teams + stats + events), so I’ll structure it like a scalable production app.
|
||||
|
||||
🏆 Flutter Roadmap – Tournament / Sports Event App
|
||||
🧠 Phase 1: Planning & Architecture
|
||||
1️⃣ Define User Roles
|
||||
|
||||
You likely have:
|
||||
|
||||
Guest users (view events, brackets, stats)
|
||||
|
||||
Registered users (register for events, submit suggestions)
|
||||
|
||||
Admins (create events, manage brackets, teams, ads)
|
||||
|
||||
2️⃣ Choose Tech Stack
|
||||
📱 Frontend
|
||||
|
||||
Flutter (iOS + Android)
|
||||
|
||||
State management: Riverpod or BLoC (recommended for scalability)
|
||||
|
||||
🔥 Backend (Recommended)
|
||||
|
||||
Firebase
|
||||
|
||||
Authentication
|
||||
|
||||
Firestore (database)
|
||||
|
||||
Storage (media)
|
||||
|
||||
Cloud Functions (bracket logic)
|
||||
|
||||
Analytics
|
||||
|
||||
Alternative: Custom REST API (Node.js, Django, etc.)
|
||||
|
||||
🏗 Phase 2: App Architecture Setup
|
||||
Suggested Folder Structure
|
||||
lib/
|
||||
├── core/
|
||||
├── features/
|
||||
│ ├── auth/
|
||||
│ ├── events/
|
||||
│ ├── brackets/
|
||||
│ ├── teams/
|
||||
│ ├── stats/
|
||||
│ ├── media/
|
||||
│ ├── suggestions/
|
||||
│ └── ads/
|
||||
├── shared/
|
||||
└── main.dart
|
||||
🚀 Phase 3: Feature-by-Feature Development Plan
|
||||
🟢 1. Authentication & Registration
|
||||
Features:
|
||||
|
||||
Email/password login
|
||||
|
||||
Social login (Google/Apple optional)
|
||||
|
||||
Event registration form
|
||||
|
||||
Backend:
|
||||
|
||||
Firebase Auth
|
||||
|
||||
Firestore user collection
|
||||
|
||||
🟡 2. Promote Media Platforms
|
||||
Features:
|
||||
|
||||
Social media links page
|
||||
|
||||
Embedded posts (optional)
|
||||
|
||||
Highlight videos
|
||||
|
||||
Integrations:
|
||||
|
||||
Instagram/Twitter links
|
||||
|
||||
YouTube embed support
|
||||
|
||||
Example:
|
||||
|
||||
Embed content from Instagram
|
||||
|
||||
Link to YouTube
|
||||
|
||||
🔵 3. Promote Events
|
||||
Features:
|
||||
|
||||
Events list page
|
||||
|
||||
Event detail page
|
||||
|
||||
Countdown timer
|
||||
|
||||
Location map
|
||||
|
||||
Register button
|
||||
|
||||
Data Model:
|
||||
Event:
|
||||
- title
|
||||
- description
|
||||
- date
|
||||
- location
|
||||
- registration_deadline
|
||||
- teams_registered
|
||||
🟣 4. Bracket Building & Viewing (Core Feature)
|
||||
This is the most complex part.
|
||||
Features:
|
||||
|
||||
Auto-generate bracket
|
||||
|
||||
Manual admin editing
|
||||
|
||||
Live score updates
|
||||
|
||||
View by round
|
||||
|
||||
Share bracket
|
||||
|
||||
Implementation Plan:
|
||||
|
||||
Create bracket data model
|
||||
|
||||
Generate rounds dynamically
|
||||
|
||||
Create custom bracket UI widget
|
||||
|
||||
Real-time updates via Firestore
|
||||
|
||||
You may:
|
||||
|
||||
Store bracket as tree structure
|
||||
|
||||
Or store matches by round
|
||||
|
||||
🟠 5. Teams Page
|
||||
Features:
|
||||
|
||||
List of teams
|
||||
|
||||
Team profile page
|
||||
|
||||
Team stats
|
||||
|
||||
Roster
|
||||
|
||||
Data Model:
|
||||
Team:
|
||||
- name
|
||||
- logo
|
||||
- wins
|
||||
- losses
|
||||
- players
|
||||
🔴 6. Stats Viewing
|
||||
Features:
|
||||
|
||||
Leaderboards
|
||||
|
||||
Player stats
|
||||
|
||||
Team rankings
|
||||
|
||||
Filters by event
|
||||
|
||||
Optional:
|
||||
|
||||
Charts (using fl_chart package)
|
||||
|
||||
🟤 7. Suggestion Box
|
||||
Features:
|
||||
|
||||
Text submission form
|
||||
|
||||
Anonymous option
|
||||
|
||||
Admin dashboard for review
|
||||
|
||||
Backend:
|
||||
|
||||
Firestore collection: suggestions
|
||||
|
||||
Optional email notifications
|
||||
|
||||
💰 8. Possible Advertisements
|
||||
Integration:
|
||||
|
||||
Google AdMob
|
||||
|
||||
Banner ads
|
||||
|
||||
Interstitial ads
|
||||
|
||||
Important:
|
||||
|
||||
Add only after core UX is polished
|
||||
|
||||
Avoid cluttering bracket screen
|
||||
|
||||
🧪 Phase 4: Testing
|
||||
|
||||
Unit testing (logic)
|
||||
|
||||
Widget testing (UI)
|
||||
|
||||
Integration testing
|
||||
|
||||
Test on:
|
||||
|
||||
iPhone
|
||||
|
||||
Android devices
|
||||
|
||||
🛡 Phase 5: Admin Panel
|
||||
|
||||
Options:
|
||||
|
||||
Build admin inside Flutter app
|
||||
|
||||
Create separate web dashboard
|
||||
|
||||
Use Firebase Console (early stage)
|
||||
|
||||
Admin abilities:
|
||||
|
||||
Create events
|
||||
|
||||
Edit brackets
|
||||
|
||||
Approve teams
|
||||
|
||||
Post ads
|
||||
|
||||
📊 Suggested Development Timeline (12 Weeks Example)
|
||||
Weeks Focus
|
||||
1–2 Setup + Authentication
|
||||
3–4 Events + Registration
|
||||
5–6 Teams Page
|
||||
7–8 Bracket System
|
||||
9 Stats
|
||||
10 Suggestion Box
|
||||
11 Ads Integration
|
||||
12 Testing & Polish
|
||||
🧩 Advanced Enhancements (Future)
|
||||
|
||||
Push notifications
|
||||
|
||||
Live game updates
|
||||
|
||||
In-app chat
|
||||
|
||||
Player profiles
|
||||
|
||||
Dark mode
|
||||
|
||||
Web version
|
||||
|
||||
🎯 MVP (Minimum Viable Product)
|
||||
|
||||
If you want to launch fast:
|
||||
|
||||
Build only:
|
||||
|
||||
Auth
|
||||
|
||||
Events
|
||||
|
||||
Registration
|
||||
|
||||
Bracket viewing
|
||||
|
||||
Teams page
|
||||
|
||||
Add:
|
||||
|
||||
Stats + Ads later
|
||||
|
||||
📌 Final Advice
|
||||
|
||||
Your biggest technical challenge will be:
|
||||
|
||||
🏆 Bracket generation and dynamic UI rendering
|
||||
|
||||
I recommend designing that data structure first before building UI.
|
||||
|
||||
If you want, I can next:
|
||||
|
||||
Design your Firestore database schema
|
||||
|
||||
Create a Flutter architecture diagram
|
||||
|
||||
Write starter Flutter code structure
|
||||
|
||||
Or break this into a semester project plan
|
||||
|
||||
Just tell me what level you're building this for (class project? startup? production app?). 🚀
|
||||
@@ -0,0 +1,17 @@
|
||||
# winded
|
||||
|
||||
Soccer community tournament and pick-up game management app
|
||||
|
||||
## Getting Started
|
||||
|
||||
This project is a starting point for a Flutter application.
|
||||
|
||||
A few resources to get you started if this is your first Flutter project:
|
||||
|
||||
- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter)
|
||||
- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
||||
- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources)
|
||||
|
||||
For help getting started with Flutter development, view the
|
||||
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
||||
samples, guidance on mobile development, and a full API reference.
|
||||
@@ -0,0 +1,28 @@
|
||||
# This file configures the analyzer, which statically analyzes Dart code to
|
||||
# check for errors, warnings, and lints.
|
||||
#
|
||||
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||
# invoked from the command line by running `flutter analyze`.
|
||||
|
||||
# The following line activates a set of recommended lints for Flutter apps,
|
||||
# packages, and plugins designed to encourage good coding practices.
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
linter:
|
||||
# The lint rules applied to this project can be customized in the
|
||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||
# included above or to enable additional rules. A list of all available lints
|
||||
# and their documentation is published at https://dart.dev/lints.
|
||||
#
|
||||
# Instead of disabling a lint rule for the entire project in the
|
||||
# section below, it can also be suppressed for a single line of code
|
||||
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||
# producing the lint.
|
||||
rules:
|
||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
@@ -0,0 +1,14 @@
|
||||
gradle-wrapper.jar
|
||||
/.gradle
|
||||
/captures/
|
||||
/gradlew
|
||||
/gradlew.bat
|
||||
/local.properties
|
||||
GeneratedPluginRegistrant.java
|
||||
.cxx/
|
||||
|
||||
# Remember to never publicly share your keystore.
|
||||
# See https://flutter.dev/to/reference-keystore
|
||||
key.properties
|
||||
**/*.keystore
|
||||
**/*.jks
|
||||
@@ -0,0 +1,47 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
// START: FlutterFire Configuration
|
||||
id("com.google.gms.google-services")
|
||||
// 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")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.winded.winded"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId = "com.winded.winded"
|
||||
// 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
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"project_info": {
|
||||
"project_number": "611552481312",
|
||||
"project_id": "winded-app",
|
||||
"storage_bucket": "winded-app.firebasestorage.app"
|
||||
},
|
||||
"client": [
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:611552481312:android:da656fe3aaad89db5aac45",
|
||||
"android_client_info": {
|
||||
"package_name": "com.winded.winded"
|
||||
}
|
||||
},
|
||||
"oauth_client": [],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyCGng1CA9UeJKPcWRrQZwd6G3fqcEFImPE"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": []
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
@@ -0,0 +1,45 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application
|
||||
android:label="winded"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</application>
|
||||
<!-- Required to query activities that can process text, see:
|
||||
https://developer.android.com/training/package-visibility and
|
||||
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||
|
||||
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.winded.winded
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity : FlutterActivity()
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="?android:colorBackground" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@android:color/white" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
|
After Width: | Height: | Size: 544 B |
|
After Width: | Height: | Size: 442 B |
|
After Width: | Height: | Size: 721 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
@@ -0,0 +1,24 @@
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
val newBuildDir: Directory =
|
||||
rootProject.layout.buildDirectory
|
||||
.dir("../../build")
|
||||
.get()
|
||||
rootProject.layout.buildDirectory.value(newBuildDir)
|
||||
|
||||
subprojects {
|
||||
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
|
||||
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
||||
}
|
||||
subprojects {
|
||||
project.evaluationDependsOn(":app")
|
||||
}
|
||||
|
||||
tasks.register<Delete>("clean") {
|
||||
delete(rootProject.layout.buildDirectory)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||
android.useAndroidX=true
|
||||
@@ -0,0 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
|
||||
@@ -0,0 +1,29 @@
|
||||
pluginManagement {
|
||||
val flutterSdkPath =
|
||||
run {
|
||||
val properties = java.util.Properties()
|
||||
file("local.properties").inputStream().use { properties.load(it) }
|
||||
val flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
|
||||
flutterSdkPath
|
||||
}
|
||||
|
||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||
id("com.android.application") version "8.11.1" apply false
|
||||
// START: FlutterFire Configuration
|
||||
id("com.google.gms.google-services") version("4.3.15") apply false
|
||||
// END: FlutterFire Configuration
|
||||
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
|
||||
}
|
||||
|
||||
include(":app")
|
||||
|
After Width: | Height: | Size: 97 KiB |
@@ -0,0 +1 @@
|
||||
{"flutter":{"platforms":{"android":{"default":{"projectId":"winded-app","appId":"1:611552481312:android:da656fe3aaad89db5aac45","fileOutput":"android/app/google-services.json"}},"dart":{"lib/firebase_options.dart":{"projectId":"winded-app","configurations":{"android":"1:611552481312:android:da656fe3aaad89db5aac45","ios":"1:611552481312:ios:2180c26facd38a915aac45","web":"1:611552481312:web:23ed1101037509755aac45"}}}}}}
|
||||
@@ -0,0 +1,34 @@
|
||||
**/dgph
|
||||
*.mode1v3
|
||||
*.mode2v3
|
||||
*.moved-aside
|
||||
*.pbxuser
|
||||
*.perspectivev3
|
||||
**/*sync/
|
||||
.sconsign.dblite
|
||||
.tags*
|
||||
**/.vagrant/
|
||||
**/DerivedData/
|
||||
Icon?
|
||||
**/Pods/
|
||||
**/.symlinks/
|
||||
profile
|
||||
xcuserdata
|
||||
**/.generated/
|
||||
Flutter/App.framework
|
||||
Flutter/Flutter.framework
|
||||
Flutter/Flutter.podspec
|
||||
Flutter/Generated.xcconfig
|
||||
Flutter/ephemeral/
|
||||
Flutter/app.flx
|
||||
Flutter/app.zip
|
||||
Flutter/flutter_assets/
|
||||
Flutter/flutter_export_environment.sh
|
||||
ServiceDefinitions.json
|
||||
Runner/GeneratedPluginRegistrant.*
|
||||
|
||||
# Exceptions to above rules.
|
||||
!default.mode1v3
|
||||
!default.mode2v3
|
||||
!default.pbxuser
|
||||
!default.perspectivev3
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>App</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>io.flutter.flutter.app</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>App</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1 @@
|
||||
#include "Generated.xcconfig"
|
||||
@@ -0,0 +1 @@
|
||||
#include "Generated.xcconfig"
|
||||
@@ -0,0 +1,620 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 54;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; };
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
|
||||
remoteInfo = Runner;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
331C8082294A63A400263BE5 /* RunnerTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
331C807B294A618700263BE5 /* RunnerTests.swift */,
|
||||
);
|
||||
path = RunnerTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */,
|
||||
);
|
||||
name = Flutter;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146E51CF9000F007C117D = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9740EEB11CF90186004384FC /* Flutter */,
|
||||
97C146F01CF9000F007C117D /* Runner */,
|
||||
97C146EF1CF9000F007C117D /* Products */,
|
||||
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146EF1CF9000F007C117D /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
97C146EE1CF9000F007C117D /* Runner.app */,
|
||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146F01CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
|
||||
97C147021CF9000F007C117D /* Info.plist */,
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
||||
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */,
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
||||
);
|
||||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
331C8080294A63A400263BE5 /* RunnerTests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||
buildPhases = (
|
||||
331C807D294A63A400263BE5 /* Sources */,
|
||||
331C807F294A63A400263BE5 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
331C8086294A63A400263BE5 /* PBXTargetDependency */,
|
||||
);
|
||||
name = RunnerTests;
|
||||
productName = RunnerTests;
|
||||
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
97C146ED1CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
9740EEB61CF901F6004384FC /* Run Script */,
|
||||
97C146EA1CF9000F007C117D /* Sources */,
|
||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||
97C146EC1CF9000F007C117D /* Resources */,
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = Runner;
|
||||
productName = Runner;
|
||||
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
97C146E61CF9000F007C117D /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastUpgradeCheck = 1510;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
331C8080294A63A400263BE5 = {
|
||||
CreatedOnToolsVersion = 14.0;
|
||||
TestTargetID = 97C146ED1CF9000F007C117D;
|
||||
};
|
||||
97C146ED1CF9000F007C117D = {
|
||||
CreatedOnToolsVersion = 7.3.1;
|
||||
LastSwiftMigration = 1100;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
|
||||
compatibilityVersion = "Xcode 9.3";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 97C146E51CF9000F007C117D;
|
||||
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
97C146ED1CF9000F007C117D /* Runner */,
|
||||
331C8080294A63A400263BE5 /* RunnerTests */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
331C807F294A63A400263BE5 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
97C146EC1CF9000F007C117D /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
|
||||
);
|
||||
name = "Thin Binary";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||
};
|
||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Run Script";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
331C807D294A63A400263BE5 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
97C146EA1CF9000F007C117D /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
||||
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 97C146ED1CF9000F007C117D /* Runner */;
|
||||
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
97C146FB1CF9000F007C117D /* Base */,
|
||||
);
|
||||
name = Main.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
97C147001CF9000F007C117D /* Base */,
|
||||
);
|
||||
name = LaunchScreen.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
249021D3217E4FDB00AE95B9 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
249021D4217E4FDB00AE95B9 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.winded.winded;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
331C8088294A63A400263BE5 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.winded.winded.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
331C8089294A63A400263BE5 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.winded.winded.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
331C808A294A63A400263BE5 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.winded.winded.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
97C147031CF9000F007C117D /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
97C147041CF9000F007C117D /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
97C147061CF9000F007C117D /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.winded.winded;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
97C147071CF9000F007C117D /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.winded.winded;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
331C8088294A63A400263BE5 /* Debug */,
|
||||
331C8089294A63A400263BE5 /* Release */,
|
||||
331C808A294A63A400263BE5 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
97C147031CF9000F007C117D /* Debug */,
|
||||
97C147041CF9000F007C117D /* Release */,
|
||||
249021D3217E4FDB00AE95B9 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
97C147061CF9000F007C117D /* Debug */,
|
||||
97C147071CF9000F007C117D /* Release */,
|
||||
249021D4217E4FDB00AE95B9 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PreviewsEnabled</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,101 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1510"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "331C8080294A63A400263BE5"
|
||||
BuildableName = "RunnerTests.xctest"
|
||||
BlueprintName = "RunnerTests"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
enableGPUValidationMode = "1"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Profile"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PreviewsEnabled</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,16 @@
|
||||
import Flutter
|
||||
import UIKit
|
||||
|
||||
@main
|
||||
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
|
||||
override func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
) -> Bool {
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
|
||||
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
|
||||
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-20x20@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-20x20@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-40x40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-40x40@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-60x60@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-60x60@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-20x20@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-20x20@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-29x29@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-29x29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-40x40@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-40x40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-76x76@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-76x76@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "83.5x83.5",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-83.5x83.5@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "1024x1024",
|
||||
"idiom" : "ios-marketing",
|
||||
"filename" : "Icon-App-1024x1024@1x.png",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 295 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 450 B |
|
After Width: | Height: | Size: 282 B |
|
After Width: | Height: | Size: 462 B |
|
After Width: | Height: | Size: 704 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 586 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 762 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 68 B |
|
After Width: | Height: | Size: 68 B |
|
After Width: | Height: | Size: 68 B |
@@ -0,0 +1,5 @@
|
||||
# Launch Screen Assets
|
||||
|
||||
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
|
||||
|
||||
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
|
||||
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="LaunchImage" width="168" height="185"/>
|
||||
</resources>
|
||||
</document>
|
||||
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Flutter View Controller-->
|
||||
<scene sceneID="tne-QT-ifu">
|
||||
<objects>
|
||||
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
@@ -0,0 +1,70 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Winded</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>winded</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<false/>
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict>
|
||||
<key>UIWindowSceneSessionRoleApplication</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UISceneClassName</key>
|
||||
<string>UIWindowScene</string>
|
||||
<key>UISceneConfigurationName</key>
|
||||
<string>flutter</string>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
|
||||
<key>UISceneStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1 @@
|
||||
#import "GeneratedPluginRegistrant.h"
|
||||
@@ -0,0 +1,6 @@
|
||||
import Flutter
|
||||
import UIKit
|
||||
|
||||
class SceneDelegate: FlutterSceneDelegate {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import Flutter
|
||||
import UIKit
|
||||
import XCTest
|
||||
|
||||
class RunnerTests: XCTestCase {
|
||||
|
||||
func testExample() {
|
||||
// If you add code to the Runner application, consider adding tests here.
|
||||
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import '../../features/auth/domain/app_user.dart';
|
||||
import '../../features/profile/domain/user_profile.dart';
|
||||
|
||||
/// Hardcoded admin allow-list for the MVP. Email match is the primary signal
|
||||
/// because the hardcoded admin doesn't carry a Firestore role document.
|
||||
const Set<String> _adminEmails = <String>{'philip@theguzmanfamily.com'};
|
||||
|
||||
/// Returns true if [user] is on the email allow-list. Primary entry point —
|
||||
/// most callers only have an [AppUser] in hand.
|
||||
bool isAdmin(AppUser? user) {
|
||||
if (user == null) return false;
|
||||
final email = user.email.trim().toLowerCase();
|
||||
if (email.isEmpty) return false;
|
||||
return _adminEmails.contains(email);
|
||||
}
|
||||
|
||||
/// Returns true when admin status is established either by email allow-list
|
||||
/// or by a Firestore profile document carrying [UserRole.admin]. Use this in
|
||||
/// places where a profile is already loaded so a future Firestore-driven
|
||||
/// admin grant works without a code change.
|
||||
bool isAdminWithProfile(AppUser? user, UserProfile? profile) {
|
||||
if (isAdmin(user)) return true;
|
||||
return profile?.role == UserRole.admin;
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'api_client.g.dart';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Configuration — set this to your Hostinger domain before building.
|
||||
// ---------------------------------------------------------------------------
|
||||
const String kApiBase = 'https://winded.prymsolutions.com/api';
|
||||
|
||||
const String _tokenKey = 'winded_auth_token';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ApiClient
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class ApiClient {
|
||||
ApiClient(this._storage);
|
||||
|
||||
final FlutterSecureStorage _storage;
|
||||
|
||||
// --- Token management ---
|
||||
|
||||
Future<String?> get token => _storage.read(key: _tokenKey);
|
||||
|
||||
Future<void> saveToken(String token) =>
|
||||
_storage.write(key: _tokenKey, value: token);
|
||||
|
||||
Future<void> clearToken() => _storage.delete(key: _tokenKey);
|
||||
|
||||
// --- HTTP helpers ---
|
||||
|
||||
Future<Map<String, String>> _headers({bool auth = true}) async {
|
||||
final headers = <String, String>{'Content-Type': 'application/json'};
|
||||
if (auth) {
|
||||
final t = await token;
|
||||
if (t != null) headers['Authorization'] = 'Bearer $t';
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
Uri _uri(String path, [Map<String, String>? params]) {
|
||||
final uri = Uri.parse('$kApiBase$path');
|
||||
return params != null ? uri.replace(queryParameters: params) : uri;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> get(
|
||||
String path, {
|
||||
Map<String, String>? params,
|
||||
bool auth = true,
|
||||
}) async {
|
||||
final res = await http.get(_uri(path, params), headers: await _headers(auth: auth));
|
||||
return _parse(res);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> post(
|
||||
String path,
|
||||
Map<String, dynamic> body, {
|
||||
bool auth = true,
|
||||
}) async {
|
||||
final res = await http.post(
|
||||
_uri(path),
|
||||
headers: await _headers(auth: auth),
|
||||
body: jsonEncode(body),
|
||||
);
|
||||
return _parse(res);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> put(
|
||||
String path,
|
||||
Map<String, dynamic> body, {
|
||||
Map<String, String>? params,
|
||||
}) async {
|
||||
final res = await http.put(
|
||||
_uri(path, params),
|
||||
headers: await _headers(),
|
||||
body: jsonEncode(body),
|
||||
);
|
||||
return _parse(res);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> delete(
|
||||
String path, {
|
||||
Map<String, String>? params,
|
||||
}) async {
|
||||
final res = await http.delete(_uri(path, params), headers: await _headers());
|
||||
return _parse(res);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _parse(http.Response res) {
|
||||
final body = jsonDecode(res.body) as Map<String, dynamic>;
|
||||
if (res.statusCode >= 400) {
|
||||
throw ApiException(
|
||||
message: (body['error'] as String?) ?? 'Unknown error',
|
||||
statusCode: res.statusCode,
|
||||
);
|
||||
}
|
||||
return body;
|
||||
}
|
||||
}
|
||||
|
||||
class ApiException implements Exception {
|
||||
const ApiException({required this.message, required this.statusCode});
|
||||
final String message;
|
||||
final int statusCode;
|
||||
|
||||
@override
|
||||
String toString() => 'ApiException($statusCode): $message';
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Providers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
ApiClient apiClient(ApiClientRef ref) {
|
||||
return ApiClient(const FlutterSecureStorage());
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package, deprecated_member_use
|
||||
|
||||
part of 'api_client.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$apiClientHash() => r'api_client_hash_placeholder';
|
||||
|
||||
/// See also [apiClient].
|
||||
@ProviderFor(apiClient)
|
||||
final apiClientProvider = Provider<ApiClient>.internal(
|
||||
apiClient,
|
||||
name: r'apiClientProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$apiClientHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef ApiClientRef = ProviderRef<ApiClient>;
|
||||
@@ -0,0 +1,245 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../features/admin/presentation/admin_shell.dart';
|
||||
import '../../features/admin/presentation/brackets/admin_bracket_form_screen.dart';
|
||||
import '../../features/admin/presentation/brackets/admin_brackets_screen.dart';
|
||||
import '../../features/admin/presentation/events/admin_event_form_screen.dart';
|
||||
import '../../features/admin/presentation/events/admin_events_screen.dart';
|
||||
import '../../features/admin/presentation/pending/admin_pending_screen.dart';
|
||||
import '../../features/admin/presentation/suggestions/admin_suggestions_screen.dart';
|
||||
import '../../features/admin/presentation/teams/admin_team_form_screen.dart';
|
||||
import '../../features/admin/presentation/teams/admin_teams_screen.dart';
|
||||
import '../../features/auth/application/auth_notifier.dart';
|
||||
import '../../features/auth/presentation/login_screen.dart';
|
||||
import '../../features/auth/presentation/register_screen.dart';
|
||||
import '../../features/brackets/presentation/bracket_detail_screen.dart';
|
||||
import '../../features/brackets/presentation/brackets_screen.dart';
|
||||
import '../../features/events/presentation/event_detail_screen.dart';
|
||||
import '../../features/events/presentation/events_screen.dart';
|
||||
import '../../features/media/presentation/media_screen.dart';
|
||||
import '../../features/profile/application/profile_notifier.dart';
|
||||
import '../../features/profile/domain/user_profile.dart';
|
||||
import '../../features/profile/presentation/manager_dashboard_screen.dart';
|
||||
import '../../features/profile/presentation/my_profile_screen.dart';
|
||||
import '../../features/profile/presentation/player_profile_screen.dart';
|
||||
import '../../features/stats/presentation/stats_screen.dart';
|
||||
import '../../features/suggestions/presentation/suggestions_screen.dart';
|
||||
import '../../features/teams/presentation/create_team_screen.dart';
|
||||
import '../../features/teams/presentation/team_detail_screen.dart';
|
||||
import '../../features/teams/presentation/teams_screen.dart';
|
||||
import '../admin/admin_guard.dart';
|
||||
import '../shell/main_shell.dart';
|
||||
|
||||
/// Routes that an unauthenticated user is allowed to visit. Anything else
|
||||
/// triggers a redirect to `/login`. Player profile pages stay reachable to
|
||||
/// signed-out viewers so shared links work.
|
||||
const _publicRoutes = {'/login', '/register'};
|
||||
|
||||
/// Path prefixes that anonymous viewers may visit even without a session.
|
||||
/// Player profile pages are intentionally readable so a roster link shared
|
||||
/// outside the app still works.
|
||||
const _viewerPrefixes = <String>['/players/'];
|
||||
|
||||
final appRouterProvider = Provider<GoRouter>((ref) {
|
||||
// GoRouter listens to this notifier and re-evaluates `redirect` whenever
|
||||
// it fires — we ping it on every auth state change.
|
||||
final refresh = _AuthRouterRefresh(ref);
|
||||
ref.onDispose(refresh.dispose);
|
||||
|
||||
return GoRouter(
|
||||
initialLocation: '/events',
|
||||
refreshListenable: refresh,
|
||||
redirect: (context, state) {
|
||||
final auth = ref.read(authNotifierProvider);
|
||||
|
||||
// Don't redirect while the initial auth check is still loading —
|
||||
// GoRouter will re-run this once the notifier has data.
|
||||
if (auth.isLoading || !auth.hasValue) return null;
|
||||
|
||||
final user = auth.value;
|
||||
final location = state.matchedLocation;
|
||||
final isPublic = _publicRoutes.contains(location);
|
||||
final isViewerOk = _viewerPrefixes.any(location.startsWith);
|
||||
final isAdminRoute = location.startsWith('/admin');
|
||||
final isManagerRoute = location == '/manager';
|
||||
|
||||
if (user == null && !isPublic && !isViewerOk) {
|
||||
return '/login';
|
||||
}
|
||||
if (user != null && isPublic) {
|
||||
return '/events';
|
||||
}
|
||||
if (isAdminRoute && !isAdmin(user)) {
|
||||
return '/events';
|
||||
}
|
||||
if (isManagerRoute) {
|
||||
// Manager dashboard is reserved for users with the manager role
|
||||
// (admins also fall through — they have their own panel).
|
||||
final role = ref.read(currentUserRoleProvider);
|
||||
if (role != UserRole.manager) {
|
||||
return '/events';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
routes: [
|
||||
GoRoute(path: '/login', builder: (context, state) => const LoginScreen()),
|
||||
GoRoute(
|
||||
path: '/register',
|
||||
builder: (context, state) => const RegisterScreen(),
|
||||
),
|
||||
ShellRoute(
|
||||
builder: (context, state, child) => MainShell(child: child),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/events',
|
||||
builder: (context, state) => const EventsScreen(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: ':id',
|
||||
builder: (context, state) =>
|
||||
EventDetailScreen(eventId: state.pathParameters['id']!),
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: '/brackets',
|
||||
builder: (context, state) => const BracketsScreen(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: ':id',
|
||||
builder: (context, state) =>
|
||||
BracketDetailScreen(bracketId: state.pathParameters['id']!),
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: '/teams',
|
||||
builder: (context, state) => const TeamsScreen(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'new',
|
||||
builder: (context, state) => const CreateTeamScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: ':id',
|
||||
builder: (context, state) =>
|
||||
TeamDetailScreen(teamId: state.pathParameters['id']!),
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: '/stats',
|
||||
builder: (context, state) => const StatsScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/media',
|
||||
builder: (context, state) => const MediaScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/suggestions',
|
||||
builder: (context, state) => const SuggestionsScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/profile',
|
||||
builder: (context, state) => const MyProfileScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/players/:uid',
|
||||
builder: (context, state) =>
|
||||
PlayerProfileScreen(uid: state.pathParameters['uid']!),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/manager',
|
||||
builder: (context, state) => const ManagerDashboardScreen(),
|
||||
),
|
||||
],
|
||||
),
|
||||
ShellRoute(
|
||||
builder: (context, state, child) => AdminShell(child: child),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/admin/events',
|
||||
builder: (context, state) => const AdminEventsScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/admin/events/new',
|
||||
builder: (context, state) => const AdminEventFormScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/admin/events/:id/edit',
|
||||
builder: (context, state) =>
|
||||
AdminEventFormScreen(eventId: state.pathParameters['id']),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/admin/teams',
|
||||
builder: (context, state) => const AdminTeamsScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/admin/teams/new',
|
||||
builder: (context, state) => const AdminTeamFormScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/admin/teams/:id/edit',
|
||||
builder: (context, state) =>
|
||||
AdminTeamFormScreen(teamId: state.pathParameters['id']),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/admin/brackets',
|
||||
builder: (context, state) => const AdminBracketsScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/admin/brackets/new',
|
||||
builder: (context, state) => const AdminBracketFormScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/admin/brackets/:id/edit',
|
||||
builder: (context, state) =>
|
||||
AdminBracketFormScreen(bracketId: state.pathParameters['id']),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/admin/suggestions',
|
||||
builder: (context, state) => const AdminSuggestionsScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/admin/pending',
|
||||
builder: (context, state) => const AdminPendingScreen(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
/// Bridges the Riverpod auth notifier (and the derived role provider) to a
|
||||
/// [ChangeNotifier] that GoRouter can subscribe to via `refreshListenable`.
|
||||
/// Sign-in/out and role changes both ping GoRouter to re-run `redirect`.
|
||||
class _AuthRouterRefresh extends ChangeNotifier {
|
||||
_AuthRouterRefresh(this._ref) {
|
||||
_authSub = _ref.listen<AsyncValue>(
|
||||
authNotifierProvider,
|
||||
(prev, next) => notifyListeners(),
|
||||
fireImmediately: false,
|
||||
);
|
||||
_roleSub = _ref.listen<UserRole>(
|
||||
currentUserRoleProvider,
|
||||
(prev, next) {
|
||||
if (prev != next) notifyListeners();
|
||||
},
|
||||
fireImmediately: false,
|
||||
);
|
||||
}
|
||||
|
||||
final Ref _ref;
|
||||
late final ProviderSubscription<AsyncValue> _authSub;
|
||||
late final ProviderSubscription<UserRole> _roleSub;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_authSub.close();
|
||||
_roleSub.close();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../features/auth/application/auth_notifier.dart';
|
||||
import '../../features/profile/application/profile_notifier.dart';
|
||||
import '../../features/profile/domain/user_profile.dart';
|
||||
import '../admin/admin_guard.dart';
|
||||
|
||||
/// Root shell holding the bottom navigation bar that switches between the
|
||||
/// six top-level tabs of the app. Labels are uppercase to match the
|
||||
/// aggressive, jersey-inspired visual language; a thin purple accent line
|
||||
/// sits above the nav bar for an edgy soccer-badge feel.
|
||||
///
|
||||
/// Admins (per [isAdmin]) see a small gear button overlaid in the top-right
|
||||
/// of the AppBar band that links into the admin panel.
|
||||
class MainShell extends ConsumerWidget {
|
||||
const MainShell({super.key, required this.child});
|
||||
|
||||
final Widget child;
|
||||
|
||||
static const _tabs = [
|
||||
(
|
||||
label: 'EVENTS',
|
||||
icon: Icons.event_outlined,
|
||||
activeIcon: Icons.event,
|
||||
path: '/events',
|
||||
),
|
||||
(
|
||||
label: 'BRACKETS',
|
||||
icon: Icons.account_tree_outlined,
|
||||
activeIcon: Icons.account_tree,
|
||||
path: '/brackets',
|
||||
),
|
||||
(
|
||||
label: 'TEAMS',
|
||||
icon: Icons.groups_outlined,
|
||||
activeIcon: Icons.groups,
|
||||
path: '/teams',
|
||||
),
|
||||
(
|
||||
label: 'STATS',
|
||||
icon: Icons.bar_chart_outlined,
|
||||
activeIcon: Icons.bar_chart,
|
||||
path: '/stats',
|
||||
),
|
||||
(
|
||||
label: 'MEDIA',
|
||||
icon: Icons.play_circle_outline,
|
||||
activeIcon: Icons.play_circle,
|
||||
path: '/media',
|
||||
),
|
||||
(
|
||||
label: 'SUGGEST',
|
||||
icon: Icons.lightbulb_outline,
|
||||
activeIcon: Icons.lightbulb,
|
||||
path: '/suggestions',
|
||||
),
|
||||
];
|
||||
|
||||
int _currentIndex(BuildContext context) {
|
||||
final location = GoRouterState.of(context).uri.path;
|
||||
final idx = _tabs.indexWhere((t) => location.startsWith(t.path));
|
||||
return idx < 0 ? 0 : idx;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final currentIndex = _currentIndex(context);
|
||||
final user = ref.watch(authNotifierProvider).valueOrNull;
|
||||
final showAdmin = isAdmin(user);
|
||||
final role = ref.watch(currentUserRoleProvider);
|
||||
final showProfile = user != null;
|
||||
final showManager = role == UserRole.manager;
|
||||
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: <Widget>[
|
||||
Positioned.fill(child: child),
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: SafeArea(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
if (showManager)
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.shield_outlined),
|
||||
tooltip: 'Manager dashboard',
|
||||
onPressed: () => context.go('/manager'),
|
||||
),
|
||||
),
|
||||
if (showAdmin)
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.settings_outlined),
|
||||
tooltip: 'Admin panel',
|
||||
onPressed: () => context.go('/admin/events'),
|
||||
),
|
||||
),
|
||||
if (showProfile)
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.person_outline),
|
||||
tooltip: 'My profile',
|
||||
onPressed: () => context.go('/profile'),
|
||||
),
|
||||
),
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
child: PopupMenuButton<_UserMenuAction>(
|
||||
icon: const Icon(Icons.account_circle_outlined),
|
||||
tooltip: 'Account',
|
||||
onSelected: (action) async {
|
||||
if (action == _UserMenuAction.signOut) {
|
||||
await ref.read(authNotifierProvider.notifier).signOut();
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => <PopupMenuEntry<_UserMenuAction>>[
|
||||
PopupMenuItem<_UserMenuAction>(
|
||||
enabled: false,
|
||||
child: Text(
|
||||
user?.email ?? '',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
const PopupMenuDivider(),
|
||||
const PopupMenuItem<_UserMenuAction>(
|
||||
value: _UserMenuAction.signOut,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Icon(Icons.logout, size: 18),
|
||||
SizedBox(width: 8),
|
||||
Text('Sign out'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Sharp purple accent line above the nav bar for an edgy look.
|
||||
Container(height: 1, color: const Color(0xFF8B30C8)),
|
||||
NavigationBar(
|
||||
selectedIndex: currentIndex,
|
||||
onDestinationSelected: (i) => context.go(_tabs[i].path),
|
||||
destinations: _tabs
|
||||
.map(
|
||||
(t) => NavigationDestination(
|
||||
icon: Icon(t.icon),
|
||||
selectedIcon: Icon(t.activeIcon),
|
||||
label: t.label,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum _UserMenuAction { signOut }
|
||||
@@ -0,0 +1,157 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Aggressive, high-contrast Shadow Oak theme.
|
||||
///
|
||||
/// Visual direction: urban soccer / techy / grunge badge style.
|
||||
/// Sharp corners, deep purple primary, near-black surfaces, heavy weight
|
||||
/// type with negative tracking on headings and uppercase tracking on labels.
|
||||
class AppTheme {
|
||||
AppTheme._();
|
||||
|
||||
static const purple = Color(0xFF8B30C8);
|
||||
static const purpleLight = Color(0xFFBF77F6);
|
||||
static const black = Color(0xFF0A0A0A);
|
||||
static const surface = Color(0xFF141414);
|
||||
static const surfaceVariant = Color(0xFF1E1E1E);
|
||||
|
||||
static final dark = ThemeData(
|
||||
useMaterial3: true,
|
||||
brightness: Brightness.dark,
|
||||
scaffoldBackgroundColor: black,
|
||||
colorScheme: const ColorScheme.dark(
|
||||
primary: purple,
|
||||
onPrimary: Colors.white,
|
||||
primaryContainer: Color(0xFF3D1260),
|
||||
onPrimaryContainer: purpleLight,
|
||||
secondary: purpleLight,
|
||||
onSecondary: Colors.black,
|
||||
secondaryContainer: Color(0xFF2A1045),
|
||||
onSecondaryContainer: purpleLight,
|
||||
surface: surface,
|
||||
onSurface: Colors.white,
|
||||
surfaceContainerHighest: surfaceVariant,
|
||||
onSurfaceVariant: Color(0xFFBBBBBB),
|
||||
outline: Color(0xFF444444),
|
||||
outlineVariant: Color(0xFF2A2A2A),
|
||||
error: Color(0xFFF44336),
|
||||
onError: Colors.white,
|
||||
),
|
||||
// Sharp shapes throughout
|
||||
cardTheme: CardThemeData(
|
||||
color: surface,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
side: const BorderSide(color: Color(0xFF2A2A2A)),
|
||||
),
|
||||
margin: EdgeInsets.zero,
|
||||
),
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: Color(0xFF0A0A0A),
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
centerTitle: false,
|
||||
titleTextStyle: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w900,
|
||||
letterSpacing: 1.5,
|
||||
),
|
||||
surfaceTintColor: Colors.transparent,
|
||||
),
|
||||
navigationBarTheme: NavigationBarThemeData(
|
||||
backgroundColor: surface,
|
||||
indicatorColor: const Color(0xFF3D1260),
|
||||
labelTextStyle: WidgetStateProperty.resolveWith((states) {
|
||||
final selected = states.contains(WidgetState.selected);
|
||||
return TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.5,
|
||||
color: selected ? purpleLight : const Color(0xFF888888),
|
||||
);
|
||||
}),
|
||||
iconTheme: WidgetStateProperty.resolveWith((states) {
|
||||
final selected = states.contains(WidgetState.selected);
|
||||
return IconThemeData(
|
||||
color: selected ? purpleLight : const Color(0xFF888888),
|
||||
size: 22,
|
||||
);
|
||||
}),
|
||||
height: 64,
|
||||
elevation: 0,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
shadowColor: Colors.transparent,
|
||||
),
|
||||
filledButtonTheme: FilledButtonThemeData(
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: purple,
|
||||
foregroundColor: Colors.white,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(4)),
|
||||
),
|
||||
textStyle: const TextStyle(
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24),
|
||||
),
|
||||
),
|
||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: purpleLight,
|
||||
side: const BorderSide(color: Color(0xFF8B30C8)),
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(4)),
|
||||
),
|
||||
textStyle: const TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 1.0,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 20),
|
||||
),
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: surfaceVariant,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
borderSide: const BorderSide(color: Color(0xFF444444)),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
borderSide: const BorderSide(color: Color(0xFF555555)),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
borderSide: const BorderSide(color: purple, width: 2),
|
||||
),
|
||||
labelStyle: const TextStyle(color: Color(0xFF888888)),
|
||||
hintStyle: const TextStyle(color: Color(0xFF555555)),
|
||||
),
|
||||
chipTheme: ChipThemeData(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(2)),
|
||||
side: BorderSide.none,
|
||||
),
|
||||
dividerTheme: const DividerThemeData(
|
||||
color: Color(0xFF1E1E1E),
|
||||
thickness: 1,
|
||||
),
|
||||
textTheme: const TextTheme(
|
||||
displayLarge: TextStyle(fontWeight: FontWeight.w900, letterSpacing: -1.5),
|
||||
displayMedium: TextStyle(fontWeight: FontWeight.w900, letterSpacing: -1.0),
|
||||
displaySmall: TextStyle(fontWeight: FontWeight.w800, letterSpacing: -0.5),
|
||||
headlineLarge: TextStyle(fontWeight: FontWeight.w800),
|
||||
headlineMedium: TextStyle(fontWeight: FontWeight.w800),
|
||||
headlineSmall: TextStyle(fontWeight: FontWeight.w700),
|
||||
titleLarge: TextStyle(fontWeight: FontWeight.w700),
|
||||
titleMedium: TextStyle(fontWeight: FontWeight.w600, letterSpacing: 0.5),
|
||||
titleSmall: TextStyle(fontWeight: FontWeight.w600, letterSpacing: 0.8),
|
||||
labelLarge: TextStyle(fontWeight: FontWeight.w700, letterSpacing: 1.2),
|
||||
labelMedium: TextStyle(fontWeight: FontWeight.w600, letterSpacing: 0.8),
|
||||
labelSmall: TextStyle(fontWeight: FontWeight.w600, letterSpacing: 1.0),
|
||||
),
|
||||
);
|
||||
|
||||
static final light = dark; // dark-only app for now
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../../brackets/domain/bracket.dart';
|
||||
import '../../brackets/infrastructure/brackets_repository.dart';
|
||||
|
||||
part 'admin_brackets_notifier.g.dart';
|
||||
|
||||
/// Live Firestore-backed stream of every bracket, used by the admin panel.
|
||||
@riverpod
|
||||
Stream<List<Bracket>> adminBracketsStream(AdminBracketsStreamRef ref) {
|
||||
final repo = ref.watch(bracketsRepositoryProvider);
|
||||
return repo.watchBrackets();
|
||||
}
|
||||
|
||||
/// Imperative wrapper around the brackets repository write methods.
|
||||
@riverpod
|
||||
class AdminBracketsNotifier extends _$AdminBracketsNotifier {
|
||||
@override
|
||||
Future<void> build() async {}
|
||||
|
||||
Future<String> create(Bracket bracket) async {
|
||||
final repo = ref.read(bracketsRepositoryProvider);
|
||||
state = const AsyncLoading();
|
||||
try {
|
||||
final id = await repo.createBracket(bracket);
|
||||
state = const AsyncData(null);
|
||||
return id;
|
||||
} catch (e, st) {
|
||||
state = AsyncError(e, st);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> save(Bracket bracket) async {
|
||||
final repo = ref.read(bracketsRepositoryProvider);
|
||||
state = const AsyncLoading();
|
||||
state = await AsyncValue.guard(() => repo.updateBracket(bracket));
|
||||
}
|
||||
|
||||
Future<void> delete(String id) async {
|
||||
final repo = ref.read(bracketsRepositoryProvider);
|
||||
state = const AsyncLoading();
|
||||
state = await AsyncValue.guard(() => repo.deleteBracket(id));
|
||||
}
|
||||
|
||||
Future<void> updateMatch(
|
||||
String bracketId,
|
||||
String roundLabel,
|
||||
BracketMatch match,
|
||||
) async {
|
||||
final repo = ref.read(bracketsRepositoryProvider);
|
||||
state = const AsyncLoading();
|
||||
state = await AsyncValue.guard(
|
||||
() => repo.updateMatch(bracketId, roundLabel, match),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'admin_brackets_notifier.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$adminBracketsStreamHash() =>
|
||||
r'2a76ca85dc76fc7514b7b9ae17a5610f1c1760d9';
|
||||
|
||||
/// Live Firestore-backed stream of every bracket, used by the admin panel.
|
||||
///
|
||||
/// Copied from [adminBracketsStream].
|
||||
@ProviderFor(adminBracketsStream)
|
||||
final adminBracketsStreamProvider =
|
||||
AutoDisposeStreamProvider<List<Bracket>>.internal(
|
||||
adminBracketsStream,
|
||||
name: r'adminBracketsStreamProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$adminBracketsStreamHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef AdminBracketsStreamRef = AutoDisposeStreamProviderRef<List<Bracket>>;
|
||||
String _$adminBracketsNotifierHash() =>
|
||||
r'ac2ba11f3c44e7feccf440538249e078c9a55031';
|
||||
|
||||
/// Imperative wrapper around the brackets repository write methods.
|
||||
///
|
||||
/// Copied from [AdminBracketsNotifier].
|
||||
@ProviderFor(AdminBracketsNotifier)
|
||||
final adminBracketsNotifierProvider =
|
||||
AutoDisposeAsyncNotifierProvider<AdminBracketsNotifier, void>.internal(
|
||||
AdminBracketsNotifier.new,
|
||||
name: r'adminBracketsNotifierProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$adminBracketsNotifierHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$AdminBracketsNotifier = AutoDisposeAsyncNotifier<void>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||
@@ -0,0 +1,49 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../../events/domain/event.dart';
|
||||
import '../../events/infrastructure/events_repository.dart';
|
||||
|
||||
part 'admin_events_notifier.g.dart';
|
||||
|
||||
/// Live Firestore-backed stream of every event in the system, used by the
|
||||
/// admin panel. The public-facing [eventsStreamProvider] still emits mocked
|
||||
/// data; admins read straight through to the real collection.
|
||||
@riverpod
|
||||
Stream<List<Event>> adminEventsStream(AdminEventsStreamRef ref) {
|
||||
final repo = ref.watch(eventsRepositoryProvider);
|
||||
return repo.watchEvents();
|
||||
}
|
||||
|
||||
/// Imperative wrapper around the events repository write methods. The notifier
|
||||
/// is `AsyncValue<void>`-shaped so screens can wire it up the same way as the
|
||||
/// existing auth/suggestions notifiers.
|
||||
@riverpod
|
||||
class AdminEventsNotifier extends _$AdminEventsNotifier {
|
||||
@override
|
||||
Future<void> build() async {}
|
||||
|
||||
Future<String> create(Event event) async {
|
||||
final repo = ref.read(eventsRepositoryProvider);
|
||||
state = const AsyncLoading();
|
||||
try {
|
||||
final id = await repo.createEvent(event);
|
||||
state = const AsyncData(null);
|
||||
return id;
|
||||
} catch (e, st) {
|
||||
state = AsyncError(e, st);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> save(Event event) async {
|
||||
final repo = ref.read(eventsRepositoryProvider);
|
||||
state = const AsyncLoading();
|
||||
state = await AsyncValue.guard(() => repo.updateEvent(event));
|
||||
}
|
||||
|
||||
Future<void> delete(String id) async {
|
||||
final repo = ref.read(eventsRepositoryProvider);
|
||||
state = const AsyncLoading();
|
||||
state = await AsyncValue.guard(() => repo.deleteEvent(id));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'admin_events_notifier.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$adminEventsStreamHash() => r'33d6cd9ec02f788540270db08f0933e9c46c72e8';
|
||||
|
||||
/// Live Firestore-backed stream of every event in the system, used by the
|
||||
/// admin panel. The public-facing [eventsStreamProvider] still emits mocked
|
||||
/// data; admins read straight through to the real collection.
|
||||
///
|
||||
/// Copied from [adminEventsStream].
|
||||
@ProviderFor(adminEventsStream)
|
||||
final adminEventsStreamProvider =
|
||||
AutoDisposeStreamProvider<List<Event>>.internal(
|
||||
adminEventsStream,
|
||||
name: r'adminEventsStreamProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$adminEventsStreamHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef AdminEventsStreamRef = AutoDisposeStreamProviderRef<List<Event>>;
|
||||
String _$adminEventsNotifierHash() =>
|
||||
r'd39031c4b14120bba5d4ea0baeed2661eb336ec0';
|
||||
|
||||
/// Imperative wrapper around the events repository write methods. The notifier
|
||||
/// is `AsyncValue<void>`-shaped so screens can wire it up the same way as the
|
||||
/// existing auth/suggestions notifiers.
|
||||
///
|
||||
/// Copied from [AdminEventsNotifier].
|
||||
@ProviderFor(AdminEventsNotifier)
|
||||
final adminEventsNotifierProvider =
|
||||
AutoDisposeAsyncNotifierProvider<AdminEventsNotifier, void>.internal(
|
||||
AdminEventsNotifier.new,
|
||||
name: r'adminEventsNotifierProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$adminEventsNotifierHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$AdminEventsNotifier = AutoDisposeAsyncNotifier<void>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||
@@ -0,0 +1,35 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../../suggestions/domain/suggestion.dart';
|
||||
import '../../suggestions/infrastructure/suggestions_repository.dart';
|
||||
|
||||
part 'admin_suggestions_notifier.g.dart';
|
||||
|
||||
/// Live Firestore-backed stream of every suggestion, newest first, for the
|
||||
/// admin review dashboard.
|
||||
@riverpod
|
||||
Stream<List<Suggestion>> adminSuggestionsStream(
|
||||
AdminSuggestionsStreamRef ref,
|
||||
) {
|
||||
final repo = ref.watch(suggestionsRepositoryProvider);
|
||||
return repo.watchAllSuggestions();
|
||||
}
|
||||
|
||||
/// Imperative wrapper around the suggestion write methods.
|
||||
@riverpod
|
||||
class AdminSuggestionsNotifier extends _$AdminSuggestionsNotifier {
|
||||
@override
|
||||
Future<void> build() async {}
|
||||
|
||||
Future<void> updateStatus(String id, SuggestionStatus status) async {
|
||||
final repo = ref.read(suggestionsRepositoryProvider);
|
||||
state = const AsyncLoading();
|
||||
state = await AsyncValue.guard(() => repo.updateStatus(id, status));
|
||||
}
|
||||
|
||||
Future<void> delete(String id) async {
|
||||
final repo = ref.read(suggestionsRepositoryProvider);
|
||||
state = const AsyncLoading();
|
||||
state = await AsyncValue.guard(() => repo.deleteSuggestion(id));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'admin_suggestions_notifier.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$adminSuggestionsStreamHash() =>
|
||||
r'e87ca116c64b03bf5e62df4c390ff5c3dcfb4e0a';
|
||||
|
||||
/// Live Firestore-backed stream of every suggestion, newest first, for the
|
||||
/// admin review dashboard.
|
||||
///
|
||||
/// Copied from [adminSuggestionsStream].
|
||||
@ProviderFor(adminSuggestionsStream)
|
||||
final adminSuggestionsStreamProvider =
|
||||
AutoDisposeStreamProvider<List<Suggestion>>.internal(
|
||||
adminSuggestionsStream,
|
||||
name: r'adminSuggestionsStreamProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$adminSuggestionsStreamHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef AdminSuggestionsStreamRef =
|
||||
AutoDisposeStreamProviderRef<List<Suggestion>>;
|
||||
String _$adminSuggestionsNotifierHash() =>
|
||||
r'fd85d538be1e2d9abad02812d9c964c2df2b547a';
|
||||
|
||||
/// Imperative wrapper around the suggestion write methods.
|
||||
///
|
||||
/// Copied from [AdminSuggestionsNotifier].
|
||||
@ProviderFor(AdminSuggestionsNotifier)
|
||||
final adminSuggestionsNotifierProvider =
|
||||
AutoDisposeAsyncNotifierProvider<AdminSuggestionsNotifier, void>.internal(
|
||||
AdminSuggestionsNotifier.new,
|
||||
name: r'adminSuggestionsNotifierProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$adminSuggestionsNotifierHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$AdminSuggestionsNotifier = AutoDisposeAsyncNotifier<void>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||
@@ -0,0 +1,46 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../../teams/domain/team.dart';
|
||||
import '../../teams/infrastructure/teams_repository.dart';
|
||||
|
||||
part 'admin_teams_notifier.g.dart';
|
||||
|
||||
/// Live Firestore-backed stream of every team (including pending and
|
||||
/// rejected), used by the admin panel.
|
||||
@riverpod
|
||||
Stream<List<Team>> adminTeamsStream(AdminTeamsStreamRef ref) {
|
||||
final repo = ref.watch(teamsRepositoryProvider);
|
||||
return repo.adminWatchAllTeams();
|
||||
}
|
||||
|
||||
/// Imperative wrapper around the teams repository write methods.
|
||||
@riverpod
|
||||
class AdminTeamsNotifier extends _$AdminTeamsNotifier {
|
||||
@override
|
||||
Future<void> build() async {}
|
||||
|
||||
Future<String> create(Team team) async {
|
||||
final repo = ref.read(teamsRepositoryProvider);
|
||||
state = const AsyncLoading();
|
||||
try {
|
||||
final id = await repo.createTeam(team);
|
||||
state = const AsyncData(null);
|
||||
return id;
|
||||
} catch (e, st) {
|
||||
state = AsyncError(e, st);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> save(Team team) async {
|
||||
final repo = ref.read(teamsRepositoryProvider);
|
||||
state = const AsyncLoading();
|
||||
state = await AsyncValue.guard(() => repo.updateTeam(team));
|
||||
}
|
||||
|
||||
Future<void> delete(String id) async {
|
||||
final repo = ref.read(teamsRepositoryProvider);
|
||||
state = const AsyncLoading();
|
||||
state = await AsyncValue.guard(() => repo.deleteTeam(id));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'admin_teams_notifier.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$adminTeamsStreamHash() => r'f392e2c9de281c80912d4fccfaf56c0cbe8ef880';
|
||||
|
||||
/// Live Firestore-backed stream of every team (including pending and
|
||||
/// rejected), used by the admin panel.
|
||||
///
|
||||
/// Copied from [adminTeamsStream].
|
||||
@ProviderFor(adminTeamsStream)
|
||||
final adminTeamsStreamProvider = AutoDisposeStreamProvider<List<Team>>.internal(
|
||||
adminTeamsStream,
|
||||
name: r'adminTeamsStreamProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$adminTeamsStreamHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef AdminTeamsStreamRef = AutoDisposeStreamProviderRef<List<Team>>;
|
||||
String _$adminTeamsNotifierHash() =>
|
||||
r'1f5febaa0f2eb35596538db76896c96dd240a1d8';
|
||||
|
||||
/// Imperative wrapper around the teams repository write methods.
|
||||
///
|
||||
/// Copied from [AdminTeamsNotifier].
|
||||
@ProviderFor(AdminTeamsNotifier)
|
||||
final adminTeamsNotifierProvider =
|
||||
AutoDisposeAsyncNotifierProvider<AdminTeamsNotifier, void>.internal(
|
||||
AdminTeamsNotifier.new,
|
||||
name: r'adminTeamsNotifierProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$adminTeamsNotifierHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$AdminTeamsNotifier = AutoDisposeAsyncNotifier<void>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||
@@ -0,0 +1,132 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
/// Shell for the admin panel. On screens 640px wide and below it shows a
|
||||
/// bottom NavigationBar; on wider viewports it switches to a NavigationRail
|
||||
/// so admins can sweep through tabs with a single click on web/desktop.
|
||||
class AdminShell extends StatelessWidget {
|
||||
const AdminShell({super.key, required this.child});
|
||||
|
||||
final Widget child;
|
||||
|
||||
static const _tabs = <_AdminTab>[
|
||||
_AdminTab(
|
||||
label: 'EVENTS',
|
||||
icon: Icons.event_outlined,
|
||||
activeIcon: Icons.event,
|
||||
path: '/admin/events',
|
||||
),
|
||||
_AdminTab(
|
||||
label: 'TEAMS',
|
||||
icon: Icons.groups_outlined,
|
||||
activeIcon: Icons.groups,
|
||||
path: '/admin/teams',
|
||||
),
|
||||
_AdminTab(
|
||||
label: 'BRACKETS',
|
||||
icon: Icons.account_tree_outlined,
|
||||
activeIcon: Icons.account_tree,
|
||||
path: '/admin/brackets',
|
||||
),
|
||||
_AdminTab(
|
||||
label: 'IDEAS',
|
||||
icon: Icons.lightbulb_outline,
|
||||
activeIcon: Icons.lightbulb,
|
||||
path: '/admin/suggestions',
|
||||
),
|
||||
_AdminTab(
|
||||
label: 'PENDING',
|
||||
icon: Icons.pending_actions_outlined,
|
||||
activeIcon: Icons.pending_actions,
|
||||
path: '/admin/pending',
|
||||
),
|
||||
];
|
||||
|
||||
int _currentIndex(BuildContext context) {
|
||||
final location = GoRouterState.of(context).uri.path;
|
||||
final idx = _tabs.indexWhere((t) => location.startsWith(t.path));
|
||||
return idx < 0 ? 0 : idx;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final currentIndex = _currentIndex(context);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('ADMIN'),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
tooltip: 'Back to app',
|
||||
onPressed: () => context.go('/events'),
|
||||
),
|
||||
),
|
||||
body: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final isWide = constraints.maxWidth >= 640;
|
||||
if (isWide) {
|
||||
return Row(
|
||||
children: <Widget>[
|
||||
NavigationRail(
|
||||
selectedIndex: currentIndex,
|
||||
onDestinationSelected: (i) => context.go(_tabs[i].path),
|
||||
labelType: NavigationRailLabelType.all,
|
||||
destinations: _tabs
|
||||
.map(
|
||||
(t) => NavigationRailDestination(
|
||||
icon: Icon(t.icon),
|
||||
selectedIcon: Icon(t.activeIcon),
|
||||
label: Text(t.label),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
const VerticalDivider(width: 1),
|
||||
Expanded(child: child),
|
||||
],
|
||||
);
|
||||
}
|
||||
return child;
|
||||
},
|
||||
),
|
||||
bottomNavigationBar: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final isWide = constraints.maxWidth >= 640;
|
||||
if (isWide) return const SizedBox.shrink();
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Container(height: 1, color: const Color(0xFF8B30C8)),
|
||||
NavigationBar(
|
||||
selectedIndex: currentIndex,
|
||||
onDestinationSelected: (i) => context.go(_tabs[i].path),
|
||||
destinations: _tabs
|
||||
.map(
|
||||
(t) => NavigationDestination(
|
||||
icon: Icon(t.icon),
|
||||
selectedIcon: Icon(t.activeIcon),
|
||||
label: t.label,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AdminTab {
|
||||
const _AdminTab({
|
||||
required this.label,
|
||||
required this.icon,
|
||||
required this.activeIcon,
|
||||
required this.path,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final IconData icon;
|
||||
final IconData activeIcon;
|
||||
final String path;
|
||||
}
|
||||
@@ -0,0 +1,791 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../../brackets/domain/bracket.dart';
|
||||
import '../../../events/domain/event.dart';
|
||||
import '../../../teams/domain/team.dart';
|
||||
import '../../../teams/infrastructure/teams_repository.dart';
|
||||
import '../../application/admin_brackets_notifier.dart';
|
||||
import '../../application/admin_events_notifier.dart';
|
||||
|
||||
/// Form for creating or editing a tournament bracket.
|
||||
///
|
||||
/// The shape (round count + matches per round) is set up front when creating.
|
||||
/// After creation — or when editing — admins can adjust team labels, set
|
||||
/// scores, change match status, and pick a winner per match.
|
||||
class AdminBracketFormScreen extends ConsumerStatefulWidget {
|
||||
const AdminBracketFormScreen({super.key, this.bracketId});
|
||||
|
||||
final String? bracketId;
|
||||
|
||||
bool get isEdit => bracketId != null;
|
||||
|
||||
@override
|
||||
ConsumerState<AdminBracketFormScreen> createState() =>
|
||||
_AdminBracketFormScreenState();
|
||||
}
|
||||
|
||||
class _AdminBracketFormScreenState
|
||||
extends ConsumerState<AdminBracketFormScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _nameCtrl = TextEditingController();
|
||||
String? _eventId;
|
||||
DateTime _createdAt = DateTime.now();
|
||||
|
||||
List<_RoundDraft> _rounds = <_RoundDraft>[];
|
||||
|
||||
// Setup-mode controls (visible when creating a brand-new bracket)
|
||||
int _setupRounds = 3;
|
||||
int _setupTeams = 8;
|
||||
|
||||
bool _hydrated = false;
|
||||
bool _submitting = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (!widget.isEdit) {
|
||||
_hydrated = true;
|
||||
_generateRounds(rounds: _setupRounds, teams: _setupTeams);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameCtrl.dispose();
|
||||
for (final r in _rounds) {
|
||||
r.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _hydrateFrom(Bracket bracket) {
|
||||
if (_hydrated) return;
|
||||
_nameCtrl.text = bracket.name;
|
||||
_eventId = bracket.eventId.isEmpty ? null : bracket.eventId;
|
||||
_createdAt = bracket.createdAt;
|
||||
_rounds = bracket.rounds.map(_RoundDraft.fromRound).toList();
|
||||
_hydrated = true;
|
||||
}
|
||||
|
||||
void _generateRounds({required int rounds, required int teams}) {
|
||||
// For a single-elimination shape, round 1 holds teams/2 matches, round 2
|
||||
// holds teams/4, etc. We round up to handle odd team counts gracefully.
|
||||
for (final r in _rounds) {
|
||||
r.dispose();
|
||||
}
|
||||
_rounds = <_RoundDraft>[];
|
||||
var matchesInRound = (teams / 2).ceil();
|
||||
for (var i = 0; i < rounds; i++) {
|
||||
_rounds.add(
|
||||
_RoundDraft(
|
||||
roundNumber: i + 1,
|
||||
label: _defaultRoundLabel(i, rounds),
|
||||
matches: List.generate(
|
||||
matchesInRound < 1 ? 1 : matchesInRound,
|
||||
(m) => _MatchDraft.empty(id: 'r${i + 1}_m${m + 1}'),
|
||||
),
|
||||
),
|
||||
);
|
||||
matchesInRound = (matchesInRound / 2).ceil();
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
String _defaultRoundLabel(int index, int total) {
|
||||
final distanceFromEnd = total - 1 - index;
|
||||
switch (distanceFromEnd) {
|
||||
case 0:
|
||||
return 'Final';
|
||||
case 1:
|
||||
return 'Semifinals';
|
||||
case 2:
|
||||
return 'Quarterfinals';
|
||||
case 3:
|
||||
return 'Round of 16';
|
||||
default:
|
||||
return 'Round ${index + 1}';
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _randomizeTeams() async {
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final List<Team> teams = await ref
|
||||
.read(teamsRepositoryProvider)
|
||||
.watchTeams()
|
||||
.first;
|
||||
if (!mounted) return;
|
||||
if (teams.length < 2) {
|
||||
messenger.showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Need at least 2 teams in Firestore to randomize.'),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (_rounds.isEmpty || _rounds.first.matches.isEmpty) {
|
||||
messenger.showSnackBar(
|
||||
const SnackBar(content: Text('No round-1 matches to fill.')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final shuffled = List<Team>.of(teams)..shuffle();
|
||||
final round1 = _rounds.first;
|
||||
|
||||
setState(() {
|
||||
var teamIdx = 0;
|
||||
for (final match in round1.matches) {
|
||||
if (teamIdx < shuffled.length) {
|
||||
final a = shuffled[teamIdx++];
|
||||
match.teamANameCtrl.text = a.name;
|
||||
match.teamAId = a.id;
|
||||
} else {
|
||||
match.teamANameCtrl.text = '';
|
||||
match.teamAId = null;
|
||||
}
|
||||
if (teamIdx < shuffled.length) {
|
||||
final b = shuffled[teamIdx++];
|
||||
match.teamBNameCtrl.text = b.name;
|
||||
match.teamBId = b.id;
|
||||
} else {
|
||||
match.teamBNameCtrl.text = '';
|
||||
match.teamBId = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Randomized ${shuffled.length} teams into round 1.'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _submit() async {
|
||||
if (!(_formKey.currentState?.validate() ?? false)) return;
|
||||
if (_eventId == null || _eventId!.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Pick an event for this bracket.')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
final id = widget.bracketId ?? '';
|
||||
final bracket = Bracket(
|
||||
id: id,
|
||||
eventId: _eventId!,
|
||||
name: _nameCtrl.text.trim(),
|
||||
createdAt: _createdAt,
|
||||
rounds: _rounds.map((r) => r.toRound()).toList(growable: false),
|
||||
);
|
||||
|
||||
setState(() => _submitting = true);
|
||||
try {
|
||||
if (widget.isEdit) {
|
||||
await ref.read(adminBracketsNotifierProvider.notifier).save(bracket);
|
||||
} else {
|
||||
await ref.read(adminBracketsNotifierProvider.notifier).create(bracket);
|
||||
}
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(widget.isEdit ? 'Bracket updated' : 'Bracket created'),
|
||||
),
|
||||
);
|
||||
context.go('/admin/brackets');
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('Save failed: $e')));
|
||||
} finally {
|
||||
if (mounted) setState(() => _submitting = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.isEdit && !_hydrated) {
|
||||
final bracketsAsync = ref.watch(adminBracketsStreamProvider);
|
||||
final brackets = bracketsAsync.valueOrNull;
|
||||
if (brackets != null) {
|
||||
Bracket? match;
|
||||
for (final b in brackets) {
|
||||
if (b.id == widget.bracketId) {
|
||||
match = b;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match != null) {
|
||||
final found = match;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
setState(() => _hydrateFrom(found));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final eventsAsync = ref.watch(adminEventsStreamProvider);
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(widget.isEdit ? 'EDIT BRACKET' : 'NEW BRACKET'),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () => context.go('/admin/brackets'),
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: <Widget>[
|
||||
TextFormField(
|
||||
controller: _nameCtrl,
|
||||
decoration: const InputDecoration(labelText: 'Bracket name'),
|
||||
validator: (v) =>
|
||||
(v == null || v.trim().isEmpty) ? 'Required' : null,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
eventsAsync.when(
|
||||
loading: () => const LinearProgressIndicator(),
|
||||
error: (e, _) => Text('Could not load events: $e'),
|
||||
data: (events) => _EventPicker(
|
||||
events: events,
|
||||
selected: _eventId,
|
||||
onChanged: (id) => setState(() => _eventId = id),
|
||||
),
|
||||
),
|
||||
if (!widget.isEdit) ...<Widget>[
|
||||
const SizedBox(height: 16),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: _submitting ? null : _randomizeTeams,
|
||||
icon: const Text('🎲', style: TextStyle(fontSize: 16)),
|
||||
label: const Text('RANDOMIZE TEAMS'),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 20),
|
||||
if (!widget.isEdit) ...<Widget>[
|
||||
Text(
|
||||
'BRACKET SHAPE',
|
||||
style: theme.textTheme.labelLarge?.copyWith(
|
||||
letterSpacing: 1.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: _NumberStepper(
|
||||
label: 'Rounds',
|
||||
value: _setupRounds,
|
||||
min: 1,
|
||||
max: 6,
|
||||
onChanged: (v) => setState(() {
|
||||
_setupRounds = v;
|
||||
_generateRounds(rounds: v, teams: _setupTeams);
|
||||
}),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _NumberStepper(
|
||||
label: 'Teams in round 1',
|
||||
value: _setupTeams,
|
||||
min: 2,
|
||||
max: 32,
|
||||
step: 2,
|
||||
onChanged: (v) => setState(() {
|
||||
_setupTeams = v;
|
||||
_generateRounds(rounds: _setupRounds, teams: v);
|
||||
}),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
..._rounds.map(
|
||||
(round) => _RoundEditor(
|
||||
round: round,
|
||||
onChanged: () => setState(() {}),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
FilledButton.icon(
|
||||
onPressed: _submitting ? null : _submit,
|
||||
icon: _submitting
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.save_outlined),
|
||||
label: Text(widget.isEdit ? 'SAVE CHANGES' : 'CREATE BRACKET'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _EventPicker extends StatelessWidget {
|
||||
const _EventPicker({
|
||||
required this.events,
|
||||
required this.selected,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
final List<Event> events;
|
||||
final String? selected;
|
||||
final ValueChanged<String?> onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final items = <DropdownMenuItem<String>>[
|
||||
const DropdownMenuItem<String>(
|
||||
value: null,
|
||||
child: Text('— select event —'),
|
||||
),
|
||||
...events.map(
|
||||
(e) => DropdownMenuItem<String>(
|
||||
value: e.id,
|
||||
child: Text(e.title, overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
final currentValue = events.any((e) => e.id == selected) ? selected : null;
|
||||
|
||||
return DropdownButtonFormField<String>(
|
||||
initialValue: currentValue,
|
||||
items: items,
|
||||
onChanged: onChanged,
|
||||
decoration: const InputDecoration(labelText: 'Event'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NumberStepper extends StatelessWidget {
|
||||
const _NumberStepper({
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.min,
|
||||
required this.max,
|
||||
required this.onChanged,
|
||||
this.step = 1,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final int value;
|
||||
final int min;
|
||||
final int max;
|
||||
final int step;
|
||||
final ValueChanged<int> onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
label,
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove),
|
||||
onPressed: value - step >= min
|
||||
? () => onChanged(value - step)
|
||||
: null,
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'$value',
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
onPressed: value + step <= max
|
||||
? () => onChanged(value + step)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RoundEditor extends StatelessWidget {
|
||||
const _RoundEditor({required this.round, required this.onChanged});
|
||||
|
||||
final _RoundDraft round;
|
||||
final VoidCallback onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
TextField(
|
||||
controller: round.labelCtrl,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Round ${round.roundNumber} label',
|
||||
hintText: 'e.g. Quarterfinals',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
for (var i = 0; i < round.matches.length; i++)
|
||||
_MatchEditor(
|
||||
index: i,
|
||||
match: round.matches[i],
|
||||
onChanged: onChanged,
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton.icon(
|
||||
onPressed: () {
|
||||
round.matches.add(
|
||||
_MatchDraft.empty(
|
||||
id: 'r${round.roundNumber}_m${round.matches.length + 1}',
|
||||
),
|
||||
);
|
||||
onChanged();
|
||||
},
|
||||
icon: const Icon(Icons.add, size: 18),
|
||||
label: Text('Add match', style: theme.textTheme.labelMedium),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MatchEditor extends StatelessWidget {
|
||||
const _MatchEditor({
|
||||
required this.index,
|
||||
required this.match,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
final int index;
|
||||
final _MatchDraft match;
|
||||
final VoidCallback onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(color: theme.colorScheme.outlineVariant),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'MATCH ${index + 1}',
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
letterSpacing: 1.2,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
DropdownButton<MatchStatus>(
|
||||
value: match.status,
|
||||
onChanged: (v) {
|
||||
if (v == null) return;
|
||||
match.status = v;
|
||||
onChanged();
|
||||
},
|
||||
items: MatchStatus.values
|
||||
.map(
|
||||
(s) => DropdownMenuItem<MatchStatus>(
|
||||
value: s,
|
||||
child: Text(_statusLabel(s)),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: match.teamANameCtrl,
|
||||
decoration: const InputDecoration(labelText: 'Team A'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
SizedBox(
|
||||
width: 72,
|
||||
child: TextField(
|
||||
controller: match.scoreACtrl,
|
||||
keyboardType: TextInputType.number,
|
||||
textAlign: TextAlign.center,
|
||||
decoration: const InputDecoration(labelText: 'Score'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: match.teamBNameCtrl,
|
||||
decoration: const InputDecoration(labelText: 'Team B'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
SizedBox(
|
||||
width: 72,
|
||||
child: TextField(
|
||||
controller: match.scoreBCtrl,
|
||||
keyboardType: TextInputType.number,
|
||||
textAlign: TextAlign.center,
|
||||
decoration: const InputDecoration(labelText: 'Score'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Winner: ',
|
||||
style: theme.textTheme.labelMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: SegmentedButton<_WinnerSelection>(
|
||||
segments: const <ButtonSegment<_WinnerSelection>>[
|
||||
ButtonSegment(
|
||||
value: _WinnerSelection.none,
|
||||
label: Text('TBD'),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: _WinnerSelection.teamA,
|
||||
label: Text('A'),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: _WinnerSelection.teamB,
|
||||
label: Text('B'),
|
||||
),
|
||||
],
|
||||
selected: <_WinnerSelection>{match.winner},
|
||||
onSelectionChanged: (set) {
|
||||
match.winner = set.first;
|
||||
onChanged();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _statusLabel(MatchStatus s) {
|
||||
switch (s) {
|
||||
case MatchStatus.scheduled:
|
||||
return 'Scheduled';
|
||||
case MatchStatus.inProgress:
|
||||
return 'In progress';
|
||||
case MatchStatus.completed:
|
||||
return 'Completed';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum _WinnerSelection { none, teamA, teamB }
|
||||
|
||||
class _RoundDraft {
|
||||
_RoundDraft({
|
||||
required this.roundNumber,
|
||||
required String label,
|
||||
required this.matches,
|
||||
}) : labelCtrl = TextEditingController(text: label);
|
||||
|
||||
factory _RoundDraft.fromRound(BracketRound round) {
|
||||
return _RoundDraft(
|
||||
roundNumber: round.roundNumber,
|
||||
label: round.label,
|
||||
matches: round.matches.map(_MatchDraft.fromMatch).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
final int roundNumber;
|
||||
final TextEditingController labelCtrl;
|
||||
final List<_MatchDraft> matches;
|
||||
|
||||
void dispose() {
|
||||
labelCtrl.dispose();
|
||||
for (final m in matches) {
|
||||
m.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
BracketRound toRound() {
|
||||
return BracketRound(
|
||||
roundNumber: roundNumber,
|
||||
label: labelCtrl.text.trim().isEmpty
|
||||
? 'Round $roundNumber'
|
||||
: labelCtrl.text.trim(),
|
||||
matches: matches.map((m) => m.toMatch()).toList(growable: false),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MatchDraft {
|
||||
_MatchDraft({
|
||||
required this.id,
|
||||
required String teamA,
|
||||
required String teamB,
|
||||
required int? scoreA,
|
||||
required int? scoreB,
|
||||
required this.status,
|
||||
required this.winner,
|
||||
this.teamAId,
|
||||
this.teamBId,
|
||||
}) : teamANameCtrl = TextEditingController(text: teamA),
|
||||
teamBNameCtrl = TextEditingController(text: teamB),
|
||||
scoreACtrl = TextEditingController(
|
||||
text: scoreA == null ? '' : '$scoreA',
|
||||
),
|
||||
scoreBCtrl = TextEditingController(
|
||||
text: scoreB == null ? '' : '$scoreB',
|
||||
);
|
||||
|
||||
factory _MatchDraft.empty({required String id}) {
|
||||
return _MatchDraft(
|
||||
id: id,
|
||||
teamA: '',
|
||||
teamB: '',
|
||||
scoreA: null,
|
||||
scoreB: null,
|
||||
status: MatchStatus.scheduled,
|
||||
winner: _WinnerSelection.none,
|
||||
);
|
||||
}
|
||||
|
||||
factory _MatchDraft.fromMatch(BracketMatch match) {
|
||||
final winner = match.winnerId == null
|
||||
? _WinnerSelection.none
|
||||
: match.isTeamAWinner
|
||||
? _WinnerSelection.teamA
|
||||
: match.isTeamBWinner
|
||||
? _WinnerSelection.teamB
|
||||
: _WinnerSelection.none;
|
||||
return _MatchDraft(
|
||||
id: match.id,
|
||||
teamA: match.teamA?.name ?? '',
|
||||
teamB: match.teamB?.name ?? '',
|
||||
scoreA: match.scoreA,
|
||||
scoreB: match.scoreB,
|
||||
status: match.status,
|
||||
winner: winner,
|
||||
teamAId: match.teamA?.id,
|
||||
teamBId: match.teamB?.id,
|
||||
);
|
||||
}
|
||||
|
||||
final String id;
|
||||
final TextEditingController teamANameCtrl;
|
||||
final TextEditingController teamBNameCtrl;
|
||||
final TextEditingController scoreACtrl;
|
||||
final TextEditingController scoreBCtrl;
|
||||
MatchStatus status;
|
||||
_WinnerSelection winner;
|
||||
String? teamAId;
|
||||
String? teamBId;
|
||||
|
||||
void dispose() {
|
||||
teamANameCtrl.dispose();
|
||||
teamBNameCtrl.dispose();
|
||||
scoreACtrl.dispose();
|
||||
scoreBCtrl.dispose();
|
||||
}
|
||||
|
||||
BracketMatch toMatch() {
|
||||
final aName = teamANameCtrl.text.trim();
|
||||
final bName = teamBNameCtrl.text.trim();
|
||||
final teamA = aName.isEmpty
|
||||
? null
|
||||
: BracketTeam(id: teamAId ?? _slug(aName), name: aName);
|
||||
final teamB = bName.isEmpty
|
||||
? null
|
||||
: BracketTeam(id: teamBId ?? _slug(bName), name: bName);
|
||||
|
||||
String? winnerId;
|
||||
switch (winner) {
|
||||
case _WinnerSelection.none:
|
||||
winnerId = null;
|
||||
break;
|
||||
case _WinnerSelection.teamA:
|
||||
winnerId = teamA?.id;
|
||||
break;
|
||||
case _WinnerSelection.teamB:
|
||||
winnerId = teamB?.id;
|
||||
break;
|
||||
}
|
||||
|
||||
return BracketMatch(
|
||||
id: id,
|
||||
teamA: teamA,
|
||||
teamB: teamB,
|
||||
scoreA: int.tryParse(scoreACtrl.text.trim()),
|
||||
scoreB: int.tryParse(scoreBCtrl.text.trim()),
|
||||
status: status,
|
||||
winnerId: winnerId,
|
||||
);
|
||||
}
|
||||
|
||||
static String _slug(String input) {
|
||||
final lower = input.toLowerCase();
|
||||
final cleaned = lower
|
||||
.replaceAll(RegExp(r'[^a-z0-9]+'), '_')
|
||||
.replaceAll(RegExp(r'^_|_$'), '');
|
||||
return cleaned.isEmpty ? 'team' : cleaned;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../../../brackets/domain/bracket.dart';
|
||||
import '../../application/admin_brackets_notifier.dart';
|
||||
|
||||
class AdminBracketsScreen extends ConsumerWidget {
|
||||
const AdminBracketsScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final bracketsAsync = ref.watch(adminBracketsStreamProvider);
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Scaffold(
|
||||
body: bracketsAsync.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (err, _) => Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 48,
|
||||
color: theme.colorScheme.error,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'Could not load brackets',
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'$err',
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
data: (brackets) {
|
||||
if (brackets.isEmpty) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
Icons.account_tree_outlined,
|
||||
size: 64,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'No brackets yet',
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Tap the + button to create your first bracket.',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||
itemCount: brackets.length,
|
||||
itemBuilder: (context, i) => _BracketRow(bracket: brackets[i]),
|
||||
);
|
||||
},
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: () => context.go('/admin/brackets/new'),
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('NEW BRACKET'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BracketRow extends ConsumerWidget {
|
||||
const _BracketRow({required this.bracket});
|
||||
|
||||
final Bracket bracket;
|
||||
|
||||
Future<void> _confirmDelete(BuildContext context, WidgetRef ref) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('Delete bracket?'),
|
||||
content: Text(
|
||||
'"${bracket.name}" and all its match data will be permanently removed.',
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(false),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton.tonal(
|
||||
onPressed: () => Navigator.of(ctx).pop(true),
|
||||
child: const Text('Delete'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed != true) return;
|
||||
if (!context.mounted) return;
|
||||
try {
|
||||
await ref.read(adminBracketsNotifierProvider.notifier).delete(bracket.id);
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Deleted "${bracket.name}"')),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Delete failed: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
final created = DateFormat.yMMMd().format(bracket.createdAt);
|
||||
final totalMatches = bracket.rounds.fold<int>(
|
||||
0,
|
||||
(sum, r) => sum + r.matches.length,
|
||||
);
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(bracket.name, style: theme.textTheme.titleMedium),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'${bracket.rounds.length} round${bracket.rounds.length == 1 ? '' : 's'} · $totalMatches match${totalMatches == 1 ? '' : 'es'} · Created $created',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
TextButton.icon(
|
||||
onPressed: () => _confirmDelete(context, ref),
|
||||
icon: const Icon(Icons.delete_outline, size: 18),
|
||||
label: const Text('Delete'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () =>
|
||||
context.go('/admin/brackets/${bracket.id}/edit'),
|
||||
icon: const Icon(Icons.edit_outlined, size: 18),
|
||||
label: const Text('Edit'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../../../events/domain/event.dart';
|
||||
import '../../application/admin_events_notifier.dart';
|
||||
|
||||
class AdminEventFormScreen extends ConsumerStatefulWidget {
|
||||
const AdminEventFormScreen({super.key, this.eventId});
|
||||
|
||||
/// Null when creating a new event; otherwise the id of the event being
|
||||
/// edited.
|
||||
final String? eventId;
|
||||
|
||||
bool get isEdit => eventId != null;
|
||||
|
||||
@override
|
||||
ConsumerState<AdminEventFormScreen> createState() =>
|
||||
_AdminEventFormScreenState();
|
||||
}
|
||||
|
||||
class _AdminEventFormScreenState extends ConsumerState<AdminEventFormScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _titleCtrl = TextEditingController();
|
||||
final _descCtrl = TextEditingController();
|
||||
final _locationCtrl = TextEditingController();
|
||||
final _imageUrlCtrl = TextEditingController();
|
||||
final _teamsRegisteredCtrl = TextEditingController(text: '0');
|
||||
final _maxTeamsCtrl = TextEditingController(text: '8');
|
||||
|
||||
DateTime _date = DateTime.now().add(const Duration(days: 7));
|
||||
DateTime _registrationDeadline = DateTime.now().add(const Duration(days: 6));
|
||||
EventCategory _category = EventCategory.pickup;
|
||||
bool _isCancelled = false;
|
||||
bool _hydrated = false;
|
||||
bool _submitting = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (!widget.isEdit) _hydrated = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_titleCtrl.dispose();
|
||||
_descCtrl.dispose();
|
||||
_locationCtrl.dispose();
|
||||
_imageUrlCtrl.dispose();
|
||||
_teamsRegisteredCtrl.dispose();
|
||||
_maxTeamsCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _hydrateFrom(Event event) {
|
||||
if (_hydrated) return;
|
||||
_titleCtrl.text = event.title;
|
||||
_descCtrl.text = event.description;
|
||||
_locationCtrl.text = event.location;
|
||||
_imageUrlCtrl.text = event.imageUrl ?? '';
|
||||
_teamsRegisteredCtrl.text = event.teamsRegistered.toString();
|
||||
_maxTeamsCtrl.text = event.maxTeams.toString();
|
||||
_date = event.date;
|
||||
_registrationDeadline = event.registrationDeadline;
|
||||
_category = event.category;
|
||||
_isCancelled = event.isCancelled;
|
||||
_hydrated = true;
|
||||
}
|
||||
|
||||
Future<void> _pickDate({required bool registration}) async {
|
||||
final initial = registration ? _registrationDeadline : _date;
|
||||
final picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: initial,
|
||||
firstDate: DateTime(2020),
|
||||
lastDate: DateTime(2100),
|
||||
);
|
||||
if (picked == null || !mounted) return;
|
||||
final time = await showTimePicker(
|
||||
context: context,
|
||||
initialTime: TimeOfDay.fromDateTime(initial),
|
||||
);
|
||||
if (time == null) return;
|
||||
final merged = DateTime(
|
||||
picked.year,
|
||||
picked.month,
|
||||
picked.day,
|
||||
time.hour,
|
||||
time.minute,
|
||||
);
|
||||
setState(() {
|
||||
if (registration) {
|
||||
_registrationDeadline = merged;
|
||||
} else {
|
||||
_date = merged;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _submit() async {
|
||||
if (!(_formKey.currentState?.validate() ?? false)) return;
|
||||
final id = widget.eventId ?? '';
|
||||
final event = Event(
|
||||
id: id,
|
||||
title: _titleCtrl.text.trim(),
|
||||
description: _descCtrl.text.trim(),
|
||||
date: _date,
|
||||
location: _locationCtrl.text.trim(),
|
||||
registrationDeadline: _registrationDeadline,
|
||||
teamsRegistered: int.tryParse(_teamsRegisteredCtrl.text.trim()) ?? 0,
|
||||
maxTeams: int.tryParse(_maxTeamsCtrl.text.trim()) ?? 0,
|
||||
category: _category,
|
||||
imageUrl: _imageUrlCtrl.text.trim().isEmpty
|
||||
? null
|
||||
: _imageUrlCtrl.text.trim(),
|
||||
isCancelled: _isCancelled,
|
||||
);
|
||||
|
||||
setState(() => _submitting = true);
|
||||
try {
|
||||
if (widget.isEdit) {
|
||||
await ref.read(adminEventsNotifierProvider.notifier).save(event);
|
||||
} else {
|
||||
await ref.read(adminEventsNotifierProvider.notifier).create(event);
|
||||
}
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(widget.isEdit ? 'Event updated' : 'Event created'),
|
||||
),
|
||||
);
|
||||
context.go('/admin/events');
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('Save failed: $e')));
|
||||
} finally {
|
||||
if (mounted) setState(() => _submitting = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.isEdit && !_hydrated) {
|
||||
final eventsAsync = ref.watch(adminEventsStreamProvider);
|
||||
final events = eventsAsync.valueOrNull;
|
||||
if (events != null) {
|
||||
final match = events.firstWhere(
|
||||
(e) => e.id == widget.eventId,
|
||||
orElse: () => _placeholderEvent(),
|
||||
);
|
||||
if (match.id.isNotEmpty) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
setState(() => _hydrateFrom(match));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final theme = Theme.of(context);
|
||||
final df = DateFormat.yMMMd().add_jm();
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(widget.isEdit ? 'EDIT EVENT' : 'NEW EVENT'),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () => context.go('/admin/events'),
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: <Widget>[
|
||||
TextFormField(
|
||||
controller: _titleCtrl,
|
||||
decoration: const InputDecoration(labelText: 'Title'),
|
||||
validator: (v) =>
|
||||
(v == null || v.trim().isEmpty) ? 'Required' : null,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: SegmentedButton<EventCategory>(
|
||||
segments: const <ButtonSegment<EventCategory>>[
|
||||
ButtonSegment<EventCategory>(
|
||||
value: EventCategory.tournament,
|
||||
label: Text('TOURNAMENT'),
|
||||
icon: Icon(Icons.emoji_events_outlined),
|
||||
),
|
||||
ButtonSegment<EventCategory>(
|
||||
value: EventCategory.pickup,
|
||||
label: Text('PICK-UP'),
|
||||
icon: Icon(Icons.sports_soccer),
|
||||
),
|
||||
],
|
||||
selected: <EventCategory>{_category},
|
||||
onSelectionChanged: (set) =>
|
||||
setState(() => _category = set.first),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: _descCtrl,
|
||||
decoration: const InputDecoration(labelText: 'Description'),
|
||||
minLines: 3,
|
||||
maxLines: 6,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: _locationCtrl,
|
||||
decoration: const InputDecoration(labelText: 'Location'),
|
||||
validator: (v) =>
|
||||
(v == null || v.trim().isEmpty) ? 'Required' : null,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_DateField(
|
||||
label: 'Event date & time',
|
||||
value: df.format(_date),
|
||||
onTap: () => _pickDate(registration: false),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_DateField(
|
||||
label: 'Registration deadline',
|
||||
value: df.format(_registrationDeadline),
|
||||
onTap: () => _pickDate(registration: true),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _teamsRegisteredCtrl,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Teams registered',
|
||||
),
|
||||
validator: _validateInt,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _maxTeamsCtrl,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(labelText: 'Max teams'),
|
||||
validator: _validateInt,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: _imageUrlCtrl,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Image URL (optional)',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SwitchListTile.adaptive(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text('Cancelled', style: theme.textTheme.bodyLarge),
|
||||
subtitle: Text(
|
||||
'Mark this event as cancelled',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
value: _isCancelled,
|
||||
onChanged: (v) => setState(() => _isCancelled = v),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
FilledButton.icon(
|
||||
onPressed: _submitting ? null : _submit,
|
||||
icon: _submitting
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.save_outlined),
|
||||
label: Text(widget.isEdit ? 'SAVE CHANGES' : 'CREATE EVENT'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static String? _validateInt(String? v) {
|
||||
if (v == null || v.trim().isEmpty) return 'Required';
|
||||
final n = int.tryParse(v.trim());
|
||||
if (n == null || n < 0) return 'Enter a non-negative number';
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Event _placeholderEvent() => Event(
|
||||
id: '',
|
||||
title: '',
|
||||
description: '',
|
||||
date: DateTime.now(),
|
||||
location: '',
|
||||
registrationDeadline: DateTime.now(),
|
||||
teamsRegistered: 0,
|
||||
maxTeams: 0,
|
||||
);
|
||||
|
||||
class _DateField extends StatelessWidget {
|
||||
const _DateField({
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final String value;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: InputDecorator(
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
suffixIcon: const Icon(Icons.calendar_today, size: 18),
|
||||
),
|
||||
child: Text(value),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../../../events/domain/event.dart';
|
||||
import '../../application/admin_events_notifier.dart';
|
||||
|
||||
class AdminEventsScreen extends ConsumerWidget {
|
||||
const AdminEventsScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final eventsAsync = ref.watch(adminEventsStreamProvider);
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Scaffold(
|
||||
body: eventsAsync.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (err, _) => _ErrorState(
|
||||
message: '$err',
|
||||
onRetry: () => ref.invalidate(adminEventsStreamProvider),
|
||||
),
|
||||
data: (events) {
|
||||
if (events.isEmpty) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
Icons.event_busy_outlined,
|
||||
size: 64,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'No events yet',
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Tap the + button to create your first event.',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||
itemCount: events.length,
|
||||
itemBuilder: (context, index) => _EventRow(event: events[index]),
|
||||
);
|
||||
},
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: () => context.go('/admin/events/new'),
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('NEW EVENT'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _EventRow extends ConsumerWidget {
|
||||
const _EventRow({required this.event});
|
||||
|
||||
final Event event;
|
||||
|
||||
Future<void> _confirmDelete(BuildContext context, WidgetRef ref) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('Delete event?'),
|
||||
content: Text(
|
||||
'"${event.title}" will be permanently removed. This cannot be undone.',
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(false),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton.tonal(
|
||||
onPressed: () => Navigator.of(ctx).pop(true),
|
||||
child: const Text('Delete'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed != true) return;
|
||||
if (!context.mounted) return;
|
||||
try {
|
||||
await ref.read(adminEventsNotifierProvider.notifier).delete(event.id);
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Deleted "${event.title}"')),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Delete failed: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
final dateLabel = DateFormat.yMMMd().add_jm().format(event.date);
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Text(
|
||||
event.title,
|
||||
style: theme.textTheme.titleMedium,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (event.isCancelled)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.errorContainer,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
child: Text(
|
||||
'CANCELLED',
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: theme.colorScheme.onErrorContainer,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
Icons.schedule,
|
||||
size: 14,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
dateLabel,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${event.teamsRegistered}/${event.maxTeams} teams',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
Icons.location_on_outlined,
|
||||
size: 14,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
event.location,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
TextButton.icon(
|
||||
onPressed: () => _confirmDelete(context, ref),
|
||||
icon: const Icon(Icons.delete_outline, size: 18),
|
||||
label: const Text('Delete'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () =>
|
||||
context.go('/admin/events/${event.id}/edit'),
|
||||
icon: const Icon(Icons.edit_outlined, size: 18),
|
||||
label: const Text('Edit'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ErrorState extends StatelessWidget {
|
||||
const _ErrorState({required this.message, required this.onRetry});
|
||||
|
||||
final String message;
|
||||
final VoidCallback onRetry;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 48,
|
||||
color: theme.colorScheme.error,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'Could not load events',
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
message,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
FilledButton.tonalIcon(
|
||||
onPressed: onRetry,
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('Try again'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||