feat: 构建flutter 基座

This commit is contained in:
Booker
2026-05-18 13:36:54 +07:00
parent ce8ce5ec13
commit 37e4d73861
75 changed files with 3887 additions and 132 deletions

170
.gitignore vendored
View File

@@ -1,138 +1,48 @@
# ---> Node
# Logs
logs
# Miscellaneous
*.class
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# 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/
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Coverage directory used by tools like istanbul
coverage
*.lcov
# Android caches
/android/.kotlin/
# nyc test coverage
.nyc_output
# Symbolication related
app.*.symbols
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# vitepress build output
**/.vitepress/dist
# vitepress cache directory
**/.vitepress/cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

33
.metadata Normal file
View File

@@ -0,0 +1,33 @@
# 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: "db50e20168db8fee486b9abf32fc912de3bc5b6a"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
base_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
- platform: android
create_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
base_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
- platform: ios
create_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
base_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
# 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'

View File

@@ -1,3 +1,53 @@
# Flutter_Shell
# im_webview_app
这是flutter 套壳里面嵌套着IM的H5的代码
Flutter WebView 套壳 App默认加载
```text
https://h5-im.imharry.work/
```
## 本地打包
```bash
flutter build apk --release
```
APK 产物:
```text
build/app/outputs/flutter-apk/app-release.apk
```
## 自动部署 APK
`app` 目录执行:
```bash
./deploy-app.sh
```
这个命令会自动执行:
```bash
flutter build apk --release
scp -P 22 ./build/app/outputs/flutter-apk/app-release.apk root@54.116.29.247:/data/wwwroot/apk/
ssh -p 22 root@54.116.29.247 "bash /data/wwwroot/apk/show_apk_link.sh app-release.apk"
```
如果已经打包好了,只想上传现有 APK
```bash
./deploy-app.sh --skip-build
```
如果要上传自定义 APK
```bash
./deploy-app.sh --skip-build --apk ./build/app/outputs/flutter-apk/app-release.apk
```
远端配置也可以通过参数覆盖:
```bash
./deploy-app.sh --host 54.116.29.247 --user root --port 22
```

28
analysis_options.yaml Normal file
View File

@@ -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

14
android/.gitignore vendored Normal file
View File

@@ -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

View File

@@ -0,0 +1,942 @@
import java.util.UUID
import java.security.SecureRandom
import java.util.Base64
plugins {
id("com.android.application") version "8.9.1"
id("org.jetbrains.kotlin.android") version "2.1.0"
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
// id("com.google.gms.google-services") // Temporarily disabled
}
// 助记词风格词表BIP39 风格,仅小写字母,符合 Android 包名规范)
// 用于生成类似冷钱包助记词的包名,如 com.abandon.ability.able.above
val MNEMONIC_WORDS = listOf(
"abandon", "ability", "able", "about", "above", "absent", "absorb", "abstract", "absurd", "abuse",
"access", "accident", "account", "accuse", "achieve", "acid", "acoustic", "acquire", "across", "act",
"action", "actor", "actress", "actual", "adapt", "add", "addict", "address", "adjust", "admit",
"adult", "advance", "advice", "aerobic", "affair", "afford", "afraid", "again", "age", "agent",
"agree", "ahead", "aim", "air", "airport", "aisle", "alarm", "album", "alcohol", "alert",
"alien", "all", "alley", "allow", "almost", "alone", "alpha", "already", "also", "alter",
"always", "amateur", "amazing", "among", "amount", "amused", "analyst", "anchor", "ancient", "anger",
"angle", "angry", "animal", "ankle", "announce", "annual", "another", "answer", "antenna", "antique",
"anxiety", "any", "apart", "apology", "appear", "apple", "approve", "april", "arch", "arctic",
"area", "arena", "argue", "arm", "armed", "armor", "army", "around", "arrange", "arrest",
"arrive", "arrow", "art", "artefact", "artist", "artwork", "ask", "aspect", "assault", "asset",
"assist", "assume", "asthma", "athlete", "atom", "attack", "attend", "attitude", "attract", "auction",
"audit", "august", "aunt", "author", "auto", "autumn", "average", "avocado", "avoid", "awake",
"aware", "away", "awesome", "awful", "axis", "baby", "bachelor", "bacon", "badge", "bag",
"balance", "balcony", "ball", "bamboo", "banana", "banner", "bar", "barely", "bargain", "barrel",
"base", "basic", "basket", "battle", "beach", "bean", "beauty", "because", "become", "beef",
"before", "begin", "behave", "behind", "believe", "below", "belt", "bench", "benefit", "best",
"betray", "better", "between", "beyond", "bicycle", "bid", "bike", "bind", "biology", "bird",
"birth", "bitter", "black", "blade", "blame", "blanket", "blast", "bleak", "bless", "blind",
"blood", "blossom", "blouse", "blue", "blur", "blush", "board", "boat", "body", "boil",
"bomb", "bone", "bonus", "book", "boost", "border", "boring", "borrow", "boss", "bottom",
"bounce", "box", "boy", "bracket", "brain", "brand", "brass", "brave", "bread", "breeze",
"brick", "bridge", "brief", "bright", "bring", "brisk", "broccoli", "broken", "bronze", "broom",
"brother", "brown", "brush", "bubble", "buddy", "budget", "buffalo", "build", "bulb", "bulk",
"bullet", "bundle", "bunker", "burden", "burger", "burst", "bus", "business", "busy", "butter",
"buyer", "buzz", "cabbage", "cabin", "cable", "cactus", "cage", "cake", "call", "calm",
"camera", "camp", "can", "canal", "cancel", "candy", "cannon", "canoe", "canvas", "canyon",
"capable", "capital", "captain", "car", "carbon", "card", "cargo", "carpet", "carry", "cart",
"case", "cash", "casino", "castle", "casual", "cat", "catalog", "catch", "category", "cattle",
"caught", "cause", "caution", "cave", "ceiling", "celery", "cement", "census", "century", "cereal",
"certain", "chair", "chalk", "champion", "change", "chaos", "chapter", "charge", "chase", "chat",
"cheap", "check", "cheese", "chef", "cherry", "chest", "chicken", "chief", "child", "chimney",
"choice", "choose", "chronic", "chuckle", "chunk", "churn", "cigar", "cinnamon", "circle", "citizen",
"city", "civil", "claim", "clap", "clarify", "claw", "clay", "clean", "clerk", "clever",
"click", "client", "cliff", "climb", "clinic", "clip", "clock", "clog", "close", "cloth",
"cloud", "clown", "club", "clump", "cluster", "clutch", "coach", "coast", "coconut", "code",
"coffee", "coil", "coin", "collect", "color", "column", "combine", "come", "comfort", "comic",
"common", "company", "concert", "conduct", "confirm", "congress", "connect", "consider", "control", "convince",
"cook", "cool", "copper", "copy", "coral", "core", "corn", "correct", "cost", "cotton",
"couch", "country", "couple", "course", "cousin", "cover", "coyote", "crack", "cradle", "craft",
"cram", "crane", "crash", "crater", "crawl", "crazy", "cream", "credit", "creek", "crew",
"cricket", "crime", "crisp", "critic", "crop", "cross", "crouch", "crowd", "crucial", "cruel",
"cruise", "crumble", "crunch", "crush", "cry", "crystal", "cube", "culture", "cup", "cupboard",
"curious", "current", "curtain", "curve", "cushion", "custom", "cute", "cycle", "dad", "damage",
"damp", "dance", "danger", "daring", "dash", "daughter", "dawn", "day", "deal", "debate",
"debris", "decade", "december", "decide", "decline", "decorate", "decrease", "deer", "defense", "define",
"defy", "degree", "delay", "deliver", "demand", "demise", "denial", "dentist", "deny", "depart",
"depend", "deposit", "depth", "deputy", "derive", "describe", "desert", "design", "desk", "despair",
"destroy", "detail", "detect", "develop", "device", "devote", "diagram", "dial", "diamond", "diary",
"dice", "diesel", "diet", "differ", "digital", "dignity", "dilemma", "dinner", "dinosaur", "direct",
"dirt", "disagree", "discover", "disease", "dish", "dismiss", "disorder", "display", "distance", "divert",
"divide", "divorce", "dizzy", "doctor", "document", "dog", "doll", "dolphin", "domain", "donate",
"donkey", "donor", "door", "dose", "double", "dove", "draft", "dragon", "drama", "drastic",
"draw", "dream", "dress", "drift", "drill", "drink", "drip", "drive", "drop", "drum",
"dry", "duck", "dumb", "dune", "during", "dust", "dutch", "duty", "dwarf", "dynamic",
"eager", "eagle", "early", "earn", "earth", "easily", "east", "easy", "echo", "ecology",
"economy", "edge", "edit", "educate", "effort", "egg", "eight", "either", "elbow", "elder",
"electric", "elegant", "element", "elephant", "elevator", "elite", "else", "embark", "embody", "embrace",
"emerge", "emotion", "employ", "empower", "empty", "enable", "enact", "end", "endless", "endorse",
"enemy", "energy", "enforce", "engage", "engine", "enhance", "enjoy", "enlist", "enough", "enrich",
"enroll", "ensure", "enter", "entire", "entry", "envelope", "episode", "equal", "equip", "era",
"erase", "erode", "erosion", "error", "erupt", "escape", "essay", "essence", "estate", "eternal",
"ethics", "evidence", "evil", "evoke", "evolve", "exact", "example", "excess", "exchange", "excite",
"exclude", "excuse", "execute", "exercise", "exhaust", "exhibit", "exile", "exist", "exit", "exotic",
"expand", "expect", "expire", "explain", "expose", "express", "extend", "extra", "eye", "eyebrow",
"fabric", "face", "faculty", "fade", "faint", "faith", "fall", "false", "fame", "family",
"famous", "fan", "fancy", "fantasy", "farm", "fashion", "fat", "fatal", "father", "fatigue",
"fault", "favorite", "feature", "february", "federal", "fee", "feed", "feel", "female", "fence",
"festival", "fetch", "fever", "few", "fiber", "fiction", "field", "figure", "file", "film",
"filter", "final", "find", "fine", "finger", "finish", "fire", "firm", "first", "fiscal",
"fish", "fit", "fitness", "fix", "flag", "flame", "flash", "flat", "flavor", "flee",
"flight", "flip", "float", "flock", "floor", "flower", "fluid", "flush", "fly", "foam",
"focus", "fog", "foil", "fold", "follow", "food", "foot", "force", "forest", "forget",
"fork", "fortune", "forum", "forward", "fossil", "foster", "found", "fox", "fragile", "frame",
"frequent", "fresh", "friend", "fringe", "frog", "front", "frost", "frown", "frozen", "fruit",
"fuel", "fun", "funny", "furnace", "fury", "future", "gadget", "gain", "galaxy", "gallery",
"game", "gap", "garage", "garbage", "garden", "garlic", "garment", "gas", "gasp", "gate",
"gather", "gauge", "gaze", "general", "genius", "genre", "gentle", "genuine", "gesture", "ghost",
"giant", "gift", "giggle", "ginger", "giraffe", "girl", "give", "glad", "glance", "glare",
"glass", "glide", "glimpse", "globe", "gloom", "glory", "glove", "glow", "glue", "goat",
"goddess", "gold", "good", "goose", "gorilla", "gospel", "gossip", "govern", "gown", "grab",
"grace", "grain", "grant", "grape", "grass", "gravity", "great", "green", "grid", "grief",
"grit", "grocery", "group", "grow", "grunt", "guard", "guess", "guide", "guilt", "guitar",
"gun", "gym", "habit", "hair", "half", "hammer", "hamster", "hand", "happy", "harbor",
"hard", "harsh", "harvest", "hat", "have", "hawk", "hazard", "head", "health", "heart",
"heavy", "hedgehog", "height", "hello", "helmet", "help", "hen", "hero", "hidden", "high",
"hill", "hint", "hip", "hire", "history", "hobby", "hockey", "hold", "hole", "holiday",
"hollow", "home", "honey", "hood", "hope", "horn", "horror", "horse", "hospital", "host",
"hotel", "hour", "hover", "hub", "huge", "human", "humble", "humor", "hundred", "hungry",
"hunt", "hurdle", "hurry", "hurt", "husband", "hybrid", "ice", "icon", "idea", "identify",
"idle", "ignore", "ill", "illegal", "illness", "image", "imitate", "immense", "immune", "impact",
"impose", "improve", "impulse", "inch", "include", "income", "increase", "index", "indicate", "indoor",
"industry", "infant", "inflict", "inform", "inhale", "inherit", "initial", "inject", "injury", "inmate",
"inner", "innocent", "input", "inquiry", "insane", "insect", "inside", "inspire", "install", "intact",
"interest", "into", "invest", "invite", "involve", "iron", "island", "isolate", "issue", "item",
"ivy", "jacket", "jaguar", "jar", "jazz", "jealous", "jeans", "jelly", "jewel", "job",
"join", "joke", "journey", "joy", "judge", "juice", "jump", "jungle", "junior", "junk",
"just", "kangaroo", "keen", "keep", "ketchup", "key", "kick", "kid", "kidney", "kind",
"kingdom", "kiss", "kit", "kitchen", "kite", "kitten", "kiwi", "knee", "knife", "knock",
"know", "lab", "label", "labor", "ladder", "lady", "lake", "lamp", "language", "laptop",
"large", "later", "latin", "laugh", "laundry", "lava", "law", "lawn", "lawsuit", "layer",
"lazy", "leader", "leaf", "learn", "leave", "lecture", "left", "leg", "legal", "legend",
"leisure", "lemon", "lend", "length", "lens", "leopard", "lesson", "letter", "level", "liar",
"liberty", "library", "license", "life", "lift", "light", "like", "limb", "limit", "link",
"lion", "liquid", "list", "little", "live", "lizard", "load", "loan", "lobster", "local",
"lock", "logic", "lonely", "long", "loop", "lottery", "loud", "lounge", "love", "loyal",
"lucky", "luggage", "lumber", "lunar", "lunch", "luxury", "lyrics", "machine", "mad", "magic",
"magnet", "maid", "mail", "main", "major", "make", "mammal", "man", "manage", "mandate",
"mango", "mansion", "manual", "maple", "marble", "march", "margin", "marine", "market", "marriage",
"mask", "mass", "master", "match", "material", "math", "matrix", "matter", "maximum", "maze",
"meadow", "mean", "measure", "meat", "mechanic", "medal", "media", "melody", "melt", "member",
"memory", "mention", "menu", "mercy", "merge", "merit", "merry", "mesh", "message", "metal",
"method", "middle", "midnight", "milk", "million", "mimic", "mind", "minimum", "minor", "minute",
"miracle", "mirror", "misery", "miss", "mistake", "mix", "mixed", "mixture", "mobile", "model",
"modify", "mom", "moment", "monitor", "monkey", "monster", "month", "moon", "moral", "more",
"morning", "mosquito", "mother", "motion", "motor", "mountain", "mouse", "move", "movie", "much",
"muffin", "mule", "multiply", "muscle", "museum", "mushroom", "music", "must", "mutual", "myself",
"mystery", "myth", "naive", "name", "napkin", "narrow", "nasty", "nation", "nature", "near",
"neck", "need", "negative", "neglect", "neither", "nephew", "nerve", "nest", "net", "network",
"neutral", "never", "news", "next", "nice", "night", "noble", "noise", "nominee", "noodle",
"normal", "north", "nose", "notable", "note", "nothing", "notice", "novel", "now", "nuclear",
"number", "nurse", "nut", "oak", "obey", "object", "oblige", "obscure", "observe", "obtain",
"obvious", "occur", "ocean", "october", "odor", "off", "offer", "office", "often", "oil",
"okay", "old", "olive", "olympic", "omit", "once", "one", "onion", "online", "only",
"open", "opera", "opinion", "oppose", "option", "orange", "orbit", "orchard", "order", "ordinary",
"organ", "orient", "original", "orphan", "ostrich", "other", "outdoor", "outer", "output", "outside",
"oval", "oven", "over", "own", "owner", "oxygen", "oyster", "ozone", "pact", "paddle",
"page", "pair", "palace", "palm", "panda", "panel", "panic", "panther", "paper", "parade",
"parent", "park", "parrot", "party", "pass", "patch", "path", "patient", "patrol", "pattern",
"pause", "pave", "payment", "peace", "peanut", "pear", "peasant", "pelican", "pen", "penalty",
"pencil", "people", "pepper", "perfect", "permit", "person", "pet", "phone", "photo", "phrase",
"physical", "piano", "picnic", "picture", "piece", "pig", "pigeon", "pill", "pilot", "pink",
"pioneer", "pipe", "pistol", "pitch", "pizza", "place", "planet", "plastic", "plate", "play",
"please", "pledge", "pluck", "plug", "plunge", "poem", "poet", "point", "polar", "pole",
"police", "pond", "pony", "pool", "popular", "portion", "position", "possible", "post", "potato",
"pottery", "poverty", "powder", "power", "practice", "praise", "predict", "prefer", "prepare", "present",
"pretty", "prevent", "price", "pride", "primary", "print", "priority", "prison", "private", "prize",
"problem", "process", "produce", "profit", "program", "project", "promote", "proof", "property", "prosper",
"protect", "proud", "provide", "public", "pudding", "pull", "pulp", "pulse", "pumpkin", "punch",
"pupil", "puppy", "purchase", "purity", "purpose", "purse", "push", "put", "puzzle", "pyramid",
"quality", "quantum", "quarter", "question", "quick", "quit", "quiz", "quote", "rabbit", "raccoon",
"race", "rack", "radar", "radio", "rail", "rain", "raise", "rally", "ramp", "ranch",
"random", "range", "rapid", "rare", "rate", "rather", "raven", "raw", "razor", "ready",
"real", "reason", "rebel", "rebuild", "recall", "receive", "recipe", "record", "recycle", "reduce",
"reflect", "reform", "refuse", "region", "regret", "regular", "reject", "relax", "release", "relief",
"rely", "remain", "remember", "remind", "remove", "render", "renew", "rent", "reopen", "repair",
"repeat", "replace", "report", "require", "rescue", "resemble", "resist", "resource", "response", "result",
"retire", "retreat", "return", "reunion", "reveal", "review", "reward", "rhythm", "rib", "ribbon",
"rice", "rich", "ride", "ridge", "rifle", "right", "rigid", "ring", "riot", "ripple",
"risk", "ritual", "rival", "river", "road", "roast", "robot", "robust", "rocket", "romance",
"roof", "rookie", "room", "rose", "rotate", "rough", "round", "route", "royal", "rubber",
"rude", "rug", "rule", "run", "runway", "rural", "sad", "saddle", "sadness", "safe",
"sail", "salad", "salmon", "salon", "salt", "salute", "same", "sample", "sand", "satisfy",
"satoshi", "sauce", "sausage", "save", "say", "scale", "scan", "scare", "scatter", "scene",
"scheme", "school", "science", "scissors", "scorpion", "scout", "scrap", "screen", "script", "scrub",
"sea", "search", "season", "seat", "second", "secret", "section", "security", "seed", "seek",
"segment", "select", "sell", "seminar", "senior", "sense", "sentence", "series", "service", "session",
"settle", "setup", "seven", "shadow", "shaft", "shallow", "share", "shed", "shell", "sheriff",
"shield", "shift", "shine", "ship", "shiver", "shock", "shoe", "shoot", "shop", "short",
"shoulder", "shove", "shrimp", "shrug", "shuffle", "shy", "sibling", "sick", "side", "siege",
"sight", "sign", "silent", "silk", "silly", "silver", "similar", "simple", "since", "sing",
"siren", "sister", "situate", "six", "size", "skate", "sketch", "ski", "skill", "skin",
"skirt", "skull", "slab", "slam", "sleep", "slender", "slice", "slide", "slight", "slim",
"slogan", "slot", "slow", "slush", "small", "smart", "smile", "smoke", "smooth", "snack",
"snake", "snap", "sniff", "snow", "soap", "soccer", "social", "sock", "soda", "soft",
"solar", "soldier", "solid", "solution", "solve", "someone", "song", "soon", "sorry", "sort",
"soul", "sound", "soup", "source", "south", "space", "spare", "spatial", "spawn", "speak",
"special", "speed", "spell", "spend", "sphere", "spice", "spider", "spike", "spin", "spirit",
"split", "spoil", "sponsor", "spoon", "sport", "spot", "spray", "spread", "spring", "spy",
"square", "squeeze", "squirrel", "stable", "stadium", "staff", "stage", "stairs", "stamp", "stand",
"start", "state", "stay", "steak", "steel", "stem", "step", "stereo", "stick", "still",
"sting", "stock", "stomach", "stone", "stool", "story", "stove", "strategy", "street", "strike",
"strong", "struggle", "student", "stuff", "stumble", "style", "subject", "submit", "subway", "success",
"such", "sudden", "suffer", "sugar", "suggest", "suit", "summer", "sun", "sunny", "sunset",
"super", "supply", "supreme", "sure", "surface", "surge", "surprise", "surround", "survey", "suspect",
"sustain", "swallow", "swamp", "swap", "swarm", "swear", "sweet", "swift", "swim", "swing",
"switch", "sword", "symbol", "symptom", "syrup", "system", "table", "tackle", "tag", "tail",
"talent", "talk", "tank", "tape", "target", "task", "taste", "tattoo", "taxi", "teach",
"team", "tell", "ten", "tenant", "tennis", "tent", "term", "test", "text", "thank",
"that", "theme", "then", "theory", "there", "they", "thing", "this", "thought", "three",
"thrive", "throw", "thumb", "thunder", "ticket", "tide", "tiger", "tilt", "timber", "time",
"tiny", "tip", "tired", "tissue", "title", "toast", "tobacco", "today", "toddler", "toe",
"together", "toilet", "token", "tomato", "tomorrow", "tone", "tongue", "tonight", "tool", "tooth",
"top", "topic", "topple", "torch", "tornado", "tortoise", "toss", "total", "tourist", "toward",
"tower", "town", "toy", "track", "trade", "traffic", "tragic", "train", "transfer", "trap",
"trash", "travel", "tray", "treat", "tree", "trend", "trial", "tribe", "trick", "trigger",
"trim", "trip", "trophy", "trouble", "truck", "true", "truly", "trumpet", "trust", "truth",
"try", "tube", "tuition", "tumble", "tuna", "tunnel", "turkey", "turn", "turtle", "twelve",
"twenty", "twice", "twin", "twist", "two", "type", "typical", "ugly", "umbrella", "unable",
"uncle", "uncover", "under", "undo", "unfair", "unfold", "unhappy", "uniform", "unique", "unit",
"universe", "unknown", "unlock", "until", "unusual", "unveil", "update", "upgrade", "uphold", "upon",
"upper", "upset", "urban", "urge", "usage", "use", "used", "useful", "useless", "usual",
"utility", "vacant", "vacuum", "vague", "valid", "valley", "valve", "van", "vanish", "vapor",
"various", "vast", "vault", "vehicle", "velvet", "vendor", "venture", "venue", "verb", "verify",
"version", "very", "vessel", "veteran", "viable", "vibrant", "vicious", "victory", "video", "view",
"village", "vintage", "violin", "virtual", "virus", "visa", "visit", "visual", "vital", "vivid",
"vocal", "voice", "void", "volcano", "volume", "vote", "voyage", "wage", "wagon", "wait",
"walk", "wall", "walnut", "want", "warfare", "warm", "warrior", "wash", "wasp", "waste",
"water", "wave", "way", "wealth", "weapon", "wear", "weasel", "weather", "web", "wedding",
"weekend", "weird", "welcome", "west", "wet", "whale", "what", "wheat", "wheel", "when",
"where", "whip", "whisper", "wide", "width", "wife", "wild", "will", "win", "window",
"wine", "wing", "wink", "winner", "winter", "wire", "wisdom", "wise", "wish", "witness",
"wolf", "woman", "wonder", "wood", "wool", "word", "work", "world", "worry", "worth",
"wrap", "wreck", "wrestle", "wrist", "write", "wrong", "yard", "year", "yellow", "you",
"young", "youth", "zebra", "zero", "zone", "zoo"
)
// 扩展助记词表(约 4 倍于基础词表,合计约 5 倍词库)
val MNEMONIC_WORDS_EXTRA = listOf(
"abacus", "abbey", "abbreviation", "abdomen", "abide", "aboard", "abolish", "abound", "abrasive", "abreast",
"abridge", "abroad", "abrupt", "absentee", "absorbent", "abstain", "abstract", "absurdity", "abundance", "abundant",
"abuse", "abyss", "academia", "academic", "academy", "accelerate", "accent", "accept", "acceptable", "accessory",
"acclaim", "acclimate", "accolade", "accommodate", "accompany", "accomplice", "accomplish", "accord", "accordion", "accountant",
"accreditation", "accumulate", "accuracy", "accusation", "accustom", "acetone", "achieve", "aching", "acidic", "acknowledge",
"acoustic", "acquaint", "acquire", "acquisition", "acrobat", "acronym", "acropolis", "acrylic", "acting", "activation",
"activist", "activity", "actuality", "acumen", "acupuncture", "adaptation", "adapter", "addiction", "additive", "address",
"adept", "adequacy", "adhesive", "adjacent", "adjective", "adjourn", "adjustment", "admin", "admiral", "admission",
"adobe", "adolescent", "adoption", "adrenaline", "adult", "advancement", "advent", "adventure", "adverb", "adversary",
"adverse", "advertise", "advertisement", "advocate", "aerial", "aerobic", "aerospace", "aesthetic", "affection", "affidavit",
"affiliate", "affinity", "affirmation", "afford", "affordable", "aftermath", "afternoon", "afterward", "agenda", "aggregate",
"aggression", "aggressive", "agile", "agility", "agony", "agreeable", "agreement", "agriculture", "ahead", "ailment",
"airline", "airplane", "airspace", "airway", "aisle", "alarm", "albatross", "alchemy", "alcohol", "algebra",
"algorithm", "alignment", "alimony", "allegation", "allegiance", "allegory", "allergy", "alleyway", "alliance", "alligator",
"allocation", "allotment", "allowance", "alloy", "allure", "almond", "alphabet", "altar", "alteration", "alternate",
"alternative", "altitude", "alto", "aluminum", "alumni", "amalgam", "amateur", "amazing", "ambassador", "amber",
"ambiguity", "ambiguous", "ambition", "ambitious", "ambulance", "ambush", "amendment", "amenity", "ammonia", "ammunition",
"amnesia", "amnesty", "amplifier", "amplify", "amputate", "amusement", "analog", "analogy", "analyst", "analytical",
"anarchist", "anarchy", "anatomy", "ancestor", "ancestry", "anchovy", "anecdote", "anemia", "anesthesia", "angel",
"angler", "anguish", "angular", "animation", "animosity", "anklet", "annex", "anniversary", "announcement", "announcer",
"annual", "annuity", "anomaly", "anonymous", "antagonist", "antarctic", "anteater", "antelope", "antenna", "anthem",
"anthology", "anthropology", "antibiotic", "antibody", "anticipate", "anticipation", "antidote", "antique", "antiquity", "antler",
"antonym", "anxiety", "apartment", "apex", "apology", "apostle", "apparatus", "apparel", "appetite", "appetizer",
"appliance", "applicant", "application", "appointment", "appraisal", "appreciate", "appreciation", "apprentice", "approach", "approval",
"approximate", "apricot", "aquarium", "aquatic", "arbiter", "arbitrary", "arbitration", "arbor", "arcade", "archaeology",
"archbishop", "architect", "architecture", "archive", "ardent", "arena", "argon", "argument", "aristocrat", "arithmetic",
"armadillo", "armament", "armchair", "armful", "armistice", "armory", "aroma", "aromatic", "arousal", "arrangement",
"array", "arrears", "arrogant", "arrowhead", "arson", "artery", "artichoke", "articulate", "artifact", "artisan",
"artistry", "ascend", "ascent", "ascertain", "ascribe", "asphalt", "aspiration", "aspirin", "assassin", "assault",
"assemblage", "assembly", "assertion", "assessment", "assignment", "assimilation", "assistance", "assistant", "associate", "association",
"assumption", "assurance", "asteroid", "asthma", "astrology", "astronaut", "astronomy", "asymmetric", "atheist", "athletic",
"atmosphere", "atom", "atomic", "atrocity", "attachment", "attacker", "attainment", "attendance", "attendant", "attention",
"attest", "attic", "attitude", "attorney", "attraction", "attractive", "attribute", "auctioneer", "audience", "auditor",
"auditory", "augment", "august", "aurora", "auspicious", "authentic", "authority", "authorization", "autobiography", "automation",
"automobile", "autonomy", "autumn", "availability", "avalanche", "avatar", "avenger", "aviation", "aviator", "avocado",
"avoidance", "awakening", "award", "awareness", "awful", "awkward", "backbone", "backdrop", "backfire", "background",
"backpack", "backup", "backward", "bacon", "bacteria", "bacterial", "badge", "badger", "badminton", "bagel",
"baggage", "bagpipe", "bailiff", "bakery", "balance", "balcony", "ballet", "balloon", "ballot", "ballroom",
"bamboo", "banister", "banjo", "banker", "banking", "bankruptcy", "banquet", "barbecue", "barbarian", "barbecue",
"barber", "barcode", "barefoot", "bargain", "barge", "baritone", "barley", "barnacle", "barometer", "baron",
"barracks", "barrel", "barricade", "barrier", "barrister", "barrow", "bartender", "baseball", "baseboard", "baseline",
"basement", "basin", "basketball", "bassoon", "bastion", "batch", "bathrobe", "bathroom", "battalion", "battery",
"battleship", "bazaar", "beacon", "beaker", "beanstalk", "bearer", "bearish", "beast", "beatitude", "beautician",
"beauty", "bedroom", "bedside", "bedspread", "bedtime", "beehive", "beekeeper", "beetle", "beforehand", "beggar",
"beginner", "begonia", "behavior", "belonging", "benchmark", "benediction", "benefactor", "beneficiary", "benevolent", "benign",
"bestseller", "beverage", "bewilder", "beyond", "bibliography", "bicycle", "biennial", "bifocal", "bilateral", "bilingual",
"billboard", "billion", "billow", "binary", "binocular", "biography", "biological", "biologist", "biology", "biopsy",
"bipartisan", "birthday", "birthplace", "birthright", "biscuit", "bishop", "blackberry", "blackbird", "blackboard", "blackout",
"blacksmith", "bladder", "blade", "blank", "blanket", "blasphemy", "blast", "blazer", "bleach", "bleacher",
"bleeding", "blender", "blessing", "blight", "blimp", "blinder", "blizzard", "blockade", "blockage", "blockbuster",
"blocker", "bloodshed", "bloodstream", "bloody", "bloom", "blossom", "blotter", "blouse", "blueberry", "bluebird",
"blueprint", "blunder", "blur", "blush", "boardroom", "boardwalk", "boast", "boaster", "boatman", "bodyguard",
"bodywork", "boiler", "boldness", "bolster", "bolt", "bombard", "bomber", "bondage", "bonfire", "bonnet",
"bonus", "bookcase", "bookend", "bookkeeper", "booklet", "bookmark", "bookstore", "boomerang", "booth", "borderline",
"bore", "borough", "borrower", "botany", "bottleneck", "bottomless", "boulevard", "bounce", "boundary", "boundless",
"bounty", "bouquet", "bourbon", "boutique", "bowler", "bowling", "bowstring", "boxer", "boxing", "boycott",
"boyfriend", "boyhood", "bracelet", "bracket", "brainstorm", "brainwash", "brainy", "branch", "brand", "brandy",
"brass", "bratwurst", "bravery", "bravery", "breadcrumb", "breadth", "breadwinner", "breakdown", "breakfast", "breakthrough",
"breakup", "breakwater", "breast", "breaststroke", "breath", "breathe", "breather", "breezeway", "brewery", "bribery",
"briefcase", "briefing", "brigade", "brigadier", "brightness", "brilliance", "brimstone", "brine", "broadband", "broadcast",
"broadway", "brochure", "broiler", "broker", "brokerage", "bronze", "brooch", "brotherhood", "browbeat", "browser",
"brunch", "brunette", "brutal", "brutality", "bubble", "bubbly", "bucket", "buckle", "buckskin", "buddha",
"buddhist", "budget", "buffalo", "buffer", "buffet", "buffoon", "builder", "building", "bulldog", "bulldozer",
"bullet", "bulletin", "bullfight", "bullfrog", "bullion", "bullish", "bullpen", "bullring", "bumblebee", "bumper",
"bunch", "bungalow", "bunkhouse", "bunkhouse", "buoyancy", "burden", "bureaucrat", "burglar", "burglary", "burial",
"burlap", "burner", "burnout", "burrito", "burst", "busboy", "bushel", "businessman", "businesswoman", "busker",
"bustle", "butane", "butcher", "buttercup", "butterfly", "buttermilk", "butterscotch", "buttress", "buzzard", "buzzer",
"bygone", "bypass", "byproduct", "byte", "cabaret", "cabbage", "cabinet", "cable", "cache", "cactus",
"cadence", "cadet", "cadre", "cafe", "caffeine", "cage", "cajole", "calamity", "calcium", "calculator",
"calendar", "caliber", "calibration", "calorie", "calypso", "camcorder", "cameo", "camouflage", "campaign", "campfire",
"campus", "canary", "cancellation", "cancer", "candidate", "candle", "candlestick", "candor", "candy", "cane",
"canine", "cannabis", "cannon", "canoe", "canopy", "cantaloupe", "canteen", "canvas", "canyon", "capability",
"capacitor", "capacity", "caper", "capitalism", "capitalist", "capitol", "capricorn", "capsule", "captain", "caption",
"captive", "captivity", "capture", "caramel", "caravan", "carbohydrate", "carbon", "cardboard", "cardigan", "cardinal",
"career", "carefree", "caregiver", "caretaker", "caricature", "carnival", "carnivore", "carousel", "carpenter", "carpet",
"carriage", "carrier", "carrot", "cartel", "cartilage", "cartographer", "cartoon", "cartridge", "carve", "carving",
"cascade", "cashew", "cashier", "cashmere", "casino", "casket", "cassette", "castaway", "castle", "casualty",
"catacomb", "catalog", "catalyst", "catapult", "catastrophe", "catchment", "catchphrase", "catchy", "category", "caterer",
"caterpillar", "catfish", "cathedral", "cation", "catnip", "cauliflower", "causeway", "caution", "cavalry", "caveman",
"cavern", "caviar", "cavity", "cedar", "celebration", "celebrity", "celery", "celestial", "cellar", "cellist",
"cellphone", "cellular", "cement", "cemetery", "censorship", "census", "centennial", "center", "centerpiece", "centigrade",
"centimeter", "central", "centralize", "century", "ceramic", "cereal", "cerebral", "ceremony", "certainty", "certificate",
"certification", "chafe", "chagrin", "chain", "chairman", "chairperson", "chalet", "challenge", "chamber", "champion",
"championship", "chancellor", "chandelier", "chandler", "change", "channel", "chant", "chaos", "chaplain", "chapter",
"character", "characteristic", "charcoal", "charity", "charlatan", "charm", "charter", "chasm", "chassis", "chastity",
"chateau", "chatter", "chauffeur", "checkbook", "checker", "checklist", "checkmate", "checkout", "checkpoint", "cheekbone",
"cheerleader", "cheese", "cheetah", "chemical", "chemist", "chemistry", "cherish", "chess", "chestnut", "chew",
"chickadee", "chicken", "chiffon", "childbirth", "childhood", "childish", "chill", "chime", "chimney", "chinaware",
"chipmunk", "chiropractor", "chisel", "chivalry", "chlorine", "chlorophyll", "chocolate", "choice", "choir", "cholera",
"cholesterol", "chord", "chorus", "christen", "chromium", "chromosome", "chronicle", "chronological", "chrysanthemum", "chuckle",
"chunk", "church", "cigarette", "cinema", "circuit", "circular", "circulation", "circumference", "circumstance", "circus",
"citation", "citizen", "citizenship", "citrus", "civilian", "civilization", "clarity", "classic", "classical", "classification",
"classmate", "classroom", "clause", "clavier", "cleaner", "cleaning", "clearance", "clearing", "cleavage", "cleaver",
"clemency", "clergy", "clergyman", "cleric", "clerk", "clever", "client", "clientele", "cliff", "climate",
"climax", "climb", "clinic", "clinical", "clipper", "clipping", "cloak", "cloakroom", "clockwork", "clog",
"clone", "closure", "clothes", "clothing", "cloudburst", "cloudy", "clover", "clown", "cluster", "clutch",
"coach", "coagulate", "coalition", "coastline", "coating", "cockpit", "cockroach", "cocktail", "cocoa", "coconut",
"codfish", "codify", "coeditor", "coeducation", "coefficient", "coexistence", "coffee", "coffin", "cognition", "cognitive",
"coherence", "cohesion", "cohort", "coincidence", "colander", "collaboration", "collage", "collapse", "collar", "collateral",
"colleague", "collection", "collective", "collector", "college", "collision", "colloquium", "colonel", "colonial", "colonist",
"colonization", "colony", "colossal", "columnist", "combat", "combatant", "combination", "combine", "combustion", "comedian",
"comedy", "comet", "comfort", "comic", "comma", "command", "commander", "commando", "commemoration", "commence",
"commencement", "commendation", "commentary", "commentator", "commerce", "commercial", "commission", "commissioner", "commitment", "committee",
"commodity", "commonwealth", "commotion", "communal", "communication", "communion", "communist", "community", "commuter", "compact",
"companion", "company", "comparison", "compartment", "compassion", "compatibility", "compensation", "competence", "competition", "competitor",
"compilation", "complaint", "complement", "completion", "complexity", "compliance", "compliment", "component", "composer", "composite",
"composition", "compost", "composure", "compound", "comprehension", "compression", "compromise", "computation", "computer", "comrade",
"concealment", "concept", "conception", "concern", "concert", "concession", "concerto", "conclusion", "concrete", "concur",
"condemnation", "condiment", "condition", "conditioner", "condolence", "condominium", "condor", "conductor", "cone", "confection",
"confederation", "conference", "confession", "confetti", "confidence", "configuration", "confinement", "confirmation", "conflict", "conformity",
"confrontation", "confusion", "congestion", "conglomerate", "congratulation", "congregation", "congress", "conjunction", "connection", "connoisseur",
"conquest", "conscience", "consciousness", "consecration", "consensus", "consent", "consequence", "conservation", "conservative", "conservatory",
"consideration", "consignment", "consistency", "consolation", "consonant", "conspiracy", "constable", "constancy", "constellation", "constituency",
"constitution", "constraint", "construction", "consultant", "consultation", "consumer", "consumption", "contact", "contagion", "container",
"contamination", "contemplation", "contemporary", "contender", "contentment", "contest", "context", "continent", "contingency", "continuation",
"continuity", "contour", "contraption", "contribution", "contributor", "contrivance", "controller", "controversy", "convenience", "convention",
"conversation", "conversion", "converter", "conviction", "convoy", "convulsion", "cookbook", "cooker", "cookie", "cooking",
"cooperation", "coordinator", "copilot", "copper", "copyright", "cord", "cordial", "cordon", "corduroy", "cornerstone",
"cornfield", "cornflake", "cornstalk", "coronation", "corporal", "corporation", "corpse", "corpuscle", "corral", "correction",
"correlation", "correspondence", "corridor", "corrosion", "corruption", "corsage", "corset", "cosmetic", "cosmic", "cosmology",
"cosmonaut", "cosmos", "costume", "cottage", "cotton", "couch", "cougar", "council", "counsel", "counselor",
"countdown", "counter", "counterpart", "countess", "countryside", "county", "coupon", "courage", "courier", "courtroom",
"courtship", "courtyard", "cousin", "coverage", "coward", "cowboy", "cowgirl", "crab", "cracker", "cradle",
"craftsman", "cramp", "cranberry", "crane", "cranium", "crate", "crater", "crayon", "creation", "creativity",
"creator", "creature", "credential", "credibility", "creditor", "creed", "creek", "creep", "cremation", "creole",
"crescent", "crest", "crevice", "crew", "crewman", "crib", "cricket", "crime", "criminal", "crimson",
"cripple", "crisis", "criterion", "critic", "criticism", "crocodile", "croissant", "crook", "crossbow", "crossing",
"crossroad", "crosswalk", "crossword", "crow", "crowd", "crown", "crucifix", "cruise", "crumb", "crumble",
"crusade", "crusader", "crush", "crystal", "cub", "cube", "cubicle", "cucumber", "cue", "cuisine",
"culprit", "cultivation", "culture", "cumin", "cupboard", "cupcake", "curator", "curfew", "curiosity", "curler",
"curriculum", "curry", "cursor", "curtain", "curve", "cushion", "custard", "custodian", "custody", "custom",
"customer", "cutlery", "cutlet", "cutting", "cyclone", "cylinder", "cymbal", "cynic", "cynical", "cypress",
"cyst", "daffodil", "dagger", "dairy", "daisy", "dam", "damage", "dame", "damn", "dampness",
"dancer", "dandelion", "dandruff", "danger", "daredevil", "darkness", "darling", "dart", "dashboard", "database",
"date", "daughter", "dawn", "daybreak", "daydream", "daylight", "daytime", "deadline", "deadlock", "dealer",
"dealership", "dean", "death", "debate", "debtor", "debut", "decade", "decadence", "decaf", "decanter",
"deception", "decibel", "decimal", "decision", "deck", "declaration", "decline", "decoder", "decoration", "decorator",
"decrease", "decree", "dedication", "deduction", "deed", "default", "defeat", "defect", "defendant", "defender",
"defense", "deferral", "deficiency", "deficit", "definition", "deflation", "deflection", "deformity", "defrost", "degenerate",
"degradation", "degree", "dehydration", "deity", "delay", "delegate", "delegation", "deletion", "delicacy", "delight",
"delinquent", "delivery", "delta", "delusion", "demand", "democracy", "democrat", "demolition", "demon", "demonstration",
"demotion", "denim", "denomination", "denominator", "density", "dent", "dentist", "department", "departure", "dependence",
"dependency", "depiction", "deposit", "depot", "depreciation", "depression", "deprivation", "depth", "deputy", "derby",
"derivation", "derivative", "derrick", "descendant", "descent", "description", "desert", "designer", "desire", "desk",
"desktop", "dessert", "destination", "destiny", "destruction", "detachment", "detective", "detector", "detention", "detergent",
"deterrent", "detour", "detox", "developer", "development", "deviation", "device", "devotee", "devotion", "dew",
"diagnosis", "diagram", "dialect", "dialogue", "diameter", "diamond", "diaper", "diaphragm", "diary", "dictionary",
"diesel", "dietitian", "difference", "differential", "differentiation", "difficulty", "diffusion", "digest", "digestion", "digger",
"digit", "dignity", "dilemma", "diligence", "dilution", "dimension", "diminutive", "dinosaur", "diploma", "diplomat",
"directory", "disability", "disadvantage", "disagreement", "disaster", "disc", "discard", "disciple", "discipline", "disclaimer",
"disclosure", "disco", "discomfort", "disconnect", "discount", "discourse", "discovery", "discrepancy", "discretion", "discrimination",
"discussion", "disdain", "disease", "disgrace", "disguise", "disgust", "dishwasher", "disk", "dismissal", "disorder",
"dispatch", "dispenser", "displacement", "display", "disposal", "disposition", "dispute", "disruption", "dissection", "dissident",
"dissolution", "distance", "distinction", "distortion", "distribution", "district", "disturbance", "diver", "diversity", "dividend",
"divinity", "division", "divorce", "dizziness", "dock", "doctor", "doctrine", "document", "documentary", "documentation",
"doghouse", "dogma", "doll", "dollar", "dolphin", "domain", "dome", "domestic", "dominance", "dominion",
"donation", "donor", "doorbell", "doorway", "dormitory", "dosage", "dossier", "dot", "double", "dough",
"doughnut", "dove", "downfall", "downpayment", "downpour", "downside", "downtown", "downturn", "dozen", "draft",
"dragon", "drainage", "drama", "dramatic", "drawer", "drawing", "dream", "dresser", "dressing", "drill",
"drinking", "drive", "driver", "driveway", "drizzle", "dromedary", "droplet", "drought", "drummer", "dryer",
"duchess", "duckling", "dugout", "duke", "dumpling", "duplicate", "durability", "duration", "dusk", "dust",
"duty", "dwarf", "dwelling", "dynamics", "dynamo", "dynasty", "eagle", "earache", "eardrum", "earl",
"earnest", "earring", "earthquake", "earthworm", "easel", "east", "easter", "echo", "eclipse", "ecology",
"economist", "economy", "ecosystem", "eddy", "edge", "edition", "editor", "editorial", "education", "educator",
"eel", "effect", "efficiency", "effort", "eggplant", "ego", "ejection", "elaboration", "elastic", "elbow",
"election", "electorate", "electrician", "electricity", "electron", "electronics", "element", "elephant", "elevation", "elevator",
"eligibility", "elimination", "elite", "ellipse", "elm", "elongation", "embassy", "embroidery", "embryo", "emergency",
"emigrant", "emigration", "eminence", "emission", "emotion", "emperor", "emphasis", "empire", "employer", "employment",
"empowerment", "enactment", "enclosure", "encounter", "encouragement", "encyclopedia", "endorsement", "endurance", "enemy", "energy",
"enforcement", "engagement", "engine", "engineer", "engineering", "enigma", "enjoyment", "enlightenment", "enrollment", "ensemble",
"enterprise", "entertainment", "enthusiasm", "entity", "entrance", "entry", "envelope", "environment", "enzyme", "epic",
"epidemic", "episode", "epoch", "equality", "equation", "equator", "equipment", "equity", "equivalent", "era",
"erosion", "errand", "error", "escalator", "escape", "escort", "essay", "essence", "establishment", "estate",
"estimate", "estimation", "estuary", "eternity", "ethics", "ethnicity", "etiquette", "eucalyptus", "eulogy", "euro",
"evacuation", "evaluation", "evaporation", "evening", "event", "evidence", "evolution", "examination", "examiner", "example",
"excavation", "exception", "excerpt", "excess", "exchange", "excitement", "exclamation", "exclusion", "excursion", "execution",
"executive", "exemption", "exercise", "exhaust", "exhibition", "exile", "existence", "expansion", "expectation", "expedition",
"expense", "experience", "experiment", "expert", "expertise", "expiration", "explanation", "exploration", "explorer", "explosion",
"export", "exporter", "exposure", "expression", "extension", "extent", "exterior", "extinction", "extract", "extraction",
"extradition", "extreme", "fabrication", "facade", "facet", "facility", "facsimile", "factor", "factory", "faculty",
"fahrenheit", "failure", "fairness", "fairy", "faith", "falcon", "fallout", "fame", "familiarity", "family",
"famine", "fanatic", "fantasy", "fare", "farmhouse", "fascination", "fashion", "fastener", "fatality", "fate",
"father", "fatigue", "faucet", "favor", "favorite", "feather", "feature", "federation", "feedback", "feeling",
"fellowship", "feminist", "fencing", "ferry", "fertility", "festival", "fiber", "fiction", "fidelity", "field",
"fighter", "figure", "filament", "filing", "filter", "finale", "finance", "financier", "finding", "fingerprint",
"finish", "firearm", "fireplace", "firework", "firm", "fisherman", "fishery", "fitness", "fixture", "flame",
"flank", "flashlight", "flatware", "flavor", "flaw", "flea", "fleet", "flesh", "flexibility", "flicker",
"flood", "floor", "flour", "flower", "flu", "fluency", "fluid", "fluorescence", "flush", "flute",
"flux", "flyer", "foe", "fog", "foliage", "folklore", "follower", "folly", "fondness", "font",
"food", "footage", "football", "footprint", "footstep", "footwear", "force", "forecast", "forehead", "foreman",
"forensic", "forest", "forester", "forever", "forgiveness", "formality", "formation", "formula", "fort", "fortress",
"fortune", "forum", "foundation", "founder", "fountain", "fraction", "fracture", "fragment", "fragrance", "frame",
"framework", "franchise", "frank", "fraud", "freedom", "freelance", "freeway", "freezer", "freight", "frequency",
"fresco", "freshman", "friction", "friendship", "fright", "fringe", "frontier", "frost", "frown", "fruit",
"frustration", "fuel", "fulfillment", "function", "functionality", "fund", "funding", "fundraiser", "funeral", "furnace",
"furniture", "fusion", "futon", "future", "gadget", "galaxy", "gallery", "gamble", "game", "gang",
"garage", "garbage", "garden", "gardener", "garlic", "garment", "gas", "gasoline", "gasp", "gate",
"gateway", "gathering", "gauge", "gazebo", "gazette", "gear", "gem", "gender", "gene", "generation",
"generator", "generosity", "genesis", "genetics", "genius", "genre", "gentleman", "geography", "geology", "geometry",
"geranium", "germ", "gesture", "ghost", "giant", "gift", "gigabyte", "gimmick", "ginger", "giraffe",
"girlfriend", "girlhood", "glacier", "gladiator", "glance", "gland", "glare", "glass", "glaze", "gleam",
"glider", "glimpse", "globe", "gloom", "glory", "glossary", "glove", "glow", "glucose", "glue",
"glycerin", "goalkeeper", "goal", "goat", "goblin", "goddess", "gold", "golf", "gondola", "goodness",
"goodwill", "gorilla", "gospel", "gossip", "governor", "gown", "grace", "grade", "grader", "gradient",
"graduation", "grain", "grammar", "grandchild", "granddaughter", "grandfather", "grandmother", "grandparent", "grandson", "grandstand",
"granite", "grant", "grapefruit", "graph", "graphic", "grasp", "grass", "gratitude", "gravel", "gravitation",
"gravity", "gravy", "grease", "greatness", "greed", "greenhouse", "greeting", "grid", "grief", "grill",
"grimace", "grin", "grip", "grocery", "groom", "gross", "ground", "group", "grove", "growth",
"guarantee", "guardian", "guerrilla", "guest", "guidance", "guide", "guideline", "guild", "guilt", "guitar",
"gulf", "gum", "gunpowder", "gymnasium", "habit", "habitat", "hack", "hacker", "hail", "haircut",
"hairdo", "hairpin", "halibut", "hallway", "hamburger", "hammer", "hamster", "handbag", "handful", "handicap",
"handkerchief", "handle", "handler", "handwriting", "hanger", "happiness", "harassment", "harbor", "hardware", "harmony",
"harness", "harp", "harvest", "haste", "hatch", "hatred", "haul", "haven", "havoc", "hazard",
"headache", "headlight", "headline", "headphone", "headquarters", "headrest", "healing", "health", "heap", "hearing",
"heartbeat", "heartbreak", "heater", "heating", "heaven", "heaviness", "hedge", "heel", "height", "heir",
"helicopter", "helium", "helmet", "hemisphere", "hemorrhage", "hen", "herald", "herb", "herd", "heredity",
"heritage", "hero", "heroin", "heroine", "hesitation", "hexagon", "hierarchy", "highland", "highway", "hike",
"hiker", "hiking", "hill", "hint", "hip", "hire", "historian", "history", "hobby", "hockey",
"holder", "hole", "holiday", "hollow", "homeland", "homemade", "hometown", "homework", "homicide", "homogeny",
"honesty", "honey", "honeymoon", "honor", "hood", "hook", "hop", "hope", "horizon", "hormone",
"horn", "horror", "horse", "hose", "hospital", "hospitality", "host", "hostage", "hostel", "hostess",
"hotel", "hour", "household", "housekeeper", "housewife", "housework", "housing", "hub", "hug", "humanity",
"humidity", "humor", "hunch", "hundred", "hunger", "hunter", "hunting", "hurdle", "hurricane", "hurry",
"husband", "hush", "hybrid", "hydrant", "hydrogen", "hygiene", "hymn", "hyphen", "hypnosis", "hypothesis",
"iceberg", "icebox", "icicle", "icon", "idea", "ideal", "identity", "ideology", "idiom", "idiot",
"idol", "igloo", "ignorance", "illusion", "illustration", "image", "imagination", "imbalance", "imitation", "immigrant",
"immigration", "impact", "impairment", "impatience", "imperative", "imperial", "implication", "import", "importance", "impostor",
"impression", "imprisonment", "improvement", "impulse", "inability", "inaccuracy", "incentive", "incident", "inclination", "income",
"incompetence", "inconvenience", "increase", "independence", "index", "indication", "indicator", "indifference", "indigenous", "indigo",
"indignation", "individual", "industry", "inequality", "inertia", "infancy", "infant", "infection", "inference", "inferiority",
"inflation", "influence", "influenza", "information", "infrastructure", "ingredient", "inhabitant", "inheritance", "inhibition", "injury",
"injustice", "inlet", "inn", "innovation", "input", "inquiry", "insanity", "insect", "insertion", "insight",
"insistence", "inspection", "inspiration", "installment", "instance", "instinct", "institution", "instruction", "instructor", "instrument",
"insulation", "insurance", "insurgency", "integer", "integration", "integrity", "intellect", "intelligence", "intensity", "intention",
"interaction", "intercept", "interchange", "interest", "interface", "interference", "interior", "intermediate", "intern", "internal",
"international", "internet", "interpretation", "interruption", "intersection", "interval", "intervention", "interview", "intestine", "intimacy",
"introduction", "intuition", "invasion", "invention", "inventory", "investigator", "investment", "investor", "invitation", "invoice",
"involvement", "irony", "irrigation", "isolation", "issue", "item", "itinerary", "ivory", "jackpot", "jail",
"jam", "janitor", "jar", "jasmine", "jaw", "jazz", "jealousy", "jellyfish", "jersey", "jet",
"jewelry", "jigsaw", "jockey", "jogger", "jogging", "joint", "joker", "journal", "journalist", "journey",
"joy", "judge", "judgment", "jug", "juggler", "juice", "jumper", "junction", "jungle", "junior",
"junk", "jurisdiction", "juror", "jury", "justice", "justification", "juvenile", "kale", "kangaroo", "karate",
"kayak", "kebab", "keeper", "kennel", "kernel", "kettle", "keyboard", "keyhole", "keystone", "kick",
"kidney", "kilometer", "kindness", "king", "kingdom", "kiss", "kitchen", "kite", "kitten", "kiwi",
"knapsack", "knee", "knife", "knight", "knob", "knot", "knowledge", "lab", "label", "laboratory",
"labour", "labyrinth", "lace", "lack", "ladder", "ladle", "ladybug", "lagoon", "lair", "lakeside",
"lamb", "lament", "lamp", "landlord", "landmark", "landscape", "lane", "language", "lantern", "laptop",
"lark", "laser", "lash", "latch", "latitude", "lattice", "laughter", "launch", "laundry", "lavender",
"lavatory", "lawmaker", "lawn", "lawsuit", "lawyer", "layer", "layout", "leader", "leadership", "leaflet",
"league", "leak", "leather", "lecture", "leek", "legacy", "legend", "legislation", "legislator", "legitimacy",
"leisure", "lemonade", "lender", "length", "lens", "leopard", "leprosy", "lesson", "letter", "lettuce",
"level", "lever", "liability", "liaison", "liar", "liberation", "liberty", "librarian", "library", "licence",
"license", "lichen", "lieutenant", "lifestyle", "lifetime", "ligament", "lightning", "likelihood", "limb", "limestone",
"limit", "limousine", "lineage", "linen", "liner", "lingerie", "linguist", "link", "linoleum", "lint",
"lion", "lipstick", "liquid", "liquor", "listener", "literacy", "literature", "litigation", "litter", "livelihood",
"livestock", "lizard", "load", "loader", "loaf", "loan", "lobby", "lobster", "locality", "location",
"locker", "locket", "locomotive", "lodge", "lodging", "logic", "logo", "loneliness", "longevity", "longitude",
"lookout", "loom", "loop", "loophole", "lord", "lotion", "lottery", "lounge", "loyalty", "lubricant",
"lucidity", "luggage", "lullaby", "lumber", "luncheon", "lunchroom", "lung", "lurch", "luxury", "lyric",
"lyricist", "macaroni", "machete", "machinery", "mackerel", "madness", "magazine", "magician", "magistrate", "magnesium",
"magnet", "magnitude", "maid", "mailbox", "mailman", "mainframe", "mainland", "mainstream", "maintenance", "majesty",
"majority", "makeup", "malady", "malice", "mall", "malnutrition", "mammal", "manager", "mandate", "manger",
"mango", "mangrove", "manhole", "manhood", "manifesto", "manor", "mansion", "mantel", "mantle", "manufacturer",
"manuscript", "map", "maple", "marathon", "marble", "march", "margin", "marina", "marinade", "mariner",
"marionette", "marker", "market", "marketing", "marksman", "marriage", "marsh", "marshmallow", "martial", "martyr",
"marvel", "mascot", "masculinity", "mason", "massage", "mast", "masterpiece", "mat", "match", "mate",
"material", "maternity", "mathematician", "mathematics", "matrix", "matter", "mattress", "maturity", "maxim", "maximum",
"mayhem", "mayor", "meadow", "mealtime", "mechanic", "mechanism", "medal", "media", "mediation", "mediator",
"medication", "medicine", "meditation", "medium", "meeting", "melody", "member", "membership", "membrane", "memento",
"memoir", "memorial", "memory", "menace", "mending", "mentality", "mentor", "menu", "merchant", "mercury",
"mercy", "meridian", "merit", "mesmerize", "message", "messenger", "metabolism", "metal", "metaphor", "meteor",
"meteorology", "meter", "method", "methodology", "metropolis", "microphone", "microscope", "microwave", "midday", "midnight",
"midpoint", "midst", "migration", "mileage", "milestone", "military", "militia", "milk", "mill", "millennium",
"miller", "million", "millionaire", "mimic", "mineral", "miner", "miniature", "minimum", "minister", "ministry",
"minority", "mint", "minute", "miracle", "mirror", "mischief", "miser", "misery", "misfortune", "misgiving",
"mishap", "misunderstanding", "mitigation", "mixer", "mixture", "moat", "mobility", "mobilization", "mockery", "moderation",
"moderator", "modernity", "modification", "modifier", "modesty", "moisture", "molecule", "momentum", "monarch", "monarchy",
"monastery", "monetary", "money", "monitor", "monkey", "monologue", "monopoly", "monsoon", "monster", "monument",
"mood", "moonlight", "moonshine", "moor", "morale", "morality", "morbidity", "mortality", "mortgage", "mortuary",
"mosque", "mosquito", "moss", "motel", "moth", "mother", "motion", "motivation", "motorcycle", "mound",
"mountain", "mountaineer", "mouse", "mouth", "movement", "movie", "mower", "mud", "muffin", "mug",
"mulch", "multitude", "mumble", "mummy", "municipality", "murder", "muscle", "museum", "mushroom", "music",
"musician", "musk", "mustard", "mutation", "mutter", "mutton", "muzzle", "mystery", "mythology", "nail",
"napkin", "narrative", "narrow", "nation", "nationality", "native", "nature", "nausea", "navigation", "navigator",
"necklace", "neckline", "needle", "negation", "negligence", "negotiation", "neighbor", "neighborhood", "nemesis", "neon",
"nephew", "nerve", "nest", "network", "neurology", "neutral", "newcomer", "newsletter", "newspaper", "newsroom",
"niche", "nickname", "niece", "nightmare", "nitrogen", "nobility", "noble", "nod", "noise", "nomad",
"nomination", "nominee", "noodle", "norm", "normal", "north", "nose", "nostalgia", "notation", "notebook",
"nothing", "notice", "notification", "notion", "novel", "novelist", "novice", "nuance", "nucleus", "nuisance",
"number", "numeral", "numerator", "nurse", "nursery", "nutmeg", "nutrition", "nylon", "oasis", "oath",
"oatmeal", "obedience", "obesity", "objection", "objective", "obligation", "observatory", "observer", "obsession", "obstacle",
"occasion", "occupation", "occurrence", "ocean", "octave", "octopus", "odds", "odor", "offense", "offer",
"office", "officer", "official", "offspring", "ointment", "olive", "omelet", "omission", "omnivore", "onion",
"onlooker", "onset", "opening", "opera", "operation", "operator", "opinion", "opponent", "opportunity", "opposition",
"optimism", "optimist", "option", "oracle", "orange", "orchestra", "orchid", "order", "ordinance", "organ",
"organization", "organizer", "orientation", "origin", "original", "originality", "ornament", "orphan", "orthodox", "ostrich",
"outbreak", "outcome", "outdoor", "outfit", "outlet", "outline", "outlook", "output", "outrage", "outset",
"outskirts", "outcome", "oval", "oven", "overcoat", "overhead", "overview", "owl", "owner", "ownership",
"oxygen", "oyster", "ozone", "pace", "pacifier", "package", "packet", "pact", "paddle", "pagan",
"page", "pagoda", "pail", "pain", "paint", "painter", "painting", "pair", "palace", "palate",
"palm", "pamphlet", "pancake", "panel", "panic", "panther", "pantry", "paper", "paperback", "paperwork",
"parachute", "parade", "paradise", "paradox", "paragraph", "paralysis", "parameter", "paramount", "paranoia", "parasite",
"pardon", "parent", "parenthesis", "park", "parking", "parliament", "parlor", "parody", "parole", "parsley",
"parsnip", "participant", "participation", "particle", "particular", "partnership", "partridge", "party", "passage", "passenger",
"passion", "passport", "password", "pastor", "pastry", "pasture", "patch", "patent", "path", "pathway",
"patience", "patient", "patriot", "patrol", "patron", "pattern", "pause", "pavement", "pavilion", "paw",
"payment", "peace", "peach", "peak", "peanut", "pear", "pearl", "peasant", "pecan", "pedal",
"pedestrian", "pediatrician", "pedigree", "peer", "penalty", "pendant", "pendulum", "penetration", "penguin", "penicillin",
"peninsula", "pension", "pentagon", "people", "pepper", "peptide", "percent", "percentage", "perception", "percussion",
"perfection", "performance", "perfume", "perimeter", "period", "periodical", "peripheral", "permission", "permit", "persistence",
"person", "personality", "personnel", "perspective", "persuasion", "pesticide", "petition", "petroleum", "pharmacy", "phase",
"phenomenon", "philanthropy", "philosopher", "philosophy", "phobia", "phosphate", "photograph", "photographer", "photography", "phrase",
"physics", "physician", "physicist", "physiology", "pianist", "piano", "pickle", "pickup", "picnic", "picture",
"piece", "pier", "pigment", "pile", "pill", "pillar", "pillow", "pilot", "pin", "pinch",
"pine", "pineapple", "pink", "pint", "pioneer", "pipe", "pipeline", "piracy", "pistol", "piston",
"pitcher", "pizza", "place", "placement", "plague", "plain", "plaintiff", "plan", "plane", "planet",
"planner", "plant", "plantation", "plasma", "plaster", "plastic", "plate", "plateau", "platform", "platinum",
"platter", "playground", "playhouse", "playwright", "plea", "pleasure", "pledge", "plenty", "plight", "plot",
"plow", "plumber", "plumbing", "plunge", "plural", "pocket", "podcast", "poem", "poet", "poetry",
"point", "pointer", "poison", "polar", "polarity", "pole", "police", "policy", "polish", "politician",
"politics", "poll", "pollution", "polyester", "polymer", "pond", "pony", "pool", "popcorn", "pope",
"poplar", "poppy", "popularity", "population", "porch", "pork", "porridge", "port", "portfolio", "portion",
"portrait", "position", "positive", "possession", "possibility", "post", "postage", "poster", "posture", "pot",
"potato", "potential", "pottery", "poultry", "poverty", "powder", "power", "practice", "praise", "prayer",
"precedent", "precipitation", "precision", "predecessor", "predicate", "prediction", "preference", "prefix", "pregnancy", "prejudice",
"premium", "preparation", "presence", "present", "presentation", "preservation", "president", "press", "pressure", "prestige",
"presumption", "pretense", "prevention", "preview", "price", "pride", "priest", "primary", "prime", "prince",
"princess", "principal", "principle", "print", "printer", "priority", "prison", "prisoner", "privacy", "private",
"privilege", "prize", "probability", "problem", "procedure", "process", "processor", "produce", "producer", "product",
"production", "productivity", "profession", "professional", "professor", "profile", "profit", "program", "programmer", "progress",
"progression", "prohibition", "project", "projection", "projector", "prologue", "promise", "promotion", "prompt", "proof",
"propaganda", "propeller", "property", "prophecy", "proponent", "proportion", "proposal", "proposition", "prosecution", "prospect",
"prosperity", "protection", "protein", "protest", "protocol", "provider", "province", "provision", "psychology", "psychiatrist",
"public", "publication", "publicity", "publisher", "pudding", "pulse", "pump", "pumpkin", "punch", "punishment",
"pupil", "puppet", "purchase", "purple", "purpose", "purse", "pursuit", "push", "puzzle", "pyramid",
"qualification", "quality", "quantity", "quantum", "quarrel", "quarter", "quartet", "quartz", "queen", "query",
"question", "questionnaire", "queue", "quilt", "quota", "quotation", "quote", "rabbit", "raccoon", "race",
"racer", "racquet", "radar", "radiation", "radiator", "radical", "radio", "radius", "raffle", "raft",
"rag", "rage", "raid", "rail", "railroad", "railway", "rainbow", "raincoat", "rainfall", "rally",
"ram", "ramp", "ranch", "random", "range", "ranger", "rank", "rap", "rape", "rapid",
"rapport", "rat", "rate", "rating", "ratio", "rationale", "raven", "ravine", "ray", "razor",
"reaction", "reader", "reading", "reality", "realm", "reaper", "reason", "rebel", "rebellion", "rebound",
"rebuild", "recall", "receipt", "receiver", "reception", "recession", "recipe", "recipient", "recognition", "recommendation",
"reconciliation", "reconstruction", "record", "recorder", "recovery", "recreation", "recruit", "rectangle", "recurrence", "recyclable",
"recycling", "redemption", "reduction", "redundancy", "reference", "referendum", "reflection", "reflex", "reform", "refuge",
"refugee", "refund", "refusal", "refuse", "regard", "regime", "region", "register", "registration", "regret",
"regulation", "regulator", "rehabilitation", "rehearsal", "reign", "reimbursement", "reinforcement", "rejection", "relation", "relationship",
"relative", "relaxation", "release", "relevance", "reliability", "relief", "religion", "relocation", "remainder", "reminder",
"remnant", "remote", "removal", "renaissance", "renewal", "renovation", "rent", "replacement", "replica", "replication",
"reply", "report", "reporter", "representation", "representative", "reproduction", "republic", "reputation", "request", "requirement",
"rescue", "research", "researcher", "resemblance", "reservation", "reserve", "reservoir", "residence", "resident", "residue",
"resignation", "resilience", "resistance", "resolution", "resort", "resource", "respect", "response", "responsibility", "restaurant",
"restoration", "restraint", "restriction", "result", "resume", "retail", "retailer", "retention", "retirement", "retreat",
"retrieval", "return", "reunion", "revenue", "reversal", "review", "revision", "revival", "revolution", "reward",
"rhetoric", "rhinoceros", "rhyme", "rhythm", "ribbon", "rice", "riddle", "rider", "ridge", "ridicule",
"rifle", "right", "rigor", "rim", "ring", "riot", "rite", "ritual", "rival", "river",
"road", "roadside", "roast", "robot", "rock", "rocket", "rod", "role", "roll", "roller",
"romance", "roof", "room", "rooster", "root", "rope", "rose", "roster", "rotation", "round",
"route", "routine", "row", "royalty", "rubber", "rubble", "rugby", "rule", "ruler", "rumor",
"runway", "rural", "rush", "rust", "sabotage", "sack", "sacrifice", "saddle", "safari", "safety",
"sail", "sailor", "salad", "salary", "sale", "salesman", "salmon", "salon", "saloon", "salt",
"salute", "salvage", "sample", "sanction", "sanctuary", "sand", "sandwich", "sanitation", "sanity", "sardine",
"satellite", "satin", "satire", "satisfaction", "sauce", "saucer", "sausage", "savage", "savings", "saxophone",
"scale", "scandal", "scanner", "scenario", "scene", "scenery", "scent", "schedule", "scheme", "scholar",
"scholarship", "school", "science", "scientist", "scoop", "scope", "score", "scorer", "scorn", "scout",
"scrap", "screen", "script", "scrutiny", "sculpture", "seal", "search", "season", "seat", "second",
"secret", "secretary", "section", "sector", "security", "segment", "selection", "selector", "semester", "seminar",
"senate", "senator", "sender", "senior", "sensation", "sense", "sensitivity", "sensor", "sentence", "sentiment",
"separation", "sequence", "serial", "series", "servant", "server", "service", "session", "settlement", "settler",
"setup", "severity", "sewage", "sewer", "shack", "shadow", "shaft", "shame", "shampoo", "shape",
"share", "shareholder", "shark", "shed", "sheep", "sheet", "shelf", "shell", "shelter", "shepherd",
"shield", "shift", "shine", "ship", "shipment", "shirt", "shock", "shoe", "shoot", "shop",
"shore", "shortage", "shortcut", "shot", "shoulder", "shovel", "show", "shower", "shred", "shrimp",
"shrine", "shrink", "shrug", "shuffle", "shutdown", "sibling", "sidewalk", "sight", "sign", "signal",
"signature", "significance", "silence", "silicon", "silk", "sill", "silver", "similarity", "simple", "simplicity",
"simulation", "sin", "singer", "single", "sink", "sister", "site", "situation", "size", "skate",
"skeleton", "sketch", "ski", "skill", "skin", "skirt", "skull", "sky", "skyscraper", "slab",
"slang", "slave", "slavery", "sleep", "sleeve", "slice", "slide", "slope", "slot", "slum",
"smell", "smile", "smoke", "snack", "snake", "snap", "snow", "soap", "soccer", "social",
"society", "sock", "sodium", "sofa", "software", "soil", "solar", "soldier", "solid", "solution",
"solvent", "sonata", "song", "sonnet", "soprano", "sorrow", "sort", "soul", "sound", "soup",
"source", "south", "space", "spade", "span", "spark", "speaker", "specialist", "species", "specimen",
"spectacle", "spectrum", "speech", "speed", "spell", "sphere", "spice", "spider", "spike", "spin",
"spine", "spirit", "spiritual", "splash", "split", "sponsor", "spoon", "sport", "spot", "spray",
"spread", "spring", "sprint", "spy", "square", "squeeze", "squirrel", "stability", "stable", "stadium",
"staff", "stage", "stain", "stair", "stake", "stall", "stamp", "stand", "standard", "standing",
"staple", "star", "starch", "stardom", "start", "state", "statement", "station", "statistic", "statue",
"status", "statute", "stay", "steak", "steam", "steel", "stem", "step", "stereo", "stew",
"stick", "stigma", "stimulus", "sting", "stock", "stomach", "stone", "stool", "stop", "storage",
"store", "storm", "story", "stove", "strategy", "straw", "stream", "street", "strength", "stress",
"stretch", "strike", "string", "strip", "stroke", "structure", "struggle", "student", "studio", "study",
"stuff", "stumble", "style", "subject", "submission", "substance", "substitute", "suburb", "subway", "success",
"succession", "sucker", "suffix", "sugar", "suggestion", "suit", "suite", "summary", "summer", "summit",
"sun", "sunset", "supermarket", "supervisor", "supplement", "supplier", "supply", "support", "surface", "surgery",
"surprise", "surround", "survey", "survival", "survivor", "suspect", "suspension", "sustain", "swallow", "swamp",
"swap", "swarm", "sweat", "sweater", "sweep", "sweet", "swim", "swing", "switch", "sword",
"symbol", "sympathy", "symptom", "syndrome", "synonym", "synthesis", "syrup", "system", "table", "tablet",
"tackle", "tactic", "tag", "tail", "tailor", "tale", "talent", "talk", "tank", "tap",
"tape", "target", "task", "taste", "tattoo", "tax", "taxi", "tea", "teacher", "team",
"tear", "technique", "technology", "telephone", "telescope", "television", "temper", "temperature", "temple", "tenant",
"tendency", "tennis", "tension", "tent", "term", "terminal", "terrace", "terrain", "territory", "terror",
"test", "testament", "testimony", "text", "textbook", "texture", "thanks", "theme", "theory", "therapy",
"thesis", "thigh", "thing", "thinking", "thirst", "thought", "thread", "threat", "threshold", "thrill",
"throat", "throne", "thumb", "thunder", "ticket", "tide", "tiger", "tile", "till", "timber",
"time", "timeline", "timer", "tin", "tip", "tire", "tissue", "title", "toast", "tobacco",
"today", "toe", "toilet", "token", "tomato", "tomorrow", "tone", "tongue", "tonight", "tool",
"tooth", "topic", "torch", "tornado", "tortoise", "total", "touch", "tour", "tourist", "tournament",
"tower", "town", "toy", "trace", "track", "trade", "tradition", "traffic", "tragedy", "train",
"trainer", "training", "trait", "transfer", "transit", "transition", "translation", "transmission", "transparency", "transport",
"trap", "trash", "travel", "treasure", "treatment", "tree", "tremor", "trend", "trial", "tribe",
"trick", "trigger", "trim", "trip", "trophy", "trouble", "truck", "trust", "truth", "tube",
"tune", "tunnel", "turkey", "turn", "tutor", "tutorial", "twilight", "twin", "twist", "type",
"typical", "tyrant", "umbrella", "uncle", "under", "understanding", "union", "unique", "unit", "universe",
"university", "update", "upgrade", "upload", "upper", "urban", "urgency", "usage", "user", "usual",
"utility", "vacation", "vaccine", "vacuum", "valley", "value", "valve", "van", "variant", "variation",
"variety", "various", "vault", "vehicle", "velocity", "vendor", "venture", "venue", "verb", "version",
"vessel", "veteran", "victim", "victory", "video", "view", "viewer", "village", "vine", "vinegar",
"violation", "violence", "violin", "virtual", "virus", "visa", "vision", "visit", "visitor", "visual",
"vital", "vitamin", "vocal", "voice", "volume", "vote", "voter", "voyage", "wage", "wagon",
"wait", "waiter", "wake", "walk", "wall", "wallet", "walnut", "want", "war", "ward",
"wardrobe", "warehouse", "warmth", "warning", "warrant", "warrior", "wash", "washer", "waste", "watch",
"water", "wave", "way", "weakness", "wealth", "weapon", "wear", "weather", "web", "wedding",
"week", "weekend", "weight", "welcome", "welfare", "west", "wheel", "where", "while", "whip",
"whiskey", "whisper", "white", "whole", "width", "wife", "wild", "will", "wind", "window",
"wine", "wing", "winner", "winter", "wire", "wisdom", "wish", "witness", "wolf", "woman",
"wonder", "wood", "woodland", "wool", "word", "work", "worker", "workplace", "world", "worry",
"worth", "wrap", "wreck", "wrist", "write", "writer", "writing", "wrong", "yard", "year",
"yesterday", "yield", "yoga", "yogurt", "youth", "zebra", "zero", "zone", "zoo"
)
// 合并词表(基础 + 扩展,约 5 倍词库)
val ALL_MNEMONIC_WORDS = MNEMONIC_WORDS + MNEMONIC_WORDS_EXTRA
// 助记词风格包名:从合并词表中随机选词,格式 com.{word1}.{word2}.{word3}.{word4}
// 类似冷钱包助记词,易读且符合 Android 包名规范(小写字母)
fun generateRandomPackageName(): String {
val words = ALL_MNEMONIC_WORDS.shuffled().take(4)
return "com.${words.joinToString(".")}"
}
// 包名生成逻辑
// 优先使用手动指定的包名,否则生成完全随机的包名
fun computeApplicationId(): String {
val manualAppId = project.findProperty("applicationId") as String?
?: System.getenv("APPLICATION_ID")
if (manualAppId != null && manualAppId.isNotBlank()) {
return manualAppId
}
val packageSuffix = project.findProperty("packageSuffix") as String?
?: System.getenv("PACKAGE_SUFFIX")
?: System.getenv("CI_COMMIT_BRANCH")?.replace("/", "_")
?: System.getenv("GITHUB_REF_NAME")?.replace("/", "_")
return if (packageSuffix != null && packageSuffix.isNotBlank()) {
"io.openim.flutter.demo.$packageSuffix"
} else {
generateRandomPackageName()
}
}
android {
namespace = "io.openim.flutter.openim"
compileSdk = flutter.compileSdkVersion
ndkVersion = "27.0.12077973" // 使用已存在的NDK版本避免下载问题
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
isCoreLibraryDesugaringEnabled = true
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
lint {
checkReleaseBuilds = false
abortOnError = false
}
// 签名配置
signingConfigs {
// Debug 签名(默认)
getByName("debug") {
// 使用默认的 debug keystore
}
// Release 签名配置
// 优先级:
// 1. 手动指定的签名(环境变量或 Gradle 属性)
// 2. 自动生成的随机签名(每次打包都不同)
// 3. Debug 签名(降级方案)
create("release") {
// 方式1: 手动指定的签名(最高优先级)
val manualKeystoreFile = project.findProperty("keystoreFile") as String?
?: System.getenv("KEYSTORE_FILE")
val manualKeystorePassword = project.findProperty("keystorePassword") as String?
?: System.getenv("KEYSTORE_PASSWORD")
val manualKeyAlias = project.findProperty("keyAlias") as String?
?: System.getenv("KEY_ALIAS")
val manualKeyPassword = project.findProperty("keyPassword") as String?
?: System.getenv("KEY_PASSWORD")
if (manualKeystoreFile != null && manualKeystorePassword != null &&
manualKeyAlias != null && manualKeyPassword != null) {
val keystore = file(manualKeystoreFile)
if (keystore.exists()) {
storeFile = keystore
storePassword = manualKeystorePassword
this.keyAlias = manualKeyAlias
this.keyPassword = manualKeyPassword
println("✓ 使用手动指定的 release 签名: $manualKeystoreFile")
return@create
} else {
println("警告: Keystore 文件不存在: $manualKeystoreFile")
}
}
// 方式2: 自动生成随机签名(每次打包都不同)
val generateRandom = System.getenv("GENERATE_RANDOM_KEYSTORE")
?: project.findProperty("generateRandomSigning") as String?
?: "true" // 默认启用随机签名
if (generateRandom == "true") {
// 将签名文件保存在 build 目录,构建清理时会自动删除
val buildDir = layout.buildDirectory.get().asFile
val keystoreDir = File(buildDir, "keystore")
keystoreDir.mkdirs()
// 生成唯一的文件名(基于时间戳和随机字符串)
val timestamp = System.currentTimeMillis()
val random = SecureRandom()
val randomBytes = ByteArray(8)
random.nextBytes(randomBytes)
val randomSuffix = Base64.getEncoder().encodeToString(randomBytes)
.replace("=", "")
.replace("/", "_")
.replace("+", "-")
.substring(0, 8)
val keystoreFile = File(keystoreDir, "release_${timestamp}_${randomSuffix}.jks")
val keyAlias = "release_key_${randomSuffix}"
// 生成随机密码16位
val randomPasswordBytes = ByteArray(16)
random.nextBytes(randomPasswordBytes)
val keystorePassword = Base64.getEncoder().encodeToString(randomPasswordBytes)
.replace("=", "")
.substring(0, 16)
// 每次打包都生成新的签名(删除旧的同名文件,如果有)
if (keystoreFile.exists()) {
keystoreFile.delete()
}
println("🔐 正在生成随机 release 签名...")
println(" Keystore: ${keystoreFile.absolutePath}")
println(" Alias: $keyAlias")
// 使用当前 JVM 的 keytool避免 CI/容器中 PATH 无 keytool 或 spawn 失败
val keytoolName = if (System.getProperty("os.name").lowercase().contains("win")) "keytool.exe" else "keytool"
val keytoolPath = File(System.getProperty("java.home"), "bin/$keytoolName").absolutePath
val keytoolFile = File(keytoolPath)
if (!keytoolFile.exists()) {
println("✗ 未找到 keytool: $keytoolPath")
println(" 将使用 debug 签名作为降级方案")
return@create
}
val processBuilder = ProcessBuilder(
keytoolPath,
"-genkey",
"-v",
"-keystore", keystoreFile.absolutePath,
"-alias", keyAlias,
"-keyalg", "RSA",
"-keysize", "2048",
"-validity", "10000", // 有效期约 27 年
"-storepass", keystorePassword,
"-keypass", keystorePassword,
"-dname", "CN=App, OU=Development, O=Company, L=City, ST=State, C=US"
)
val process = try {
processBuilder.start()
} catch (e: Exception) {
println("✗ 执行 keytool 失败 (如 CI/root 环境 spawn 受限): ${e.message}")
println(" 将使用 debug 签名作为降级方案")
return@create
}
val exitCode = process.waitFor()
if (exitCode == 0) {
if (keystoreFile.exists()) {
println("✓ 随机签名生成成功: ${keystoreFile.absolutePath}")
println(" 文件大小: ${keystoreFile.length()} 字节")
} else {
println("✗ 签名文件生成失败: 文件不存在")
println(" 将使用 debug 签名作为降级方案")
return@create
}
} else {
val errorOutput = process.errorStream.bufferedReader().readText()
println("✗ 签名生成失败: $errorOutput")
println(" 将使用 debug 签名作为降级方案")
return@create
}
storeFile = keystoreFile
storePassword = keystorePassword
this.keyAlias = keyAlias
this.keyPassword = keystorePassword
println("✓ 使用随机生成的 release 签名")
return@create
}
// 方式3: 降级到 debug 签名
println("⚠️ 未配置 release 签名,将使用 debug 签名")
}
}
defaultConfig {
// 动态 Application ID可通过环境变量或 Gradle 属性修改)
val computedAppId = computeApplicationId()
applicationId = computedAppId
println("构建包名: $computedAppId")
// 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 = 33
versionCode = flutter.versionCode
versionName = flutter.versionName
multiDexEnabled = true
// .so 文件加固配置
// 注意:当使用 flutter build apk --split-per-abi 或 --target-platform 时,
// Flutter 会自动设置 ABI 过滤器,这里不需要手动配置,否则会冲突
// 如果需要手动控制架构,可以通过以下方式:
// 1. 不使用 --split-per-abi使用完整的 APK
// 2. 或者移除这里的 abiFilters 配置,让 Flutter 命令行参数控制
//
// 如果需要手动设置,取消下面的注释(但不要与 --split-per-abi 同时使用)
// ndk {
// // 只打包主流架构,减少暴露风险
// // 建议只打包 arm64-v8a主流设备和 armeabi-v7a兼容旧设备
// abiFilters += listOf("arm64-v8a", "armeabi-v7a")
// }
}
buildTypes {
release {
// 如果配置了 release 签名,优先使用;否则使用 debug 签名
val releaseSigningConfig = signingConfigs.findByName("release")
if (releaseSigningConfig != null && releaseSigningConfig.storeFile != null) {
signingConfig = releaseSigningConfig
println("使用自定义 release 签名")
} else {
signingConfig = signingConfigs.getByName("debug")
println("使用 debug 签名release 构建)")
}
// 按照OpenIM官方文档建议默认禁用混淆解决白屏问题
// 如果需要启用混淆加固,可以通过环境变量 ENABLE_PROGUARD=true 来启用
val enableProguard = project.findProperty("enableProguard") as String?
?: System.getenv("ENABLE_PROGUARD")
?: "false"
if (enableProguard == "true") {
println("⚠️ 已启用代码混淆(可能导致白屏问题,请充分测试)")
// 启用代码混淆和资源压缩(简单加固)
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
} else {
println("✅ 已禁用代码混淆按照OpenIM官方建议避免白屏问题")
// 按照OpenIM官方文档建议禁用混淆解决白屏问题
isMinifyEnabled = false
isShrinkResources = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
// 启用代码优化(不影响混淆)
isDebuggable = false
isJniDebuggable = false
isRenderscriptDebuggable = false
}
}
// .so 文件加固:打包后处理任务(可选)
// 注意:这需要在构建后手动处理,或者使用第三方加固工具
// 这里只是提供配置示例
packaging {
jniLibs {
// 仅排除调试符号(减少 APK 体积)。不要排除 libc++_shared.so否则 release 安装后闪退Flutter 引擎/插件依赖该库)
excludes += listOf("**/*.so.dbg", "**/*.so.debug")
}
// 移除未使用的资源
resources {
excludes += listOf("META-INF/*.kotlin_module", "META-INF/*.version")
}
}
}
flutter {
source = "../.."
}
dependencies {
implementation("androidx.core:core:1.13.1")
implementation("androidx.multidex:multidex:2.0.1")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
// 华为/荣耀等设备 SSL 握手失败修复(不依赖 Google Play Services使用 Conscrypt 替代 GMS ProviderInstaller
implementation("org.conscrypt:conscrypt-android:2.5.2")
}

126
android/app/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,126 @@
# ============================================
# 代码混淆和加固配置
# ============================================
# 优化配置
-optimizationpasses 5
-dontusemixedcaseclassnames
-dontskipnonpubliclibraryclasses
-verbose
# 移除日志减少APK大小提高安全性
-assumenosideeffects class android.util.Log {
public static *** d(...);
public static *** v(...);
public static *** i(...);
public static *** w(...);
public static *** e(...);
public static *** println(...);
}
# ============================================
# OpenIM SDK混淆规则
# ============================================
-keep class io.openim.** { *; }
-keep class open_im_sdk.** { *; }
-keep class open_im_sdk_callback.** { *; }
# ============================================
# Flutter相关(必须保留)
# ============================================
-keep class io.flutter.** { *; }
-keep class io.flutter.plugins.** { *; }
-keep class io.flutter.embedding.** { *; }
# Flutter Engine
-keep class io.flutter.embedding.engine.** { *; }
-keep class io.flutter.plugin.common.** { *; }
# ============================================
# 保持所有native方法
# ============================================
-keepclasseswithmembernames class * {
native <methods>;
}
# ============================================
# 保持所有枚举
# ============================================
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
# ============================================
# 保持所有Serializable类
# ============================================
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
# ============================================
# 保持反射使用的类
# ============================================
-keepattributes Signature
-keepattributes *Annotation*
-keepattributes EnclosingMethod
-keepattributes InnerClasses
-keepattributes Exceptions
# ============================================
# 保持Parcelable
# ============================================
-keepclassmembers class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator CREATOR;
}
# ============================================
# 保持R类资源类
# ============================================
-keepclassmembers class **.R$* {
public static <fields>;
}
# ============================================
# 混淆配置(提高安全性)
# ============================================
# 混淆类名
-keepnames class * extends android.app.Activity
-keepnames class * extends android.app.Application
-keepnames class * extends android.app.Service
-keepnames class * extends android.content.BroadcastReceiver
-keepnames class * extends android.content.ContentProvider
# 保持MainActivity应用入口
-keep class io.openim.flutter.openim.MainActivity { *; }
# ============================================
# 防止反编译(增强加固)
# ============================================
# 移除行号信息(提高安全性,崩溃日志可通过符号文件还原)
-keepattributes SourceFile
-renamesourcefileattribute SourceFile
# 移除调试信息
-assumenosideeffects class * {
public static *** print*(...);
public static *** println*(...);
}
# 混淆类名和包名(增强安全性)
-repackageclasses ''
-flattenpackagehierarchy ''
# 优化配置(增强混淆效果)
-optimizationpasses 5
-allowaccessmodification
-dontpreverify
# 移除未使用的代码
-dontwarn **

View File

@@ -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>

View File

@@ -0,0 +1,111 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 网络权限 -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<!-- 存储权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<!-- Image Cropper 权限 -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<!-- 相机和麦克风权限 -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<!-- 通知权限 -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- 系统窗口权限 -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<!-- 设备信息权限 -->
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<!-- 位置权限(如果需要) -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- 蓝牙权限(音视频通话可能需要) -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- 音频焦点权限 -->
<uses-permission android:name="android.permission.AUDIO_FOCUS" />
<!-- 前台服务权限 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
<!-- 安装应用权限(升级功能) -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<!-- 查询其他应用权限 -->
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
<application
android:label="集中营"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:networkSecurityConfig="@xml/network_security_config"
android:usesCleartextTraffic="true">
<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>
<!-- File Provider for APK install/update flows inherited from the old app. -->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<!-- 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>

View File

@@ -0,0 +1,5 @@
package io.openim.flutter.im_webview_app
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

View File

@@ -0,0 +1,146 @@
package io.openim.flutter.openim
import android.content.Intent
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.util.Log
import androidx.core.content.FileProvider
import io.flutter.embedding.android.FlutterActivity
import org.conscrypt.Conscrypt
import java.security.Security
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import java.io.File
class MainActivity : FlutterActivity() {
private val CHANNEL = "io.openim.flutter.openim/apk_info"
private val TAG = "MainActivity"
override fun onCreate(savedInstanceState: android.os.Bundle?) {
// 华为/荣耀/OPPO 等国产设备:在任意网络请求之前同步安装 Conscrypt修复 SSL 握手失败(无 GMS 时系统 SSL 实现不完整)
try {
Security.insertProviderAt(Conscrypt.newProvider(), 1)
Log.d(TAG, "Conscrypt security provider installed (SSL fix for domestic devices)")
} catch (e: Exception) {
Log.w(TAG, "Conscrypt provider install failed: ${e.message}")
}
super.onCreate(savedInstanceState)
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"getApkPackageName" -> {
val apkPath = call.argument<String>("apkPath")
if (apkPath != null) {
try {
val packageName = getApkPackageName(apkPath)
result.success(packageName)
} catch (e: Exception) {
result.error("ERROR", "无法解析APK包名: ${e.message}", null)
}
} else {
result.error("ERROR", "APK路径为空", null)
}
}
"getCurrentPackageName" -> {
try {
val packageName = packageName
result.success(packageName)
} catch (e: Exception) {
result.error("ERROR", "无法获取当前包名: ${e.message}", null)
}
}
"canRequestPackageInstalls" -> {
try {
val canInstall = canRequestPackageInstalls()
result.success(canInstall)
} catch (e: Exception) {
result.error("ERROR", "无法检查安装权限: ${e.message}", null)
}
}
"installApk" -> {
val apkPath = call.argument<String>("apkPath")
if (apkPath != null) {
try {
val success = installApk(apkPath)
result.success(success)
} catch (e: Exception) {
result.error("ERROR", "安装APK失败: ${e.message}", null)
}
} else {
result.error("ERROR", "APK路径为空", null)
}
}
else -> {
result.notImplemented()
}
}
}
}
private fun getApkPackageName(apkPath: String): String? {
return try {
val packageManager = packageManager
val packageInfo: PackageInfo = packageManager.getPackageArchiveInfo(
apkPath,
PackageManager.GET_ACTIVITIES
) ?: return null
packageInfo.packageName
} catch (e: Exception) {
null
}
}
/// 检查是否可以请求安装包权限Android 8.0+
private fun canRequestPackageInstalls(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// Android 8.0+ 需要检查安装权限
packageManager.canRequestPackageInstalls()
} else {
// Android 8.0 以下版本默认允许安装
true
}
}
/// 使用 Intent 直接打开安装界面
private fun installApk(apkPath: String): Boolean {
return try {
val file = File(apkPath)
if (!file.exists()) {
return false
}
val intent = Intent(Intent.ACTION_VIEW).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
val apkUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// Android 7.0+ 使用 FileProvider
FileProvider.getUriForFile(
this@MainActivity,
"${packageName}.fileprovider",
file
)
} else {
// Android 7.0 以下直接使用 file://
Uri.fromFile(file)
}
setDataAndType(apkUri, "application/vnd.android.package-archive")
// Android 7.0+ 需要添加临时读取权限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
}
startActivity(intent)
true
} catch (e: Exception) {
false
}
}
}

View File

@@ -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>

View File

@@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -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>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">本地打包</string>
</resources>

View File

@@ -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>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="external_files" path="."/>
<cache-path name="cache" path="."/>
<external-cache-path name="external_cache" path="."/>
<files-path name="files" path="."/>
</paths>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
国内网络 / 国产手机华为、荣耀、OPPO、小米等兼容配置
- cleartextTrafficPermitted允许 HTTP与后端是否 HTTPS 无关,部分 ROM 会严格检查
- trust-anchors system + user系统证书 + 用户证书,兼容企业/校园网等需安装根证书的环境
-->
<network-security-config>
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system" />
<!-- 兼容国内企业/校园网等需安装根证书或代理证书的环境(华为/荣耀/OPPO 等部分场景) -->
<certificates src="user" />
</trust-anchors>
</base-config>
</network-security-config>

View File

@@ -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>

149
android/build.gradle.kts Normal file
View File

@@ -0,0 +1,149 @@
allprojects {
repositories {
// 优先使用阿里云镜像(国内访问更快)
maven { url = uri("https://maven.aliyun.com/repository/public") }
maven { url = uri("https://maven.aliyun.com/repository/google") }
maven { url = uri("https://maven.aliyun.com/repository/central") }
maven { url = uri("https://maven.aliyun.com/repository/gradle-plugin") }
// 备用:官方仓库
google()
mavenCentral()
gradlePluginPortal()
}
// 固定 rtc_room_engine 版本,避免解析 3.6.+ 时去 JitPack 拉 maven-metadata 导致 Read timed out服务器网络
// 该依赖在 Maven Central 有 3.6.x用固定版本后从阿里云/中央仓库解析即可
configurations.all {
resolutionStrategy {
force("io.trtc.uikit:rtc_room_engine:3.6.4.104")
}
}
}
buildscript {
repositories {
// 优先使用阿里云镜像(国内访问更快)
maven { url = uri("https://maven.aliyun.com/repository/public") }
maven { url = uri("https://maven.aliyun.com/repository/google") }
maven { url = uri("https://maven.aliyun.com/repository/central") }
maven { url = uri("https://maven.aliyun.com/repository/gradle-plugin") }
// 备用:官方仓库
google()
mavenCentral()
gradlePluginPortal()
}
dependencies {
// Google Play Services / Firebase 已移除,不再需要 google-services
}
}
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")
// 为所有缺少 namespace 的 Android 库项目自动设置 namespace解决 AGP 8.x 要求)
// 使用 whenPluginAdded 在插件应用时立即配置
project.plugins.whenPluginAdded {
if (this is com.android.build.gradle.LibraryPlugin) {
// 在插件应用时立即配置,不要延迟到 afterEvaluate
try {
project.extensions.configure<com.android.build.gradle.LibraryExtension>("android") {
// 检查 namespace 是否已设置
val currentNamespace = try {
this::class.java.getDeclaredField("namespace").apply { isAccessible = true }.get(this) as? String
} catch (e: Exception) {
null
}
if (currentNamespace.isNullOrBlank()) {
// 尝试从 AndroidManifest.xml 读取 package 作为 namespace
val manifestFile = project.file("src/main/AndroidManifest.xml")
val namespaceValue = if (manifestFile.exists()) {
try {
val manifestContent = manifestFile.readText()
val packageMatch = Regex("package=\"([^\"]+)\"").find(manifestContent)
packageMatch?.groupValues?.get(1)
} catch (e: Exception) {
null
}
} else {
null
}
// 如果从 manifest 读取失败,根据项目名称设置默认值
val finalNamespace = namespaceValue ?: when {
project.name.contains("flutter_openim_sdk", ignoreCase = true) -> "io.openim.flutter.openim_sdk"
project.name.contains("flutter_openim", ignoreCase = true) -> "io.openim.flutter.${project.name.replace("-", "_")}"
else -> null
}
if (finalNamespace != null) {
namespace = finalNamespace
println("已为 ${project.name} 设置 namespace: $finalNamespace")
}
}
}
} catch (e: Exception) {
// 如果扩展还不存在,忽略错误
println("警告: 无法为 ${project.name} 配置 namespace: ${e.message}")
}
}
}
// 双重保险:也尝试使用 withId适用于插件已应用的情况
project.plugins.withId("com.android.library") {
try {
project.extensions.configure<com.android.build.gradle.LibraryExtension>("android") {
// 检查 namespace 是否已设置
val currentNamespace = try {
this::class.java.getDeclaredField("namespace").apply { isAccessible = true }.get(this) as? String
} catch (e: Exception) {
null
}
if (currentNamespace.isNullOrBlank()) {
// 尝试从 AndroidManifest.xml 读取 package 作为 namespace
val manifestFile = project.file("src/main/AndroidManifest.xml")
val namespaceValue = if (manifestFile.exists()) {
try {
val manifestContent = manifestFile.readText()
val packageMatch = Regex("package=\"([^\"]+)\"").find(manifestContent)
packageMatch?.groupValues?.get(1)
} catch (e: Exception) {
null
}
} else {
null
}
// 如果从 manifest 读取失败,根据项目名称设置默认值
val finalNamespace = namespaceValue ?: when {
project.name.contains("flutter_openim_sdk", ignoreCase = true) -> "io.openim.flutter.openim_sdk"
project.name.contains("flutter_openim", ignoreCase = true) -> "io.openim.flutter.${project.name.replace("-", "_")}"
else -> null
}
if (finalNamespace != null) {
namespace = finalNamespace
println("已为 ${project.name} 设置 namespace (withId): $finalNamespace")
}
}
}
} catch (e: Exception) {
// 如果扩展还不存在,忽略错误
println("警告: 无法为 ${project.name} 配置 namespace (withId): ${e.message}")
}
}
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

22
android/gradle.properties Normal file
View File

@@ -0,0 +1,22 @@
org.gradle.jvmargs=-Xmx6144M -XX:MaxMetaspaceSize=1536m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
org.gradle.parallel=true
# 若服务器出现 "Unable to delete directory" 或 Kotlin 缓存冲突,可临时改为 false 再打包
org.gradle.parallel=true
# 服务器打包时拉取依赖易超时,适当增大 HTTP 超时(如 JitPack/Maven
systemProp.org.gradle.internal.http.connectionTimeout=120000
systemProp.org.gradle.internal.http.socketTimeout=120000
# 关闭 Kotlin 增量编译,避免 CI 多任务并行时 "Could not close incremental caches" / "Storage is already registered"
kotlin.incremental=false
org.gradle.configuration-cache=false
org.gradle.daemon=true
# 禁用文件系统监听器,避免 "Already watching path" 错误
org.gradle.vfs.watch=false
# 增加 daemon 空闲超时时间,避免频繁重启
org.gradle.daemon.idletimeout=10800000
# 禁用构建扫描CI 环境不需要)
org.gradle.unsafe.configuration-cache.max-problems=0
# 优化构建性能
org.gradle.caching=true
android.useAndroidX=true
android.enableJetifier=true

View File

@@ -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.12-all.zip

View File

@@ -0,0 +1,32 @@
pluginManagement {
repositories {
// 优先使用阿里云镜像(国内访问更快)
maven { url = uri("https://maven.aliyun.com/repository/public") }
maven { url = uri("https://maven.aliyun.com/repository/google") }
maven { url = uri("https://maven.aliyun.com/repository/central") }
maven { url = uri("https://maven.aliyun.com/repository/gradle-plugin") }
// 备用:官方仓库
google()
mavenCentral()
gradlePluginPortal()
}
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")
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.9.1" apply false
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
}
include(":app")

7
deploy-app.sh Executable file
View File

@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
"$SCRIPT_DIR/scripts/deploy-apk.sh" "$@"

34
ios/.gitignore vendored Normal file
View File

@@ -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

View File

@@ -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>

View File

@@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@@ -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 = io.openim.flutter.imWebviewApp;
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 = io.openim.flutter.imWebviewApp.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 = io.openim.flutter.imWebviewApp.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 = io.openim.flutter.imWebviewApp.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 = io.openim.flutter.imWebviewApp;
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 = io.openim.flutter.imWebviewApp;
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 */;
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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)
}
}

View File

@@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

@@ -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.

View File

@@ -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>

View File

@@ -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>

121
ios/Runner/Info.plist Normal file
View File

@@ -0,0 +1,121 @@
<?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>集中营</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>集中营</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>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>comgooglemaps</string>
<string>baidumap</string>
<string>iosamap</string>
<string>waze</string>
<string>yandexmaps</string>
<string>yandexnavi</string>
<string>citymapper</string>
<string>mapswithme</string>
<string>osmandmaps</string>
<string>dgis</string>
<string>qqmap</string>
<string>here-location</string>
</array>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSAppleMusicUsageDescription</key>
<string>请点击“好”以允许访问。若不允许,你将无法给好友发送或者上传本地相册图片及视频内容。</string>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>App requires access to Bluetooth to connect to devices.</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>App requires access to Bluetooth to communicate with peripheral devices.</string>
<key>NSCameraUsageDescription</key>
<string>请点击“好”以允许访问。若不允许,你将无法使用拍照、录制视频、扫一扫等功能。</string>
<key>NSFaceIDUsageDescription</key>
<string>请点击“好”以允许访问。若不允许你将无法使用Touch ID 或 Face ID解锁功能。</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>请点击“好”以允许访问。若不允许,你将无法使用聊天时发送定位等功能。</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>请点击“好”以允许访问。若不允许,你将无法使用聊天时发送定位等功能。</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>请点击“好”以允许访问。若不允许,你将无法使用聊天时发送定位等功能。</string>
<key>NSMicrophoneUsageDescription</key>
<string>请点击“好”以允许访问。若不允许,你将无法使用视频通话、发送语音消息或录制视频等功能。</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>请点击“好”以允许访问。若不允许,你将无法给好友发送或者上传本地相册图片及视频内容。</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>请点击“好”以允许访问。若不允许,你将无法给好友发送或者上传本地相册图片及视频内容。</string>
<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>UIRequiresFullScreen</key>
<true/>
<key>UIStatusBarHidden</key>
<false/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>UISupportsDocumentBrowser</key>
<true/>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>io.flutter.embedded_views_preview</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

View File

@@ -0,0 +1,6 @@
import Flutter
import UIKit
class SceneDelegate: FlutterSceneDelegate {
}

View File

@@ -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.
}
}

314
lib/main.dart Normal file
View File

@@ -0,0 +1,314 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:webview_flutter_android/webview_flutter_android.dart';
import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart';
const _homeUrl = 'https://h5-im.imharry.work/';
const _stopWebMediaScript = r'''
(() => {
try {
window.__stopOpenIMVoicePlayback?.();
} catch (_) {}
document.querySelectorAll('audio, video').forEach((media) => {
try {
media.pause();
media.currentTime = 0;
} catch (_) {}
});
})();
''';
void main() {
WidgetsFlutterBinding.ensureInitialized();
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.dark,
systemNavigationBarColor: Colors.white,
systemNavigationBarIconBrightness: Brightness.dark,
),
);
runApp(const ImWebViewApp());
}
class ImWebViewApp extends StatelessWidget {
const ImWebViewApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '集中营',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF1F6FEB)),
scaffoldBackgroundColor: Colors.white,
useMaterial3: true,
),
home: const H5ShellPage(),
);
}
}
class H5ShellPage extends StatefulWidget {
const H5ShellPage({super.key});
@override
State<H5ShellPage> createState() => _H5ShellPageState();
}
class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
late final WebViewController _controller;
int _progress = 0;
String? _loadError;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_controller = _buildController()..loadRequest(Uri.parse(_homeUrl));
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.inactive ||
state == AppLifecycleState.hidden ||
state == AppLifecycleState.paused ||
state == AppLifecycleState.detached) {
unawaited(_stopWebMedia());
}
}
WebViewController _buildController() {
PlatformWebViewControllerCreationParams params =
const PlatformWebViewControllerCreationParams();
if (WebViewPlatform.instance is WebKitWebViewPlatform) {
params = WebKitWebViewControllerCreationParams(
allowsInlineMediaPlayback: true,
mediaTypesRequiringUserAction: const <PlaybackMediaTypes>{},
);
} else if (WebViewPlatform.instance is AndroidWebViewPlatform) {
params = AndroidWebViewControllerCreationParams
.fromPlatformWebViewControllerCreationParams(params);
}
final controller = WebViewController.fromPlatformCreationParams(
params,
onPermissionRequest: (request) {
unawaited(_handleWebViewPermissionRequest(request));
},
)
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setBackgroundColor(Colors.white)
..setNavigationDelegate(
NavigationDelegate(
onProgress: (progress) {
if (mounted) {
setState(() => _progress = progress);
}
},
onPageStarted: (_) {
if (mounted) {
setState(() {
_loadError = null;
_progress = 0;
});
}
},
onPageFinished: (_) {
if (mounted) {
setState(() => _progress = 100);
}
},
onWebResourceError: (error) {
if (error.isForMainFrame ?? true) {
if (mounted) {
setState(() => _loadError = error.description);
}
}
},
onUrlChange: (_) {
unawaited(_stopWebMedia());
},
onNavigationRequest: _handleNavigationRequest,
),
);
final platformController = controller.platform;
if (platformController is AndroidWebViewController) {
AndroidWebViewController.enableDebugging(false);
unawaited(platformController.setMediaPlaybackRequiresUserGesture(false));
unawaited(platformController.setGeolocationEnabled(true));
unawaited(
platformController.setGeolocationPermissionsPromptCallbacks(
onShowPrompt: (_) async {
final allowed =
await _requestPermission(Permission.locationWhenInUse);
return GeolocationPermissionsResponse(
allow: allowed,
retain: allowed,
);
},
),
);
}
return controller;
}
Future<void> _runJavaScriptSafely(String source) async {
try {
await _controller.runJavaScript(source);
} catch (_) {
// WebView can reject JavaScript while a page is still navigating.
}
}
Future<void> _stopWebMedia() {
return _runJavaScriptSafely(_stopWebMediaScript);
}
Future<NavigationDecision> _handleNavigationRequest(
NavigationRequest request,
) async {
unawaited(_stopWebMedia());
final uri = Uri.tryParse(request.url);
if (uri == null) {
return NavigationDecision.prevent;
}
const webSchemes = {'http', 'https', 'about', 'data'};
if (webSchemes.contains(uri.scheme)) {
return NavigationDecision.navigate;
}
try {
await launchUrl(uri, mode: LaunchMode.externalApplication);
} catch (_) {
// Ignore unsupported custom schemes so the WebView does not navigate to
// an error page.
}
return NavigationDecision.prevent;
}
Future<void> _handleWebViewPermissionRequest(
WebViewPermissionRequest request,
) async {
final permissions = <Permission>[];
if (request.types.contains(WebViewPermissionResourceType.camera)) {
permissions.add(Permission.camera);
}
if (request.types.contains(WebViewPermissionResourceType.microphone)) {
permissions.add(Permission.microphone);
}
final allowed = permissions.isEmpty ||
await Future.wait(permissions.map(_requestPermission))
.then((results) => results.every((allowed) => allowed));
if (allowed) {
await request.grant();
} else {
await request.deny();
}
}
Future<bool> _requestPermission(Permission permission) async {
final status = await permission.request();
return status.isGranted || status.isLimited;
}
Future<void> _handleBackNavigation() async {
await _stopWebMedia();
if (await _controller.canGoBack()) {
await _controller.goBack();
} else {
await SystemNavigator.pop();
}
}
@override
Widget build(BuildContext context) {
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, _) {
if (!didPop) {
unawaited(_handleBackNavigation());
}
},
child: Scaffold(
body: SafeArea(
child: Stack(
children: [
WebViewWidget(controller: _controller),
if (_progress < 100)
LinearProgressIndicator(
value: _progress == 0 ? null : _progress / 100,
minHeight: 2,
),
if (_loadError != null)
_ErrorPanel(
message: _loadError!,
onRetry: () => _controller.loadRequest(Uri.parse(_homeUrl)),
),
],
),
),
),
);
}
}
class _ErrorPanel extends StatelessWidget {
const _ErrorPanel({required this.message, required this.onRetry});
final String message;
final VoidCallback onRetry;
@override
Widget build(BuildContext context) {
return ColoredBox(
color: Colors.white,
child: Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.wifi_off_rounded, size: 44),
const SizedBox(height: 16),
const Text(
'页面加载失败',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
),
const SizedBox(height: 8),
Text(
message,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 20),
FilledButton(
onPressed: onRetry,
child: const Text('重新加载'),
),
],
),
),
),
);
}
}

370
pubspec.lock Normal file
View File

@@ -0,0 +1,370 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
async:
dependency: transitive
description:
name: async
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.dev"
source: hosted
version: "2.13.1"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
characters:
dependency: transitive
description:
name: characters
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.4.1"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.2"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
url: "https://pub.dev"
source: hosted
version: "6.0.0"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
lints:
dependency: transitive
description:
name: lints
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
url: "https://pub.dev"
source: hosted
version: "6.1.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev"
source: hosted
version: "0.12.19"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.13.0"
meta:
dependency: transitive
description:
name: meta
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.17.0"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
permission_handler:
dependency: "direct main"
description:
name: permission_handler
sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1
url: "https://pub.dev"
source: hosted
version: "12.0.1"
permission_handler_android:
dependency: transitive
description:
name: permission_handler_android
sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6"
url: "https://pub.dev"
source: hosted
version: "13.0.1"
permission_handler_apple:
dependency: transitive
description:
name: permission_handler_apple
sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023
url: "https://pub.dev"
source: hosted
version: "9.4.7"
permission_handler_html:
dependency: transitive
description:
name: permission_handler_html
sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24"
url: "https://pub.dev"
source: hosted
version: "0.1.3+5"
permission_handler_platform_interface:
dependency: transitive
description:
name: permission_handler_platform_interface
sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878
url: "https://pub.dev"
source: hosted
version: "4.3.0"
permission_handler_windows:
dependency: transitive
description:
name: permission_handler_windows
sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
url: "https://pub.dev"
source: hosted
version: "0.2.1"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
source_span:
dependency: transitive
description:
name: source_span
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev"
source: hosted
version: "1.10.2"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev"
source: hosted
version: "0.7.10"
url_launcher:
dependency: "direct main"
description:
name: url_launcher
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
url: "https://pub.dev"
source: hosted
version: "6.3.2"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: "3bb000251e55d4a209aa0e2e563309dc9bb2befea2295fd0cec1f51760aac572"
url: "https://pub.dev"
source: hosted
version: "6.3.29"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0"
url: "https://pub.dev"
source: hosted
version: "6.4.1"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a
url: "https://pub.dev"
source: hosted
version: "3.2.2"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18"
url: "https://pub.dev"
source: hosted
version: "3.2.5"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34"
url: "https://pub.dev"
source: hosted
version: "2.4.3"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f"
url: "https://pub.dev"
source: hosted
version: "3.1.5"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.2.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
url: "https://pub.dev"
source: hosted
version: "15.2.0"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
webview_flutter:
dependency: "direct main"
description:
name: webview_flutter
sha256: a3da219916aba44947d3a5478b1927876a09781174b5a2b67fa5be0555154bf9
url: "https://pub.dev"
source: hosted
version: "4.13.1"
webview_flutter_android:
dependency: "direct main"
description:
name: webview_flutter_android
sha256: ad5182eff9a550925330cb9f0cb038eddfdd5712aba8b77aa0f0400e50f6e688
url: "https://pub.dev"
source: hosted
version: "4.12.0"
webview_flutter_platform_interface:
dependency: transitive
description:
name: webview_flutter_platform_interface
sha256: "1221c1b12f5278791042f2ec2841743784cf25c5a644e23d6680e5d718824f04"
url: "https://pub.dev"
source: hosted
version: "2.15.1"
webview_flutter_wkwebview:
dependency: "direct main"
description:
name: webview_flutter_wkwebview
sha256: "82648217f537573e1ca9ae9952d3eacedca6ab5aee69dc84445fc763766dcea2"
url: "https://pub.dev"
source: hosted
version: "3.25.1"
sdks:
dart: ">=3.10.0 <4.0.0"
flutter: ">=3.38.0"

91
pubspec.yaml Normal file
View File

@@ -0,0 +1,91 @@
name: im_webview_app
description: "Flutter WebView shell for the OpenIM H5 app."
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 6.9.1+1
environment:
sdk: ">=3.6.0 <4.0.0"
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
permission_handler: ^12.0.1
url_launcher: ^6.3.2
webview_flutter: ^4.13.1
webview_flutter_android: ^4.10.5
webview_flutter_wkwebview: ^3.23.1
dev_dependencies:
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^6.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/to/asset-from-package
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package

94
scripts/deploy-apk.sh Executable file
View File

@@ -0,0 +1,94 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
APK_PATH="${APK_PATH:-$PROJECT_ROOT/build/app/outputs/flutter-apk/app-release.apk}"
REMOTE_USER="${REMOTE_USER:-root}"
REMOTE_HOST="${REMOTE_HOST:-54.116.29.247}"
REMOTE_PORT="${REMOTE_PORT:-22}"
REMOTE_DIR="${REMOTE_DIR:-/data/wwwroot/apk}"
REMOTE_SCRIPT="${REMOTE_SCRIPT:-/data/wwwroot/apk/show_apk_link.sh}"
SHOULD_BUILD=true
usage() {
cat <<'EOF'
Usage: scripts/deploy-apk.sh [options]
Options:
--skip-build Upload the existing APK without running flutter build.
--apk PATH APK path to upload.
--host HOST Remote host. Default: 54.116.29.247
--user USER Remote user. Default: root
--port PORT SSH port. Default: 22
--remote-dir DIR Remote APK directory. Default: /data/wwwroot/apk
--remote-script PATH Remote link script. Default: /data/wwwroot/apk/show_apk_link.sh
-h, --help Show this help.
Environment variables with the same names are also supported:
APK_PATH, REMOTE_USER, REMOTE_HOST, REMOTE_PORT, REMOTE_DIR, REMOTE_SCRIPT
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--skip-build)
SHOULD_BUILD=false
shift
;;
--apk)
APK_PATH="$2"
shift 2
;;
--host)
REMOTE_HOST="$2"
shift 2
;;
--user)
REMOTE_USER="$2"
shift 2
;;
--port)
REMOTE_PORT="$2"
shift 2
;;
--remote-dir)
REMOTE_DIR="$2"
shift 2
;;
--remote-script)
REMOTE_SCRIPT="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option: $1" >&2
usage
exit 1
;;
esac
done
if [[ "$SHOULD_BUILD" == true ]]; then
(cd "$PROJECT_ROOT" && flutter build apk --release)
fi
if [[ ! -f "$APK_PATH" ]]; then
echo "APK not found: $APK_PATH" >&2
exit 1
fi
APK_NAME="$(basename "$APK_PATH")"
REMOTE_TARGET="$REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/"
echo "Uploading $APK_PATH to $REMOTE_TARGET"
scp -P "$REMOTE_PORT" "$APK_PATH" "$REMOTE_TARGET"
echo "Generating APK link on $REMOTE_HOST"
ssh -p "$REMOTE_PORT" "$REMOTE_USER@$REMOTE_HOST" \
"bash '$REMOTE_SCRIPT' '$APK_NAME'"

9
test/widget_test.dart Normal file
View File

@@ -0,0 +1,9 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:im_webview_app/main.dart';
void main() {
test('creates the WebView shell app widget', () {
expect(const ImWebViewApp(), isA<Widget>());
});
}