diff --git a/.github/workflows/deploy-cloudflare.yml b/.github/workflows/deploy-cloudflare.yml index 2bed689..dcfabd4 100644 --- a/.github/workflows/deploy-cloudflare.yml +++ b/.github/workflows/deploy-cloudflare.yml @@ -6,6 +6,8 @@ on: types: [completed] branches: [main] workflow_dispatch: + schedule: + - cron: '17 3 * * *' permissions: contents: read @@ -21,7 +23,8 @@ jobs: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push' && github.event.workflow_run.head_sha == github.sha) || - (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') + (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') || + github.event_name == 'schedule' runs-on: ubuntu-latest timeout-minutes: 20 environment: @@ -43,7 +46,9 @@ jobs: run: npm ci - name: Build production assets - run: npm run build + run: npm run sponsors:generate && npm run build + env: + GITHUB_TOKEN: ${{ github.token }} - name: Deploy to Cloudflare run: npx wrangler deploy diff --git a/package.json b/package.json index e3e6df9..f2bc44c 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "scripts": { "ng": "ng", "docs:generate": "tsx scripts/generate-docs.ts", + "sponsors:generate": "tsx scripts/generate-sponsors.ts", "prestart": "npm run docs:generate", "start": "ng serve", "prestart:ja": "npm run docs:generate", @@ -17,7 +18,7 @@ "build": "ng build && npm run static:prepare && pagefind --site dist/capacitor-plugins-docs/browser --output-subdir pagefind && tsx --test scripts/build-output.test.ts", "watch": "ng build --watch --configuration development", "pretest": "npm run docs:generate", - "test": "tsx --test scripts/docgen-readme.test.ts scripts/html-policy.test.ts scripts/markdown-headings.test.ts scripts/package-markdown.test.ts scripts/package-repository.test.ts scripts/bilingual-update-blocker.test.ts scripts/site-contract.test.ts && ng test --watch=false", + "test": "tsx --test scripts/docgen-readme.test.ts scripts/html-policy.test.ts scripts/markdown-headings.test.ts scripts/package-markdown.test.ts scripts/package-repository.test.ts scripts/github-sponsors.test.ts scripts/bilingual-update-blocker.test.ts scripts/site-contract.test.ts && ng test --watch=false", "fmt": "prettier --write .", "fmt:check": "prettier --check .", "deploy": "npm run build && wrangler deploy", diff --git a/scripts/build-output.test.ts b/scripts/build-output.test.ts index dd50522..4bb882b 100644 --- a/scripts/build-output.test.ts +++ b/scripts/build-output.test.ts @@ -2,6 +2,7 @@ import assert from 'node:assert/strict'; import { access, constants, readdir, readFile, stat } from 'node:fs/promises'; import { join } from 'node:path'; import test from 'node:test'; +import { CURRENT_SPONSORS, PAST_SPONSORS } from '../src/app/generated/sponsors.generated'; test('places locale-specific static 404 pages in the browser output', async () => { const [english, japanese] = await Promise.all([ @@ -52,6 +53,34 @@ test('prerender output includes localized SEO metadata', async () => { assert.match(html, /name="twitter:card" content="summary_large_image"/); }); +test('prerenders current and past public sponsors in both locales', async () => { + const [english, japanese] = await Promise.all([ + readFile( + new URL('../dist/capacitor-plugins-docs/browser/support/index.html', import.meta.url), + 'utf8', + ), + readFile( + new URL('../dist/capacitor-plugins-docs/browser/ja/support/index.html', import.meta.url), + 'utf8', + ), + ]); + + for (const html of [english, japanese]) { + for (const sponsor of [...CURRENT_SPONSORS, ...PAST_SPONSORS]) { + assert.match(html, new RegExp(`href="${sponsor.profileUrl}"`)); + } + assert.doesNotMatch(html, /\$\d/); + } + if (CURRENT_SPONSORS.length > 0) { + assert.match(english, />Current sponsors); + assert.match(japanese, />現在のスポンサー); + } + if (PAST_SPONSORS.length > 0) { + assert.match(english, />Past sponsors); + assert.match(japanese, />過去のスポンサー); + } +}); + test('Japanese home prerender uses slashless canonical SEO URLs', async () => { const html = await readFile( new URL('../dist/capacitor-plugins-docs/browser/ja/index.html', import.meta.url), diff --git a/scripts/generate-sponsors.ts b/scripts/generate-sponsors.ts new file mode 100644 index 0000000..9df7b09 --- /dev/null +++ b/scripts/generate-sponsors.ts @@ -0,0 +1,10 @@ +import { writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { fetchPublicSponsors, sponsorsModule } from './github-sponsors'; + +const outputPath = join(process.cwd(), 'src/app/generated/sponsors.generated.ts'); +const sponsors = await fetchPublicSponsors('rdlabo', process.env['GITHUB_TOKEN'] ?? ''); +await writeFile(outputPath, sponsorsModule(sponsors)); +console.log( + `Generated ${sponsors.current.length} current and ${sponsors.past.length} past public sponsors.`, +); diff --git a/scripts/github-sponsors.test.ts b/scripts/github-sponsors.test.ts new file mode 100644 index 0000000..6c0a12d --- /dev/null +++ b/scripts/github-sponsors.test.ts @@ -0,0 +1,118 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { fetchPublicSponsors, sponsorsModule } from './github-sponsors'; + +function githubResponse(body: unknown): Response { + return new Response(JSON.stringify(body), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); +} + +test('fetches only valid public sponsors and sorts by tier without publishing amounts', async () => { + const requests: { activeOnly: boolean; cursor: string | null }[] = []; + const smallSponsor = { + sponsorEntity: { + login: 'small', + name: null, + avatarUrl: 'https://avatars.githubusercontent.com/u/1?v=4', + url: 'https://github.com/small', + }, + tier: { monthlyPriceInDollars: 5 }, + }; + const largeSponsor = { + sponsorEntity: { + login: 'large', + name: 'Large Sponsor', + avatarUrl: 'https://avatars.githubusercontent.com/u/2?v=4', + url: 'https://github.com/large', + }, + tier: { monthlyPriceInDollars: 100 }, + }; + const pastSponsor = { + sponsorEntity: { + login: 'one-time', + name: 'One-time Sponsor', + avatarUrl: 'https://avatars.githubusercontent.com/u/3?v=4', + url: 'https://github.com/one-time', + }, + tier: null, + }; + const fetchImplementation: typeof fetch = async (_input, init) => { + const body = JSON.parse(String(init?.body)) as { + variables: { activeOnly: boolean; cursor: string | null }; + }; + const { activeOnly, cursor } = body.variables; + requests.push({ activeOnly, cursor }); + const nodes = activeOnly + ? cursor + ? [largeSponsor] + : [ + smallSponsor, + { + sponsorEntity: { + login: 'unsafe', + name: 'Unsafe image', + avatarUrl: 'https://example.com/avatar.png', + url: 'https://github.com/unsafe', + }, + tier: { monthlyPriceInDollars: 10_000 }, + }, + ] + : [smallSponsor, largeSponsor, largeSponsor, pastSponsor]; + return githubResponse({ + data: { + user: { + sponsorshipsAsMaintainer: { + nodes, + pageInfo: { + hasNextPage: activeOnly && cursor === null, + endCursor: activeOnly && cursor === null ? 'next' : null, + }, + }, + }, + }, + }); + }; + + const sponsors = await fetchPublicSponsors('rdlabo', 'test-token', fetchImplementation); + + assert.deepEqual(requests, [ + { activeOnly: true, cursor: null }, + { activeOnly: false, cursor: null }, + { activeOnly: true, cursor: 'next' }, + ]); + assert.deepEqual(sponsors, { + current: [ + { + login: 'large', + name: 'Large Sponsor', + avatarUrl: 'https://avatars.githubusercontent.com/u/2?v=4', + profileUrl: 'https://github.com/large', + }, + { + login: 'small', + name: 'small', + avatarUrl: 'https://avatars.githubusercontent.com/u/1?v=4', + profileUrl: 'https://github.com/small', + }, + ], + past: [ + { + login: 'one-time', + name: 'One-time Sponsor', + avatarUrl: 'https://avatars.githubusercontent.com/u/3?v=4', + profileUrl: 'https://github.com/one-time', + }, + ], + }); + assert.doesNotMatch(sponsorsModule(sponsors), /monthlyPriceInDollars/); +}); + +test('fails closed when GitHub rejects the request', async () => { + const fetchImplementation: typeof fetch = async () => new Response('', { status: 401 }); + await assert.rejects( + () => fetchPublicSponsors('rdlabo', 'bad-token', fetchImplementation), + /HTTP 401/, + ); +}); diff --git a/scripts/github-sponsors.ts b/scripts/github-sponsors.ts new file mode 100644 index 0000000..ec504a2 --- /dev/null +++ b/scripts/github-sponsors.ts @@ -0,0 +1,184 @@ +export interface PublicSponsor { + login: string; + name: string; + avatarUrl: string; + profileUrl: string; +} + +export interface PublicSponsors { + current: PublicSponsor[]; + past: PublicSponsor[]; +} + +interface SponsorWithTier extends PublicSponsor { + monthlyPriceInDollars: number; +} + +interface SponsorshipNode { + sponsorEntity?: { + login?: unknown; + name?: unknown; + avatarUrl?: unknown; + url?: unknown; + } | null; + tier?: { monthlyPriceInDollars?: unknown } | null; +} + +interface SponsorsResponse { + data?: { + user?: { + sponsorshipsAsMaintainer?: { + nodes?: SponsorshipNode[]; + pageInfo?: { hasNextPage?: boolean; endCursor?: string | null }; + }; + } | null; + }; + errors?: { message?: string }[]; +} + +const query = ` + query PublicSponsors($login: String!, $cursor: String, $activeOnly: Boolean!) { + user(login: $login) { + sponsorshipsAsMaintainer( + first: 100 + after: $cursor + activeOnly: $activeOnly + includePrivate: false + ) { + nodes { + sponsorEntity { + ... on User { login name avatarUrl url } + ... on Organization { login name avatarUrl url } + } + tier { monthlyPriceInDollars } + } + pageInfo { hasNextPage endCursor } + } + } + } +`; + +function isExpectedUrl(value: unknown, hostname: string): value is string { + if (typeof value !== 'string') return false; + try { + const url = new URL(value); + return url.protocol === 'https:' && url.hostname === hostname; + } catch { + return false; + } +} + +function parseSponsor(node: SponsorshipNode): SponsorWithTier | undefined { + const entity = node.sponsorEntity; + const monthlyPrice = node.tier?.monthlyPriceInDollars; + if ( + typeof entity?.login !== 'string' || + entity.login.length === 0 || + !isExpectedUrl(entity.avatarUrl, 'avatars.githubusercontent.com') || + !isExpectedUrl(entity.url, 'github.com') || + (monthlyPrice !== null && + monthlyPrice !== undefined && + (typeof monthlyPrice !== 'number' || !Number.isFinite(monthlyPrice))) + ) { + return undefined; + } + return { + login: entity.login, + name: + typeof entity.name === 'string' && entity.name.trim().length > 0 + ? entity.name.trim() + : entity.login, + avatarUrl: entity.avatarUrl, + profileUrl: entity.url, + monthlyPriceInDollars: typeof monthlyPrice === 'number' ? monthlyPrice : 0, + }; +} + +async function fetchSponsorships( + login: string, + token: string, + activeOnly: boolean, + fetchImplementation: typeof fetch = fetch, +): Promise
+ @for (group of sponsorGroups; track group.id) { + @if (group.sponsors.length > 0) { +
+ @if (group.id === 'current') {
+
Capacitor Brother Print is a native Brother Print SDK implementation for iOS & Android. Support These models.
\nThis plugin is still in the RC (release candidate) phase.
\nBrother Print SDK is incompatible with CocoaPods and Minimum Developments iOS 14 and is not working at this time, please use Swift Package Manager.
\nEach product link is an Amazon affiliate link. If you choose to make a purchase through these links, it would be greatly appreciated and would help support development costs. Thank you!
\n| Product | \nModel | \niOS/WiFi | \niOS/BT | \niOS/BLE | \nAndroid/USB | \nAndroid/WiFi | \nAndroid/BT | \nAndroid/BLE | \n
|---|---|---|---|---|---|---|---|---|
| QL-810W | \nQL_810W | \n✗ | \n✗ | \n✗ | \n◯ | \n✗ | \n✗ | \n✗ | \n
| QL-820NWB | \nQL_820NWB | \n◯ | \n※1 | \n✗ | \n△ | \n◯ | \n△ | \n✗ | \n
| QL-820NWBc | \nQL_820NWB | \n◯ | \n※2 | \n✗ | \n✗ | \n◯ | \n◯ | \n✗ | \n
| TD-2320D | \nTD_2320D_203 | \n✗ | \n✗ | \n✗ | \n△ | \n✗ | \n✗ | \n✗ | \n
| TD-2350D | \nTD_2350D_300 | \n◯ | \n△ | \n△ | \n◯ | \n◯ | \n◯ | \n△ | \n
Amazon Affiliate Links: https://amzn.to/3AiiOFT
\nSupplement
\n| \n | description | \n
|---|---|
| ◯ | \nSupported and tested | \n
| △ | \nImplemented but not tested | \n
| - | \nPlugin is not supported | \n
| ✗ | \nDevice is not supported | \n
| BT | \nBluetooth | \n
| BLE | \nBluetooth Low Energy | \n
※1 Due to low Bluetooth version, connection is not possible with iOS. Ref: https://okbizcs.okwave.jp/brother/qa/q9932082.html
\n※2 The iOS/BT implementation for the QL-820NWBc is in place, but it’s uncertain if it functions correctly. It’s unclear whether this is an implementation issue, as Brother’s official app also doesn’t work well.
\n% npm install @rdlabo/capacitor-brotherprint\nFor detailed SDK setup and permission configuration, see Installation.
\nSee Search, Print, and Events.
\n", + "html": "Capacitor Brother Print is a native Brother Print SDK implementation for iOS & Android. Support These models.
\nThis plugin is still in the RC (release candidate) phase.
\nBrother Print SDK is incompatible with CocoaPods and Minimum Developments iOS 14 and is not working at this time, please use Swift Package Manager.
\nEach product link is an Amazon affiliate link. If you choose to make a purchase through these links, it would be greatly appreciated and would help support development costs. Thank you!
\n| Product | \nModel | \niOS/WiFi | \niOS/BT | \niOS/BLE | \nAndroid/USB | \nAndroid/WiFi | \nAndroid/BT | \nAndroid/BLE | \n
|---|---|---|---|---|---|---|---|---|
| QL-810W | \nQL_810W | \n❌ | \n❌ | \n❌ | \n✅ | \n❌ | \n❌ | \n❌ | \n
| QL-820NWB | \nQL_820NWB | \n✅ | \n※1 | \n❌ | \n△ | \n✅ | \n△ | \n❌ | \n
| QL-820NWBc | \nQL_820NWB | \n✅ | \n※2 | \n❌ | \n❌ | \n✅ | \n✅ | \n❌ | \n
| TD-2320D | \nTD_2320D_203 | \n❌ | \n❌ | \n❌ | \n△ | \n❌ | \n❌ | \n❌ | \n
| TD-2350D | \nTD_2350D_300 | \n✅ | \n△ | \n△ | \n✅ | \n✅ | \n✅ | \n△ | \n
Amazon Affiliate Links: https://amzn.to/3AiiOFT
\nSupplement
\n| \n | description | \n
|---|---|
| ✅ | \nSupported and tested | \n
| △ | \nImplemented but not tested | \n
| - | \nPlugin is not supported | \n
| ❌ | \nDevice is not supported | \n
| BT | \nBluetooth | \n
| BLE | \nBluetooth Low Energy | \n
※1 Due to low Bluetooth version, connection is not possible with iOS. Ref: https://okbizcs.okwave.jp/brother/qa/q9932082.html
\n※2 The iOS/BT implementation for the QL-820NWBc is in place, but it’s uncertain if it functions correctly. It’s unclear whether this is an implementation issue, as Brother’s official app also doesn’t work well.
\n% npm install @rdlabo/capacitor-brotherprint\nFor detailed SDK setup and permission configuration, see Installation.
\nSee Search, Print, and Events.
\n", "headings": [ { "id": "supported-models", diff --git a/src/app/generated/projects/ionic-angular-photo-editor.en.generated.ts b/src/app/generated/projects/ionic-angular-photo-editor.en.generated.ts index 75629a1..2c747e5 100644 --- a/src/app/generated/projects/ionic-angular-photo-editor.en.generated.ts +++ b/src/app/generated/projects/ionic-angular-photo-editor.en.generated.ts @@ -36,7 +36,7 @@ export const PROJECT = { "file": "readme.md", "section": "Quickstart", "path": "/projects/ionic-angular-photo-editor/docs/readme", - "html": "This is a photo editor and viewer for modal page of Ionic Angular project using Capacitor.
\n| Goal | \nGuide | \n
|---|---|
| Load a photo from camera or album | \nPhotoFileService | \n
| Crop and edit in a modal | \nPhoto Editor | \n
| Browse images in a modal | \nPhoto Viewer | \n
| Override editor colors | \nTheme | \n
After Installation, load a photo:
\nimport { PhotoFileService } from '@rdlabo/ionic-angular-photo-editor';\n\nconst files = await this.photoFileService.loadPhoto(1);\nThen present the editor or viewer. Details: PhotoFileService, Photo Editor, Photo Viewer.
\nnpm install @rdlabo/ionic-angular-photo-editor\nIf you use capacitor, you need to install plugin:
\nnpm install @capacitor/camera swiper tui-image-editor\nAnd set permission. more info is here: Camera
\nIf you public your project to the web, you need to add the following input tag to the index.html.
\n<div style=\"width: 0; height: 0; overflow: hidden\">\n <input id=\"browserPhotoUploader\" type=\"file\" accept=\"image/*\" />\n</div>\nStart with Installation, then pick a guide.
\nThis is a photo editor and viewer for modal page of Ionic Angular project using Capacitor.
\n| Goal | \nGuide | \n
|---|---|
| Load a photo from camera or album | \nPhotoFileService | \n
| Crop and edit in a modal | \nPhoto Editor | \n
| Browse images in a modal | \nPhoto Viewer | \n
| Override editor colors | \nTheme | \n
| Upgrade from an earlier release | \nMigration guide | \n
After Installation, load a photo:
\nimport { PhotoFileService } from '@rdlabo/ionic-angular-photo-editor';\n\nconst files = await this.photoFileService.loadPhoto(1);\nThen present the editor or viewer. Details: PhotoFileService, Photo Editor, Photo Viewer.
\nnpm install @rdlabo/ionic-angular-photo-editor\nIf you use capacitor, you need to install plugin:
\nnpm install @capacitor/camera swiper tui-image-editor\nAnd set permission. more info is here: Camera
\nIf you public your project to the web, you need to add the following input tag to the index.html.
\n<div style=\"width: 0; height: 0; overflow: hidden\">\n <input id=\"browserPhotoUploader\" type=\"file\" accept=\"image/*\" />\n</div>\nStart with Installation, then pick a guide.
\nOverride the editor colors after Installation.
\nDefault color is set, but user can overwrite it: https://github.com/rdlabo-dev/ionic-angular-library/blob/v21.6.2/projects/photo-editor/src/lib/pages/core.scss
\n:root {\n --ion-photo-editor-background: #2a2a2a;\n --ion-photo-editor-background-tint: #414141;\n\n --ion-photo-editor-color: #f0f0f0;\n --ion-photo-editor-color-tint: #dbdbdb;\n\n --ion-photo-editor-primary: #4d8dff;\n --ion-photo-editor-danger: #f24c58;\n --ion-photo-editor-success: #2dd55b;\n}\nOverride the editor colors after Installation.
\nDefault color is set, but user can overwrite it: https://github.com/rdlabo-dev/ionic-angular-library/blob/v21.6.2/projects/photo-editor/src/lib/pages/core.scss
\n:root {\n --ion-photo-editor-background: #2a2a2a;\n --ion-photo-editor-background-tint: #414141;\n\n --ion-photo-editor-color: #f0f0f0;\n --ion-photo-editor-color-tint: #dbdbdb;\n\n --ion-photo-editor-primary: #4d8dff;\n --ion-photo-editor-danger: #f24c58;\n --ion-photo-editor-success: #2dd55b;\n\n --ion-photo-editor-header-button-color-on-light: #222428;\n --ion-photo-editor-header-button-color-on-dark: #f4f5f8;\n}\nPhotoEditorPage and PhotoViewerPage require headerButtonColorScheme: 'light' | 'dark' in their modal componentProps. Select dark for a dark/black ion-toolbar and light for a light/white toolbar. The consumer must choose because the library cannot reliably infer the final toolbar appearance from CSS, translucency, or runtime theme overrides.
For @rdlabo/ionic-theme-ios26 v3, import the optional integration stylesheet after the iOS 26 theme and dark-mode styles:
@import '@rdlabo/ionic-theme-ios26/dist/css/ionic-theme-ios26.css';\n@import '@ionic/angular/css/palettes/dark.class.css';\n@import '@rdlabo/ionic-theme-ios26/dist/css/ionic-theme-ios26-dark-class.css';\n@import '@rdlabo/ionic-angular-photo-editor/css/ios26-header-button-color-scheme.css';\nUse the matching Always or System dark-mode import instead when appropriate. The photo-editor integration stylesheet must remain last so its local header scheme can override the ambient application scheme. Applications that do not use the iOS 26 theme should not import this optional stylesheet; they receive only the regular Ionic button foreground-color switch.
\n", "headings": [ { "id": "how-to-overwrite", "text": "How to overwrite", "level": 2 + }, + { + "id": "header-button-color-scheme", + "text": "Header button color scheme", + "level": 2 } ], "codes": [], @@ -118,7 +123,7 @@ export const PROJECT = { "file": "editor.md", "section": "Guides", "path": "/projects/ionic-angular-photo-editor/docs/editor", - "html": "Present PhotoEditorPage in an Ionic modal. Call this after Installation.
import { PhotoEditorPage, IPhotoEditorDismiss } from '@rdlabo/ionic-angular-photo-editor';\n\n(async () => {\n const modal = await this.modalCtrl.create({\n component: PhotoEditorPage,\n componentProps: {\n requireSquare: false,\n value: 'https://picsum.photos/200/300',\n labels: {\n save: '送信', // change '保存' to '送信'\n },\n },\n });\n await modal.present();\n const { data } = await modal.onWillDismiss<IPhotoEditorDismiss>();\n if (data?.value) {\n console.log(data.value);\n }\n})();\nIf true, the image must be cropped to a square at first.
\nThe image url or base64 string.
\nIf set, the label is overwritten.
\nList is here.
\n", + "html": "Present PhotoEditorPage in an Ionic modal. Call this after Installation.
import { PhotoEditorPage, IPhotoEditorDismiss, PhotoEditorProps } from '@rdlabo/ionic-angular-photo-editor';\n\n(async () => {\n const componentProps = {\n requireSquare: false,\n value: 'https://picsum.photos/200/300',\n headerButtonColorScheme: 'dark',\n labels: {\n save: '送信', // change '保存' to '送信'\n },\n } satisfies PhotoEditorProps;\n const modal = await this.modalCtrl.create({\n component: PhotoEditorPage,\n componentProps,\n });\n await modal.present();\n const { data } = await modal.onWillDismiss<IPhotoEditorDismiss>();\n if (data?.value) {\n console.log(data.value);\n }\n})();\nIf true, the image must be cropped to a square at first.
\nThe image url or base64 string.
\nIf set, the label is overwritten.
\nList is here.
\nRequired. Select dark for a dark/black ion-toolbar and light for a light/white toolbar. The library cannot infer the toolbar appearance from CSS, translucent content, or runtime theme overrides.
Present PhotoViewerPage in an Ionic modal. Call this after Installation.
import { PhotoViewerPage, IPhotoViewerDismiss } from '@rdlabo/ionic-angular-photo-editor';\n\n(async () => {\n const modal = await this.modalCtrl.create({\n component: PhotoViewerPage,\n componentProps: {\n imageUrls: ['https://picsum.photos/200/300', 'https://picsum.photos/200/300'],\n index: 0,\n isCircle: false,\n },\n });\n await modal.present();\n const { data } = await modal.onWillDismiss<IPhotoViewerDismiss>();\n if (data?.delete) {\n // User delete image\n }\n})();\nThe image url or base64 string[].
\nThe index of imageUrls.
\nIf set, the image is displayed in a circle.
\nIf true, the delete button is displayed.
\nIf true, enable footer safe area for iOS.
\nIf set, the label is overwritten.
\nList is here.
\n", + "html": "Present PhotoViewerPage in an Ionic modal. Call this after Installation.
import { PhotoViewerPage, IPhotoViewerDismiss, PhotoViewerProps } from '@rdlabo/ionic-angular-photo-editor';\n\n(async () => {\n const componentProps = {\n imageUrls: ['https://picsum.photos/200/300', 'https://picsum.photos/200/300'],\n index: 0,\n isCircle: false,\n headerButtonColorScheme: 'dark',\n } satisfies PhotoViewerProps;\n const modal = await this.modalCtrl.create({\n component: PhotoViewerPage,\n componentProps,\n });\n await modal.present();\n const { data } = await modal.onWillDismiss<IPhotoViewerDismiss>();\n if (data?.delete) {\n // User delete image\n }\n})();\nThe image url or base64 string[].
\nThe index of imageUrls.
\nIf set, the image is displayed in a circle.
\nIf true, the delete button is displayed.
\nIf true, enable footer safe area for iOS.
\nIf set, the label is overwritten.
\nList is here.
\nRequired. Select dark for a dark/black ion-toolbar and light for a light/white toolbar. The library cannot infer the toolbar appearance from CSS, translucent content, or runtime theme overrides.
インストール のあと、エディターの色を上書きします。
\nデフォルトの色は設定済みですが、上書きできます: https://github.com/rdlabo-dev/ionic-angular-library/blob/v21.6.2/projects/photo-editor/src/lib/pages/core.scss
\n:root {\n --ion-photo-editor-background: #2a2a2a;\n --ion-photo-editor-background-tint: #414141;\n\n --ion-photo-editor-color: #f0f0f0;\n --ion-photo-editor-color-tint: #dbdbdb;\n\n --ion-photo-editor-primary: #4d8dff;\n --ion-photo-editor-danger: #f24c58;\n --ion-photo-editor-success: #2dd55b;\n}\nインストール のあと、エディターの色を上書きします。
\nデフォルトの色は設定済みですが、上書きできます: https://github.com/rdlabo-dev/ionic-angular-library/blob/v21.6.2/projects/photo-editor/src/lib/pages/core.scss
\n:root {\n --ion-photo-editor-background: #2a2a2a;\n --ion-photo-editor-background-tint: #414141;\n\n --ion-photo-editor-color: #f0f0f0;\n --ion-photo-editor-color-tint: #dbdbdb;\n\n --ion-photo-editor-primary: #4d8dff;\n --ion-photo-editor-danger: #f24c58;\n --ion-photo-editor-success: #2dd55b;\n\n --ion-photo-editor-header-button-color-on-light: #222428;\n --ion-photo-editor-header-button-color-on-dark: #f4f5f8;\n}\nPhotoEditorPage と PhotoViewerPage では、モーダルの componentProps に headerButtonColorScheme: 'light' | 'dark' を指定する必要があります。ion-toolbar が暗色または黒色の場合は dark、明色または白色の場合は light を選択してください。最終的なツールバーの外観はCSS、半透明効果、実行時のテーマ上書きによって変わるため、ライブラリ側では確実に判定できません。利用側で明示的に選択する必要があります。
@rdlabo/ionic-theme-ios26 v3では、iOS 26テーマとダークモードのスタイルよりあとに、オプションの連携スタイルシートを読み込みます。
@import '@rdlabo/ionic-theme-ios26/dist/css/ionic-theme-ios26.css';\n@import '@ionic/angular/css/palettes/dark.class.css';\n@import '@rdlabo/ionic-theme-ios26/dist/css/ionic-theme-ios26-dark-class.css';\n@import '@rdlabo/ionic-angular-photo-editor/css/ios26-header-button-color-scheme.css';\n必要に応じて、対応するAlwaysまたはSystemダークモード用のimportへ置き換えてください。photo-editor連携スタイルシートは、局所的なヘッダーの配色がアプリ全体の配色を上書きできるよう、必ず最後に読み込む必要があります。iOS 26テーマを使用しないアプリでは、このオプションのスタイルシートを読み込まないでください。その場合は、通常のIonicボタンの前景色切り替えだけが適用されます。
\n", "headings": [ { "id": "%E4%B8%8A%E6%9B%B8%E3%81%8D%E6%96%B9%E6%B3%95", "text": "上書き方法", "level": 2 + }, + { + "id": "%E3%83%98%E3%83%83%E3%83%80%E3%83%BC%E3%83%9C%E3%82%BF%E3%83%B3%E3%81%AE%E3%82%AB%E3%83%A9%E3%83%BC%E3%82%B9%E3%82%AD%E3%83%BC%E3%83%A0", + "text": "ヘッダーボタンのカラースキーム", + "level": 2 } ], "codes": [], @@ -118,7 +123,7 @@ export const PROJECT = { "file": "editor.md", "section": "ガイド", "path": "/projects/ionic-angular-photo-editor/docs/editor", - "html": "Ionic モーダルで PhotoEditorPage を表示します。インストール のあとで呼び出します。
import { PhotoEditorPage, IPhotoEditorDismiss } from '@rdlabo/ionic-angular-photo-editor';\n\n(async () => {\n const modal = await this.modalCtrl.create({\n component: PhotoEditorPage,\n componentProps: {\n requireSquare: false,\n value: 'https://picsum.photos/200/300',\n labels: {\n save: '送信', // change '保存' to '送信'\n },\n },\n });\n await modal.present();\n const { data } = await modal.onWillDismiss<IPhotoEditorDismiss>();\n if (data?.value) {\n console.log(data.value);\n }\n})();\ntrue の場合、最初に画像を正方形に切り抜く必要があります。
\n画像の URL または base64 文字列です。
\n設定すると、ラベルが上書きされます。
\n一覧はこちらです。
\n", + "html": "Ionic モーダルで PhotoEditorPage を表示します。インストール のあとで呼び出します。
import { PhotoEditorPage, IPhotoEditorDismiss, PhotoEditorProps } from '@rdlabo/ionic-angular-photo-editor';\n\n(async () => {\n const componentProps = {\n requireSquare: false,\n value: 'https://picsum.photos/200/300',\n headerButtonColorScheme: 'dark',\n labels: {\n save: '送信', // change '保存' to '送信'\n },\n } satisfies PhotoEditorProps;\n const modal = await this.modalCtrl.create({\n component: PhotoEditorPage,\n componentProps,\n });\n await modal.present();\n const { data } = await modal.onWillDismiss<IPhotoEditorDismiss>();\n if (data?.value) {\n console.log(data.value);\n }\n})();\ntrue の場合、最初に画像を正方形に切り抜く必要があります。
\n画像の URL または base64 文字列です。
\n設定すると、ラベルが上書きされます。
\n一覧はこちらです。
\n必須です。ion-toolbar が暗色または黒色の場合は dark、明色または白色の場合は light を選択してください。ツールバーの外観はCSS、半透明コンテンツ、実行時のテーマ上書きによって変わるため、ライブラリ側では判定できません。
Ionic モーダルで PhotoViewerPage を表示します。インストール のあとで呼び出します。
import { PhotoViewerPage, IPhotoViewerDismiss } from '@rdlabo/ionic-angular-photo-editor';\n\n(async () => {\n const modal = await this.modalCtrl.create({\n component: PhotoViewerPage,\n componentProps: {\n imageUrls: ['https://picsum.photos/200/300', 'https://picsum.photos/200/300'],\n index: 0,\n isCircle: false,\n },\n });\n await modal.present();\n const { data } = await modal.onWillDismiss<IPhotoViewerDismiss>();\n if (data?.delete) {\n // User delete image\n }\n})();\n画像の URL または base64 文字列の配列です。
\nimageUrls のインデックスです。
\n設定すると、画像が円形で表示されます。
\ntrue の場合、削除ボタンが表示されます。
\ntrue の場合、iOS 向けにフッターのセーフエリアを有効にします。
\n設定すると、ラベルが上書きされます。
\n一覧はこちらです。
\n", + "html": "Ionic モーダルで PhotoViewerPage を表示します。インストール のあとで呼び出します。
import { PhotoViewerPage, IPhotoViewerDismiss, PhotoViewerProps } from '@rdlabo/ionic-angular-photo-editor';\n\n(async () => {\n const componentProps = {\n imageUrls: ['https://picsum.photos/200/300', 'https://picsum.photos/200/300'],\n index: 0,\n isCircle: false,\n headerButtonColorScheme: 'dark',\n } satisfies PhotoViewerProps;\n const modal = await this.modalCtrl.create({\n component: PhotoViewerPage,\n componentProps,\n });\n await modal.present();\n const { data } = await modal.onWillDismiss<IPhotoViewerDismiss>();\n if (data?.delete) {\n // User delete image\n }\n})();\n画像の URL または base64 文字列の配列です。
\nimageUrls のインデックスです。
\n設定すると、画像が円形で表示されます。
\ntrue の場合、削除ボタンが表示されます。
\ntrue の場合、iOS 向けにフッターのセーフエリアを有効にします。
\n設定すると、ラベルが上書きされます。
\n一覧はこちらです。
\n必須です。ion-toolbar が暗色または黒色の場合は dark、明色または白色の場合は light を選択してください。ツールバーの外観はCSS、半透明コンテンツ、実行時のテーマ上書きによって変わるため、ライブラリ側では判定できません。