diff --git a/ExportOptions.plist b/ExportOptions.plist
new file mode 100644
index 0000000..8b14c2f
--- /dev/null
+++ b/ExportOptions.plist
@@ -0,0 +1,13 @@
+
+
+
+
+ method
+ development
+ compileBitcode
+
+ signingStyle
+ automatic
+
+
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 340e7df..cb760b2 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -36,6 +36,10 @@
-
+
+
+
+
+
diff --git a/angular.json b/angular.json
index 9b72354..9bee49f 100644
--- a/angular.json
+++ b/angular.json
@@ -75,6 +75,10 @@
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
+ "options": {
+ "host": "0.0.0.0",
+ "port": 4200
+ },
"configurations": {
"production": {
"buildTarget": "app:build:production"
diff --git a/build.sh b/build.sh
new file mode 100755
index 0000000..a8e6194
--- /dev/null
+++ b/build.sh
@@ -0,0 +1,43 @@
+#!/bin/bash
+set -e # fail fast
+
+### 0) Caminhos/nomes
+PROJECT_ROOT="$(pwd)" # raiz do app Ionic
+IOS_DIR="${PROJECT_ROOT}/ios/App"
+WORKSPACE_PATH="${IOS_DIR}/App.xcworkspace"
+SCHEME_NAME="App"
+ARCHIVE_PATH="${PROJECT_ROOT}/build/${SCHEME_NAME}.xcarchive"
+IPA_EXPORT_PATH="${PROJECT_ROOT}/build/ipa"
+EXPORT_OPTIONS_PLIST="${IOS_DIR}/ExportOptions.plist" # já existe no projeto iOS
+
+### 1) Build web + sync
+echo "📦 Building web assets…"
+# Patch conhecido do Capacitor iOS (milissegundos)
+node scripts/patch-cap-ios.mjs || true
+npm run build
+npx cap sync ios # copia www e plugins
+
+### 2) Limpar build anterior
+rm -rf "${PROJECT_ROOT}/build"
+mkdir -p "$IPA_EXPORT_PATH"
+
+### 3) Arquivar (Release)
+echo "🛠️ Archiving (${SCHEME_NAME})…"
+xcodebuild \
+ -workspace "$WORKSPACE_PATH" \
+ -scheme "$SCHEME_NAME" \
+ -configuration Release \
+ -sdk iphoneos \
+ -destination 'generic/platform=iOS' \
+ -archivePath "$ARCHIVE_PATH" \
+ archive
+
+### 4) Exportar IPA
+echo "📤 Exporting IPA…"
+xcodebuild \
+ -exportArchive \
+ -archivePath "$ARCHIVE_PATH" \
+ -exportOptionsPlist "$EXPORT_OPTIONS_PLIST" \
+ -exportPath "$IPA_EXPORT_PATH"
+
+echo "✅ IPA gerada em: $IPA_EXPORT_PATH"
diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj
index 64375f6..e17cc0c 100644
--- a/ios/App/App.xcodeproj/project.pbxproj
+++ b/ios/App/App.xcodeproj/project.pbxproj
@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
- objectVersion = 48;
+ objectVersion = 53;
objects = {
/* Begin PBXBuildFile section */
@@ -122,13 +122,13 @@
504EC2FC1FED79650016851F /* Project object */ = {
isa = PBXProject;
attributes = {
+ BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 0920;
- LastUpgradeCheck = 1420;
+ LastUpgradeCheck = 1430;
TargetAttributes = {
504EC3031FED79650016851F = {
CreatedOnToolsVersion = 9.2;
LastSwiftMigration = 1100;
- ProvisioningStyle = Automatic;
};
};
};
@@ -267,7 +267,6 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
- CODE_SIGN_IDENTITY = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -327,7 +326,6 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
- CODE_SIGN_IDENTITY = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
@@ -343,7 +341,8 @@
IPHONEOS_DEPLOYMENT_TARGET = 16.2;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
- SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
VALIDATE_PRODUCT = YES;
};
name = Release;
@@ -353,19 +352,30 @@
baseConfigurationReference = FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 5H4TCPBPYP;
INFOPLIST_FILE = App/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = PinkCOdeScanner;
+ INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
IPHONEOS_DEPLOYMENT_TARGET = 16.2;
- LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
MARKETING_VERSION = 1.0;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = com.vagner.pinkcode;
+ "PRODUCT_BUNDLE_IDENTIFIER[sdk=*]" = com.vagner.pinkcode;
PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
+ SUPPORTS_MACCATALYST = NO;
+ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2";
+ TARGETED_DEVICE_FAMILY = 1;
};
name = Debug;
};
@@ -374,18 +384,29 @@
baseConfigurationReference = AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 5H4TCPBPYP;
INFOPLIST_FILE = App/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = PinkCOdeScanner;
+ INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
IPHONEOS_DEPLOYMENT_TARGET = 16.2;
- LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.vagner.pinkcode;
+ "PRODUCT_BUNDLE_IDENTIFIER[sdk=*]" = com.vagner.pinkcode;
PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
+ SUPPORTS_MACCATALYST = NO;
+ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2";
+ TARGETED_DEVICE_FAMILY = 1;
};
name = Release;
};
diff --git a/ios/App/App/Info.plist b/ios/App/App/Info.plist
index d8dfae4..add3bcf 100644
--- a/ios/App/App/Info.plist
+++ b/ios/App/App/Info.plist
@@ -22,6 +22,8 @@
$(CURRENT_PROJECT_VERSION)
LSRequiresIPhoneOS
+ NSCameraUsageDescription
+ We need camera access to scan barcodes and QR codes.
UILaunchStoryboardName
LaunchScreen
UIMainStoryboardFile
@@ -33,15 +35,11 @@
UISupportedInterfaceOrientations
UIInterfaceOrientationPortrait
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
UISupportedInterfaceOrientations~ipad
UIInterfaceOrientationPortrait
UIInterfaceOrientationPortraitUpsideDown
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
UIViewControllerBasedStatusBarAppearance
diff --git a/ios/App/ExportOptions.plist b/ios/App/ExportOptions.plist
index 4e3db35..a3db099 100644
--- a/ios/App/ExportOptions.plist
+++ b/ios/App/ExportOptions.plist
@@ -3,12 +3,12 @@
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
- methodad-hoc
- signingStylemanual
- provisioningProfiles
-
- com.vagner.pinkcodePINKCODE_AdHoc
-
- teamIDABCDE12345
+ method
+ development
+ compileBitcode
+
+ signingStyle
+ automatic
+
diff --git a/package.json b/package.json
index cdd0860..1c9fa78 100644
--- a/package.json
+++ b/package.json
@@ -9,7 +9,10 @@
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test",
- "lint": "ng lint"
+ "lint": "ng lint",
+ "splash:apply": "node scripts/apply-splash.mjs",
+ "native:sync": "npx cap sync",
+ "postinstall": "node scripts/patch-cap-ios.mjs"
},
"private": true,
"dependencies": {
diff --git a/scripts/apply-splash.mjs b/scripts/apply-splash.mjs
new file mode 100644
index 0000000..cdfa7df
--- /dev/null
+++ b/scripts/apply-splash.mjs
@@ -0,0 +1,51 @@
+#!/usr/bin/env node
+import { cpSync, existsSync, mkdirSync } from 'node:fs';
+import { join } from 'node:path';
+
+const root = process.cwd();
+const srcLogo = join(root, 'src', 'assets', 'logo.png');
+
+if (!existsSync(srcLogo)) {
+ console.error('Logo não encontrado em src/assets/logo.png');
+ process.exit(1);
+}
+
+// Android: substituir todos os splash.png existentes e garantir nodpi
+const androidDirs = [
+ 'android/app/src/main/res/drawable',
+ 'android/app/src/main/res/drawable-land-hdpi',
+ 'android/app/src/main/res/drawable-land-mdpi',
+ 'android/app/src/main/res/drawable-land-xhdpi',
+ 'android/app/src/main/res/drawable-land-xxhdpi',
+ 'android/app/src/main/res/drawable-land-xxxhdpi',
+ 'android/app/src/main/res/drawable-port-hdpi',
+ 'android/app/src/main/res/drawable-port-mdpi',
+ 'android/app/src/main/res/drawable-port-xhdpi',
+ 'android/app/src/main/res/drawable-port-xxhdpi',
+ 'android/app/src/main/res/drawable-port-xxxhdpi',
+ 'android/app/src/main/res/drawable-nodpi',
+];
+
+for (const d of androidDirs) {
+ const dir = join(root, d);
+ mkdirSync(dir, { recursive: true });
+ cpSync(srcLogo, join(dir, 'splash.png'));
+}
+
+// iOS: substituir imagens do Splash.imageset
+const iosDir = join(root, 'ios', 'App', 'App', 'Assets.xcassets', 'Splash.imageset');
+const iosTargets = [
+ 'splash-2732x2732.png',
+ 'splash-2732x2732-1.png',
+ 'splash-2732x2732-2.png',
+];
+try {
+ for (const f of iosTargets) {
+ cpSync(srcLogo, join(iosDir, f));
+ }
+} catch (e) {
+ // ignore if iOS folder not present
+}
+
+console.log('Logo aplicado às pastas de splash (Android/iOS).');
+console.log('Execute: npx cap sync para propagar para os projetos nativos.');
diff --git a/scripts/patch-cap-ios.mjs b/scripts/patch-cap-ios.mjs
new file mode 100644
index 0000000..70e38a3
--- /dev/null
+++ b/scripts/patch-cap-ios.mjs
@@ -0,0 +1,45 @@
+#!/usr/bin/env node
+import { readFileSync, writeFileSync, existsSync } from 'node:fs';
+import { join } from 'node:path';
+
+const root = process.cwd();
+const decoder = join(root, 'node_modules', '@capacitor', 'ios', 'Capacitor', 'Capacitor', 'Codable', 'JSValueDecoder.swift');
+const encoder = join(root, 'node_modules', '@capacitor', 'ios', 'Capacitor', 'Capacitor', 'Codable', 'JSValueEncoder.swift');
+const files = [decoder, encoder];
+
+let changed = 0;
+for (const file of files) {
+ if (!existsSync(file)) continue;
+ const src = readFileSync(file, 'utf8');
+ let out = src;
+ let fileChanged = false;
+ if (out.includes('MSEC_PER_SEC')) {
+ out = out.replaceAll('MSEC_PER_SEC', '1000.0');
+ fileChanged = true;
+ }
+ if (file === encoder) {
+ // Ensure switch returns String in EncodingContainer.type
+ out = out.replace(
+ /case \.singleValue:\s*"SingleValueContainer"/m,
+ 'case .singleValue:\n return "SingleValueContainer"'
+ );
+ out = out.replace(
+ /case \.unkeyed:\s*"UnkeyedContainer"/m,
+ 'case .unkeyed:\n return "UnkeyedContainer"'
+ );
+ out = out.replace(
+ /case \.keyed:\s*"KeyedContainer"/m,
+ 'case .keyed:\n return "KeyedContainer"'
+ );
+ if (out !== src) fileChanged = true;
+ }
+ if (fileChanged) {
+ writeFileSync(file, out);
+ changed++;
+ console.log(`Patched: ${file}`);
+ }
+}
+
+if (changed === 0) {
+ console.log('No iOS Capacitor patches needed.');
+}
diff --git a/src/app/app.component.html b/src/app/app.component.html
index 13b9677..3e2fba1 100644
--- a/src/app/app.component.html
+++ b/src/app/app.component.html
@@ -1,3 +1,4 @@
+
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index 1da531b..be61a15 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -1,10 +1,11 @@
import { Component } from '@angular/core';
import { IonApp, IonRouterOutlet } from '@ionic/angular/standalone';
+import { TabsPage } from './tabs/tabs.page';
@Component({
selector: 'app-root',
templateUrl: 'app.component.html',
- imports: [IonApp, IonRouterOutlet],
+ imports: [IonApp, IonRouterOutlet, TabsPage],
})
export class AppComponent {
constructor() {}
diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts
index e3f3f8e..c828330 100644
--- a/src/app/app.routes.ts
+++ b/src/app/app.routes.ts
@@ -1,13 +1,9 @@
import { Routes } from '@angular/router';
export const routes: Routes = [
- {
- path: 'home',
- loadComponent: () => import('./home/home.page').then((m) => m.HomePage),
- },
{
path: '',
- redirectTo: 'home',
+ redirectTo: 'history',
pathMatch: 'full',
},
{
@@ -28,4 +24,9 @@ export const routes: Routes = [
loadComponent: () =>
import('./settings/settings.page').then((m) => m.SettingsPage),
},
+ {
+ path: '*',
+ loadComponent: () =>
+ import('./error/not-found/not-found.page').then((m) => m.NotFoundPage),
+ },
];
diff --git a/src/app/error/not-found/not-found.page.html b/src/app/error/not-found/not-found.page.html
new file mode 100644
index 0000000..1a01b0c
--- /dev/null
+++ b/src/app/error/not-found/not-found.page.html
@@ -0,0 +1,13 @@
+
+
+ not-found
+
+
+
+
+
+
+ not-found
+
+
+
diff --git a/src/app/error/not-found/not-found.page.scss b/src/app/error/not-found/not-found.page.scss
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/error/not-found/not-found.page.spec.ts b/src/app/error/not-found/not-found.page.spec.ts
new file mode 100644
index 0000000..e703d14
--- /dev/null
+++ b/src/app/error/not-found/not-found.page.spec.ts
@@ -0,0 +1,17 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { NotFoundPage } from './not-found.page';
+
+describe('NotFoundPage', () => {
+ let component: NotFoundPage;
+ let fixture: ComponentFixture;
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(NotFoundPage);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/error/not-found/not-found.page.ts b/src/app/error/not-found/not-found.page.ts
new file mode 100644
index 0000000..ff99281
--- /dev/null
+++ b/src/app/error/not-found/not-found.page.ts
@@ -0,0 +1,20 @@
+import { Component, OnInit } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { IonContent, IonHeader, IonTitle, IonToolbar } from '@ionic/angular/standalone';
+
+@Component({
+ selector: 'app-not-found',
+ templateUrl: './not-found.page.html',
+ styleUrls: ['./not-found.page.scss'],
+ standalone: true,
+ imports: [IonContent, IonHeader, IonTitle, IonToolbar, CommonModule, FormsModule]
+})
+export class NotFoundPage implements OnInit {
+
+ constructor() { }
+
+ ngOnInit() {
+ }
+
+}
diff --git a/src/app/history/history.page.html b/src/app/history/history.page.html
index e2427f6..ed1e98a 100644
--- a/src/app/history/history.page.html
+++ b/src/app/history/history.page.html
@@ -1,13 +1,45 @@
- history
+ History
- history
+ History
+
+
+
+
+ Clear history
+
+
+
+
+
+ `
+ {{ i.content }}
+
+
+ {{ i.timestamp | date:'short' }}
+
+
+
+
+
+
+
+
+
+ No items yet.
+
diff --git a/src/app/history/history.page.ts b/src/app/history/history.page.ts
index 2121fff..63bc101 100644
--- a/src/app/history/history.page.ts
+++ b/src/app/history/history.page.ts
@@ -1,27 +1,69 @@
import { CommonModule } from '@angular/common';
-import { Component } from '@angular/core';
-import { FormsModule } from '@angular/forms';
+import { Component, signal } from '@angular/core';
import {
- IonContent,
+ IonButton,
IonHeader,
- IonTitle,
+ IonIcon,
+ IonItem,
+ IonLabel,
+ IonList,
IonToolbar,
+ IonContent,
+ IonTitle,
+ ViewWillEnter,
} from '@ionic/angular/standalone';
+import { StorageService } from '../services/storage.service';
@Component({
+ standalone: true,
selector: 'app-history',
templateUrl: './history.page.html',
- styleUrls: ['./history.page.scss'],
- standalone: true,
+
imports: [
- IonContent,
IonHeader,
- IonTitle,
+ IonIcon,
+ IonButton,
+ IonList,
IonToolbar,
+ IonItem,
+ IonLabel,
+ IonTitle,
+ IonContent,
CommonModule,
- FormsModule,
],
})
-export class HistoryPage {
- constructor() {}
+export class HistoryPage implements ViewWillEnter {
+ list = signal>({}); // group by section
+ items: any = [];
+
+ constructor(private store: StorageService) {}
+ ionViewWillEnter(): void {
+ this.load();
+ }
+ async load() {
+ this.items = (await this.store.all()) || [];
+ const sections: Record = {};
+ const today = new Date().toDateString();
+ const yesterday = new Date(Date.now() - 864e5).toDateString();
+
+ this.items.forEach((code: string | number | Date) => {
+ const key =
+ new Date(code).toDateString() === today
+ ? 'Today'
+ : new Date(code).toDateString() === yesterday
+ ? 'Yesterday'
+ : 'Last 7 Days';
+ sections[key] ??= [];
+ sections[key].push(code as unknown as string);
+ });
+ this.list.set(sections);
+ }
+
+ clear() {
+ console.log(`clear`);
+ }
+
+ copy(content: string) {
+ console.log(`copied`);
+ }
}
diff --git a/src/app/home/home.page.html b/src/app/home/home.page.html
index 911cab7..ed8b39e 100644
--- a/src/app/home/home.page.html
+++ b/src/app/home/home.page.html
@@ -1,8 +1,6 @@
-
- Blank
-
+ Blank
@@ -14,7 +12,22 @@
-
Ready to create an app?
-
Start with Ionic UI Components
+
+
+
+ Scan Barcode/QR
+
+
+
+ View History
+
+
diff --git a/src/app/home/home.page.ts b/src/app/home/home.page.ts
index dca8b77..7c0b43c 100644
--- a/src/app/home/home.page.ts
+++ b/src/app/home/home.page.ts
@@ -1,12 +1,23 @@
import { Component } from '@angular/core';
-import { IonHeader, IonToolbar, IonTitle, IonContent } from '@ionic/angular/standalone';
+import {
+ IonButton,
+ IonContent,
+ IonHeader,
+ IonIcon,
+ IonTitle,
+ IonToolbar,
+} from '@ionic/angular/standalone';
+import { addIcons } from 'ionicons';
+import { camera, list, time } from 'ionicons/icons';
@Component({
selector: 'app-home',
templateUrl: 'home.page.html',
styleUrls: ['home.page.scss'],
- imports: [IonHeader, IonToolbar, IonTitle, IonContent],
+ imports: [IonHeader, IonToolbar, IonTitle, IonContent, IonButton, IonIcon],
})
export class HomePage {
- constructor() {}
+ constructor() {
+ addIcons({ camera, time, list });
+ }
}
diff --git a/src/app/scan/scan.page.html b/src/app/scan/scan.page.html
index 8c38c95..683622f 100644
--- a/src/app/scan/scan.page.html
+++ b/src/app/scan/scan.page.html
@@ -1,46 +1,44 @@
- Scan
+ Scanner
+
+
+
-
-
-
-
-
- Scan
-
-
-
-
- {{ isScanning ? 'Scanning…' : 'Start Scan' }}
-
+
+
+
+
+

+
+
-
-
- Cancel
-
+
+
+
+
+
+
-
-
- Last result
-
-
- {{ lastResult }}
-
-
-
+
+
diff --git a/src/app/scan/scan.page.scss b/src/app/scan/scan.page.scss
index e69de29..99a7ccd 100644
--- a/src/app/scan/scan.page.scss
+++ b/src/app/scan/scan.page.scss
@@ -0,0 +1,83 @@
+:host {
+ --overlay-padding: 32px;
+}
+
+.scanner-content {
+ position: relative;
+ --background: transparent; /* permite ver a câmera por trás quando ativarmos */
+ padding-bottom: 140px; /* espaço para action bar + fab */
+}
+
+.scan-overlay {
+ display: grid;
+ place-items: center;
+ height: calc(100% - 160px);
+ padding: var(--overlay-padding);
+ position: relative;
+}
+
+.scan-window {
+ width: 75vw;
+ height: 55vw;
+ max-width: 480px;
+ max-height: 360px;
+ border-radius: 24px;
+ border: 3px dashed var(--scan-outline);
+}
+
+.scan-watermark {
+ position: absolute;
+ width: 220px;
+ height: 220px;
+ object-fit: contain;
+ opacity: .12; /* sutil, como no mock */
+ filter: drop-shadow(0 20px 60px rgba(0,0,0,.35));
+}
+
+.scan-fab {
+ --box-shadow: 0 12px 40px rgba(0,0,0,.35);
+ ion-fab-button {
+ width: 68px;
+ height: 68px;
+ --border-radius: 50%;
+ ion-icon { font-size: 28px; }
+ }
+}
+
+.action-bar {
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ padding: 16px 20px calc(16px + env(safe-area-inset-bottom));
+ background: var(--surface);
+ box-shadow: var(--elevation-1);
+ border-top-left-radius: 20px;
+ border-top-right-radius: 20px;
+ display: flex;
+ align-items: center;
+ justify-content: space-around;
+}
+
+.action-item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 6px;
+ color: var(--ion-text-color);
+}
+
+.icon-circle {
+ width: 56px;
+ height: 56px;
+ border-radius: 50%;
+ background: var(--surface-2);
+ display: grid;
+ place-items: center;
+ ion-icon { font-size: 24px; opacity: .9; }
+}
+
+.label {
+ font-size: 12px;
+ opacity: .8;
+}
diff --git a/src/app/scan/scan.page.ts b/src/app/scan/scan.page.ts
index 168cb68..a1de747 100644
--- a/src/app/scan/scan.page.ts
+++ b/src/app/scan/scan.page.ts
@@ -1,22 +1,21 @@
import { CommonModule } from '@angular/common';
-import { Component } from '@angular/core';
+import { Component, inject } from '@angular/core';
import { FormsModule } from '@angular/forms';
import {
IonContent,
IonHeader,
IonTitle,
IonToolbar,
- IonButton,
IonIcon,
- IonCard,
- IonCardHeader,
- IonCardTitle,
- IonCardContent,
+ IonButtons,
+ IonFab,
+ IonFabButton,
} from '@ionic/angular/standalone';
import { addIcons } from 'ionicons';
-import { camera, close, checkmark, copy, time, trash } from 'ionicons/icons';
+import { settings, scan, images, flash, create, time, close } from 'ionicons/icons';
import { Qr } from '../services/qr';
-import { Storage, ScanEntry } from '../services/storage';
+import { Storage } from '../services/storage';
+import { Router } from '@angular/router';
@Component({
selector: 'app-scan',
@@ -28,23 +27,29 @@ import { Storage, ScanEntry } from '../services/storage';
IonHeader,
IonTitle,
IonToolbar,
- IonButton,
IonIcon,
- IonCard,
- IonCardHeader,
- IonCardTitle,
- IonCardContent,
+ IonButtons,
+ IonFab,
+ IonFabButton,
CommonModule,
FormsModule,
],
})
export class ScanPage {
+ private qr = inject(Qr);
+ private store = inject(Storage);
+ private router = inject(Router);
+
isScanning = false;
lastResult: string | null = null;
- saving = false;
- constructor(private qr: Qr, private store: Storage) {
- addIcons({ camera, close, checkmark, copy, time, trash });
+ constructor() {
+ addIcons({ settings, scan, images, flash, create, time, close });
+ }
+
+ async onFabClick() {
+ if (this.isScanning) return this.cancel();
+ await this.start();
}
async start() {
@@ -54,7 +59,7 @@ export class ScanPage {
const content = await this.qr.startScan();
if (content) {
this.lastResult = content;
- await this.save(content);
+ await this.store.addEntry(content);
}
} finally {
this.isScanning = false;
@@ -66,16 +71,26 @@ export class ScanPage {
this.isScanning = false;
}
- async save(content: string) {
- this.saving = true;
- try {
- await this.store.addEntry(content);
- } finally {
- this.saving = false;
- }
+ goHistory() {
+ this.router.navigateByUrl('/history');
+ }
+
+ goSettings() {
+ this.router.navigateByUrl('/settings');
+ }
+
+ openGallery() {
+ // Placeholder da POC: futura leitura por imagem/galeria
+ console.log('openGallery: not implemented in POC');
+ }
+
+ toggleFlash() {
+ // Placeholder da POC: avaliar suporte do plugin/alternativas
+ console.log('toggleFlash: not implemented in POC');
}
- copyToClipboard(text: string) {
- if (navigator?.clipboard) navigator.clipboard.writeText(text).catch(() => {});
+ createCode() {
+ // Placeholder da POC: futura tela de criação de QR
+ console.log('createCode: not implemented in POC');
}
}
diff --git a/src/app/services/qr.ts b/src/app/services/qr.ts
index 4b21aa5..a4f75f6 100644
--- a/src/app/services/qr.ts
+++ b/src/app/services/qr.ts
@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core';
-import { BarcodeScanner } from '@capacitor/barcode-scanner';
+import { CapacitorBarcodeScanner, CapacitorBarcodeScannerTypeHint } from '@capacitor/barcode-scanner';
@Injectable({
providedIn: 'root'
@@ -7,47 +7,25 @@ import { BarcodeScanner } from '@capacitor/barcode-scanner';
export class Qr {
private active = false;
- async ensurePermission(): Promise
{
- const status = await BarcodeScanner.checkPermission({ force: true });
- return status.granted === true;
- }
-
- private setBackground(active: boolean) {
- const body = document.querySelector('body');
- if (!body) return;
- if (active) {
- body.classList.add('scanner-active');
- } else {
- body.classList.remove('scanner-active');
- }
- }
-
async startScan(): Promise {
- const granted = await this.ensurePermission();
- if (!granted) return null;
-
this.active = true;
- this.setBackground(true);
- await BarcodeScanner.hideBackground();
-
try {
- const result = await BarcodeScanner.startScan();
- const content = (result as any)?.content;
+ const result = await CapacitorBarcodeScanner.scanBarcode({
+ hint: (17 as CapacitorBarcodeScannerTypeHint), // ALL
+ scanInstructions: '',
+ scanButton: false,
+ });
+ const content = (result as any)?.ScanResult ?? null;
return content ?? null;
+ } catch (e) {
+ return null;
} finally {
- await this.stopScan();
+ this.active = false;
}
}
async stopScan(): Promise {
- if (!this.active) return;
+ // Plugin atual não expõe stop; manter no-op para compatibilidade
this.active = false;
- try {
- await BarcodeScanner.showBackground();
- } catch {}
- try {
- await BarcodeScanner.stopScan();
- } catch {}
- this.setBackground(false);
}
}
diff --git a/src/app/services/storage.service.ts b/src/app/services/storage.service.ts
new file mode 100644
index 0000000..5622ec4
--- /dev/null
+++ b/src/app/services/storage.service.ts
@@ -0,0 +1,25 @@
+import { Injectable } from '@angular/core';
+import { Preferences } from '@capacitor/preferences';
+
+const KEY = 'history';
+
+@Injectable({ providedIn: 'root' })
+export class StorageService {
+ async all(): Promise {
+ const { value } = await Preferences.get({ key: KEY });
+ return value ? JSON.parse(value) : [];
+ }
+
+ async save(item: string) {
+ const list = await this.all();
+ list.unshift(item);
+ await Preferences.set({
+ key: KEY,
+ value: JSON.stringify(list.slice(0, 50)),
+ });
+ }
+
+ async clear() {
+ await Preferences.remove({ key: KEY });
+ }
+}
diff --git a/src/app/settings/settings.page.html b/src/app/settings/settings.page.html
index 003abd6..7e9c9f1 100644
--- a/src/app/settings/settings.page.html
+++ b/src/app/settings/settings.page.html
@@ -1,13 +1,45 @@
-
+
- settings
+ Settings
-
-
-
- settings
-
-
+
+
+ Preferences
+
+ App Theme
+ System
+
+
+ Language
+ English
+
+
+
+ Notifications
+
+ App Updates
+
+
+
+ Scan Notifications
+
+
+
+
+ Privacy
+
+ Data Management
+
+
+ Privacy Policy
+
+
+
+ About
+
+ App Version
+ 1.2.3
+
diff --git a/src/app/settings/settings.page.ts b/src/app/settings/settings.page.ts
index 83d889f..a0229f3 100644
--- a/src/app/settings/settings.page.ts
+++ b/src/app/settings/settings.page.ts
@@ -6,6 +6,11 @@ import {
IonHeader,
IonTitle,
IonToolbar,
+ IonNote,
+ IonToggle,
+ IonItem,
+ IonListHeader,
+ IonLabel,
} from '@ionic/angular/standalone';
@Component({
@@ -18,6 +23,11 @@ import {
IonHeader,
IonTitle,
IonToolbar,
+ IonNote,
+ IonToggle,
+ IonLabel,
+ IonItem,
+ IonListHeader,
CommonModule,
FormsModule,
],
diff --git a/src/app/tabs/tabs.page.html b/src/app/tabs/tabs.page.html
index 3f4e0c7..0f51e5f 100644
--- a/src/app/tabs/tabs.page.html
+++ b/src/app/tabs/tabs.page.html
@@ -1,13 +1,18 @@
-
-
- tabs
-
-
+
+
+
+
+ Scan
+
-
-
-
- tabs
-
-
-
+
+
+ History
+
+
+
+
+ Settings
+
+
+
diff --git a/src/app/tabs/tabs.page.ts b/src/app/tabs/tabs.page.ts
index 7558e6d..5e9f42e 100644
--- a/src/app/tabs/tabs.page.ts
+++ b/src/app/tabs/tabs.page.ts
@@ -2,26 +2,38 @@ import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import {
- IonContent,
- IonHeader,
- IonTitle,
- IonToolbar,
+ IonIcon,
+ IonLabel,
+ IonTabBar,
+ IonTabButton,
+ IonTabs,
} from '@ionic/angular/standalone';
+import { addIcons } from 'ionicons';
+
+import { settingsOutline, refreshOutline, qrCodeOutline } from 'ionicons/icons';
+
@Component({
selector: 'app-tabs',
templateUrl: './tabs.page.html',
styleUrls: ['./tabs.page.scss'],
standalone: true,
imports: [
- IonContent,
- IonHeader,
- IonTitle,
- IonToolbar,
+ IonTabs,
+ IonTabBar,
+ IonTabButton,
+ IonIcon,
+ IonLabel,
CommonModule,
FormsModule,
],
})
export class TabsPage {
- constructor() {}
+ constructor() {
+ addIcons({
+ settingsOutline,
+ refreshOutline,
+ qrCodeOutline,
+ });
+ }
}
diff --git a/src/global.scss b/src/global.scss
index 999ff53..121d308 100644
--- a/src/global.scss
+++ b/src/global.scss
@@ -35,3 +35,9 @@
/* @import "@ionic/angular/css/palettes/dark.always.css"; */
/* @import "@ionic/angular/css/palettes/dark.class.css"; */
@import '@ionic/angular/css/palettes/dark.system.css';
+
+/* Transparent background when scanner active to reveal camera preview */
+body.scanner-active {
+ background: transparent !important;
+}
+
diff --git a/src/index.html b/src/index.html
index 29902c2..52f303f 100644
--- a/src/index.html
+++ b/src/index.html
@@ -12,7 +12,8 @@
-
+
+
diff --git a/src/theme/variables.scss b/src/theme/variables.scss
index 6146c39..169d8d5 100644
--- a/src/theme/variables.scss
+++ b/src/theme/variables.scss
@@ -1,2 +1,174 @@
// For information on how to create your own theme, please see:
// http://ionicframework.com/docs/theming/
+/* ==========================================================================
+ Pink-Code · Ionic 8 Design Tokens
+ Place this complete file at: src/theme/variables.scss
+ ========================================================================== */
+
+/* 1 ▪ Base palette (Light mode) */
+:root {
+ /* --- Brand ------------------------------------------------------------- */
+ --ion-color-primary: #ff5eae;
+ --ion-color-primary-rgb: 255, 94, 174;
+ --ion-color-primary-contrast: #ffffff;
+ --ion-color-primary-contrast-rgb: 255, 255, 255;
+ --ion-color-primary-shade: #e0559c;
+ --ion-color-primary-tint: #ff7ab9;
+
+ /* Optional secondary / tertiary accents */
+ --ion-color-secondary: #ffb347; /* laranja suave */
+ --ion-color-tertiary: #5ec6ff; /* azul para estados neutros */
+ /* Secondary full token set */
+ --ion-color-secondary-rgb: 255, 179, 71;
+ --ion-color-secondary-contrast: #ffffff;
+ --ion-color-secondary-contrast-rgb: 255, 255, 255;
+ --ion-color-secondary-shade: #e09e3f;
+ --ion-color-secondary-tint: #ffc46a;
+ /* Tertiary full token set */
+ --ion-color-tertiary-rgb: 94, 198, 255;
+ --ion-color-tertiary-contrast: #ffffff;
+ --ion-color-tertiary-contrast-rgb: 255, 255, 255;
+ --ion-color-tertiary-shade: #52addf;
+ --ion-color-tertiary-tint: #7bd1ff;
+
+ /* Neutral greys */
+ --ion-color-step-50: #f9f9fa;
+ --ion-color-step-100: #f4f5f8;
+ --ion-color-step-150: #e9eaee;
+ --ion-color-step-200: #dcdde3;
+ --ion-color-step-250: #c3c5ce;
+ --ion-color-step-300: #a9acb8;
+ --ion-color-step-350: #9194a1;
+ --ion-color-step-400: #7a7d8b;
+ --ion-color-step-450: #636673;
+ --ion-color-step-500: #4c4f5b;
+ --ion-color-step-550: #373a44;
+ --ion-color-step-600: #272a33;
+
+ /* Text & surface */
+ --ion-text-color: var(--ion-color-step-500);
+ --ion-background-color: var(--ion-color-step-50);
+ --ion-card-background: #ffffff;
+ --ion-item-background: #ffffff;
+
+ /* Status colors (optional) */
+ --ion-color-success: #4cd964;
+ --ion-color-success-rgb: 76, 217, 100;
+ --ion-color-success-contrast: #ffffff;
+ --ion-color-success-contrast-rgb: 255, 255, 255;
+ --ion-color-success-shade: #42bf58;
+ --ion-color-success-tint: #69e07d;
+
+ --ion-color-warning: #ffcc00;
+ --ion-color-warning-rgb: 255, 204, 0;
+ --ion-color-warning-contrast: #1f222a;
+ --ion-color-warning-contrast-rgb: 31, 34, 42;
+ --ion-color-warning-shade: #e0b400;
+ --ion-color-warning-tint: #ffd52b;
+
+ --ion-color-danger: #ff3b30;
+ --ion-color-danger-rgb: 255, 59, 48;
+ --ion-color-danger-contrast: #ffffff;
+ --ion-color-danger-contrast-rgb: 255, 255, 255;
+ --ion-color-danger-shade: #e0352b;
+ --ion-color-danger-tint: #ff5c52;
+
+ /* Surfaces & elevation */
+ --surface: #ffffff;
+ --surface-2: var(--ion-color-step-100);
+ --elevation-1: 0 8px 24px rgba(0, 0, 0, 0.12);
+
+ /* Scanner outline */
+ --scan-outline: var(--ion-color-primary);
+}
+
+/* 2 ▪ Dark mode overrides */
+@media (prefers-color-scheme: dark) {
+ :root {
+ --ion-color-primary: #ff7ab9; /* tom mais claro p/ contraste */
+ --ion-color-primary-rgb: 255, 122, 185;
+ --ion-color-primary-contrast: #ffffff;
+ --ion-color-primary-contrast-rgb: 255, 255, 255;
+ --ion-color-primary-shade: #e467a3;
+ --ion-color-primary-tint: #ff93c5;
+
+ /* Neutrals flipped */
+ --ion-color-step-50: #1f222a;
+ --ion-color-step-100: #242731;
+ --ion-color-step-150: #2a2e38;
+ --ion-color-step-200: #30333e;
+ --ion-color-step-250: #363a45;
+ --ion-color-step-300: #3d404c;
+ --ion-color-step-350: #454854;
+ --ion-color-step-400: #4d505d;
+ --ion-color-step-450: #5a5d6a;
+ --ion-color-step-500: #dadde5; /* texto principal #E4E7EC like */
+ --ion-color-step-550: #e4e7ec;
+ --ion-color-step-600: #f0f1f5;
+
+ --ion-text-color: var(--ion-color-step-550);
+ --ion-background-color: var(--ion-color-step-50);
+ --ion-card-background: #2a2e38;
+ --ion-item-background: #2a2e38;
+
+ /* Secondary/Tertiary full sets in dark */
+ --ion-color-secondary-rgb: 255, 179, 71;
+ --ion-color-secondary-contrast: #ffffff;
+ --ion-color-secondary-contrast-rgb: 255, 255, 255;
+ --ion-color-secondary-shade: #e09e3f;
+ --ion-color-secondary-tint: #ffc46a;
+
+ --ion-color-tertiary-rgb: 94, 198, 255;
+ --ion-color-tertiary-contrast: #ffffff;
+ --ion-color-tertiary-contrast-rgb: 255, 255, 255;
+ --ion-color-tertiary-shade: #52addf;
+ --ion-color-tertiary-tint: #7bd1ff;
+
+ --ion-color-success-rgb: 76, 217, 100;
+ --ion-color-success-contrast: #1f222a;
+ --ion-color-success-contrast-rgb: 31, 34, 42;
+ --ion-color-success-shade: #42bf58;
+ --ion-color-success-tint: #69e07d;
+
+ --ion-color-warning-rgb: 255, 204, 0;
+ --ion-color-warning-contrast: #1f222a;
+ --ion-color-warning-contrast-rgb: 31, 34, 42;
+ --ion-color-warning-shade: #e0b400;
+ --ion-color-warning-tint: #ffd52b;
+
+ --ion-color-danger-rgb: 255, 59, 48;
+ --ion-color-danger-contrast: #ffffff;
+ --ion-color-danger-contrast-rgb: 255, 255, 255;
+ --ion-color-danger-shade: #e0352b;
+ --ion-color-danger-tint: #ff5c52;
+
+ /* Surfaces & elevation */
+ --surface: #2a2e38;
+ --surface-2: #242731;
+ --elevation-1: 0 12px 36px rgba(0, 0, 0, 0.45);
+
+ /* Scanner outline */
+ --scan-outline: var(--ion-color-primary);
+ }
+}
+
+/* 3 ▪ Typography helpers */
+ion-title {
+ font-weight: 600;
+}
+
+ion-card-title, ion-label {
+ line-height: 1.3;
+}
+
+/* 4 ▪ Button tweaks (optional) */
+ion-button {
+ --border-radius: 24px;
+ font-weight: 600;
+}
+
+/* Tabs active color aligned to brand */
+ion-tab-bar {
+ --color-selected: var(--ion-color-primary);
+}
+
\ No newline at end of file