From 0736e40b2f593ae79f17f81fab5a3b89bb785abd Mon Sep 17 00:00:00 2001 From: Amazefcc233 Date: Sun, 17 Dec 2023 00:14:52 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=89=20init:=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E4=B8=8B=E8=BD=BD=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .editorconfig | 12 - .gitattributes | 3 - .github/workflows/release.yml | 147 -- .gitignore | 145 -- .gitmodules | 6 - .metadata | 45 - LICENSE | 674 ------ Privacy Policy.md | 46 - README.md | 31 +- Term of use.md | 31 - analysis_options.yaml | 34 - android/.gitignore | 13 - android/app/build.gradle | 88 - android/app/proguard-rules.pro | 3 - android/app/src/debug/AndroidManifest.xml | 6 - android/app/src/main/AndroidManifest.xml | 64 - android/app/src/main/kotlin/MainActivity.kt | 6 - .../res/drawable-v21/launch_background.xml | 9 - .../main/res/drawable/launch_background.xml | 9 - android/app/src/main/res/drawable/splash.png | Bin 4440 -> 0 bytes android/app/src/main/res/mipmap/icon.png | Bin 30646 -> 0 bytes .../src/main/res/values-b+zh+CN/strings.xml | 4 - .../src/main/res/values-b+zh+TW/strings.xml | 4 - .../app/src/main/res/values-night/styles.xml | 18 - .../app/src/main/res/values-v31/styles.xml | 18 - android/app/src/main/res/values/strings.xml | 4 - android/app/src/main/res/values/styles.xml | 19 - .../main/res/xml/network_security_config.xml | 8 - android/app/src/profile/AndroidManifest.xml | 6 - android/build.gradle | 29 - android/gradle.properties | 4 - .../gradle/wrapper/gradle-wrapper.properties | 6 - android/index.html | 126 + android/settings.gradle | 11 - assets/course/art.png | Bin 3074 -> 0 bytes assets/course/biological.png | Bin 4397 -> 0 bytes assets/course/building.png | Bin 4318 -> 0 bytes assets/course/business.png | Bin 4198 -> 0 bytes assets/course/chemical.png | Bin 2838 -> 0 bytes assets/course/circuit.png | Bin 3199 -> 0 bytes assets/course/computer.png | Bin 2299 -> 0 bytes assets/course/control.png | Bin 1634 -> 0 bytes assets/course/curriculum.png | Bin 4175 -> 0 bytes assets/course/design.png | Bin 1763 -> 0 bytes assets/course/economic.png | Bin 3746 -> 0 bytes assets/course/electricity.png | Bin 2386 -> 0 bytes assets/course/engineering.png | Bin 2903 -> 0 bytes assets/course/experiment.png | Bin 3686 -> 0 bytes assets/course/generality.png | Bin 3981 -> 0 bytes assets/course/geography.png | Bin 4591 -> 0 bytes assets/course/history.png | Bin 2833 -> 0 bytes assets/course/ideological.png | Bin 2605 -> 0 bytes assets/course/internship.png | Bin 3286 -> 0 bytes assets/course/language.png | Bin 3277 -> 0 bytes assets/course/literature.png | Bin 2801 -> 0 bytes assets/course/management.png | Bin 4821 -> 0 bytes assets/course/mathematics.png | Bin 2629 -> 0 bytes assets/course/mechanical.png | Bin 2923 -> 0 bytes assets/course/music.png | Bin 3480 -> 0 bytes assets/course/physical.png | Bin 4479 -> 0 bytes assets/course/political .png | Bin 3130 -> 0 bytes assets/course/practice.png | Bin 5012 -> 0 bytes assets/course/principle.png | Bin 4452 -> 0 bytes assets/course/reading.png | Bin 3943 -> 0 bytes assets/course/running.png | Bin 5378 -> 0 bytes assets/course/social.png | Bin 4403 -> 0 bytes assets/course/sports.png | Bin 3837 -> 0 bytes assets/course/statistical.png | Bin 2465 -> 0 bytes assets/course/technology.png | Bin 2574 -> 0 bytes assets/course/training.png | Bin 4985 -> 0 bytes assets/fonts/ywb_iconfont.ttf | Bin 164796 -> 0 bytes assets/icon.svg | 1 - assets/l10n/en.yaml | 697 ------ assets/l10n/zh-Hans.yaml | 697 ------ assets/l10n/zh-Hant.yaml | 698 ------ assets/room_list.json | 1 - assets/user_agent.json | 99 - assets/webview/dark.js | 4 - assets/yellow_pages.json | 1 - distribute_options.yaml | 1 - index.html | 119 + ios-build-tools/addBuildNumber.py | 44 - ios-build-tools/toDistribution.py | 18 - ios/.gitignore | 34 - ios/ExportOptions.plist | 17 - ios/Flutter/AppFrameworkInfo.plist | 26 - ios/Flutter/Debug.xcconfig | 2 - ios/Flutter/Release.xcconfig | 2 - ios/Gemfile | 3 - ios/Podfile | 51 - ios/Podfile.lock | 293 --- ios/Runner.xcodeproj/project.pbxproj | 605 ----- .../contents.xcworkspacedata | 7 - .../xcshareddata/IDEWorkspaceChecks.plist | 8 - .../xcshareddata/WorkspaceSettings.xcsettings | 8 - .../xcshareddata/xcschemes/Runner.xcscheme | 91 - .../contents.xcworkspacedata | 10 - .../xcshareddata/IDEWorkspaceChecks.plist | 8 - .../xcshareddata/WorkspaceSettings.xcsettings | 8 - ios/Runner/AppDelegate.swift | 13 - .../AppIcon.appiconset/1024x1024.png | Bin 24170 -> 0 bytes .../AppIcon.appiconset/120x120 1.png | Bin 2496 -> 0 bytes .../AppIcon.appiconset/120x120.png | Bin 2496 -> 0 bytes .../AppIcon.appiconset/152x152.png | Bin 3219 -> 0 bytes .../AppIcon.appiconset/167x167.png | Bin 3494 -> 0 bytes .../AppIcon.appiconset/180x180.png | Bin 3783 -> 0 bytes .../AppIcon.appiconset/20x20.png | Bin 525 -> 0 bytes .../AppIcon.appiconset/29x29 1.png | Bin 701 -> 0 bytes .../AppIcon.appiconset/29x29.png | Bin 701 -> 0 bytes .../AppIcon.appiconset/40x40 1.png | Bin 874 -> 0 bytes .../AppIcon.appiconset/40x40 2.png | Bin 874 -> 0 bytes .../AppIcon.appiconset/40x40.png | Bin 874 -> 0 bytes .../AppIcon.appiconset/58x58 1.png | Bin 1231 -> 0 bytes .../AppIcon.appiconset/58x58.png | Bin 1231 -> 0 bytes .../AppIcon.appiconset/60x60.png | Bin 1310 -> 0 bytes .../AppIcon.appiconset/76x76.png | Bin 1604 -> 0 bytes .../AppIcon.appiconset/80x80 1.png | Bin 1709 -> 0 bytes .../AppIcon.appiconset/80x80.png | Bin 1709 -> 0 bytes .../AppIcon.appiconset/87x87.png | Bin 1869 -> 0 bytes .../AppIcon.appiconset/Contents.json | 122 - .../BrandingImage.imageset/256x256.png | Bin 5283 -> 0 bytes .../BrandingImage.imageset/Contents.json | 21 - ios/Runner/Assets.xcassets/Contents.json | 6 - .../LaunchBackground.imageset/Contents.json | 20 - .../LaunchImage.imageset/256x256.png | Bin 5283 -> 0 bytes .../LaunchImage.imageset/Contents.json | 21 - .../LaunchImage.imageset/README.md | 5 - ios/Runner/Base.lproj/LaunchScreen.storyboard | 52 - ios/Runner/Base.lproj/Main.storyboard | 28 - ios/Runner/Info.plist | 89 - ios/Runner/Runner-Bridging-Header.h | 1 - ios/Runner/Runner.entitlements | 24 - ios/Runner/RunnerProfile.entitlements | 8 - ios/Runner/en.lproj/InfoPlist.strings | 6 - ios/Runner/zh-Hans.lproj/InfoPlist.strings | 6 - ios/Runner/zh-Hant.lproj/InfoPlist.strings | 6 - ios/RunnerDebug.entitlements | 8 - ios/RunnerTests/RunnerTests.swift | 12 - ios/index.html | 139 ++ lib/app.dart | 115 - lib/credentials/entity/credential.dart | 32 - lib/credentials/entity/credential.g.dart | 96 - lib/credentials/entity/login_status.dart | 15 - lib/credentials/entity/login_status.g.dart | 54 - lib/credentials/entity/user_type.dart | 39 - lib/credentials/entity/user_type.g.dart | 49 - lib/credentials/error.dart | 15 - lib/credentials/i18n.dart | 26 - lib/credentials/init.dart | 24 - lib/credentials/storage/credential.dart | 73 - lib/credentials/utils.dart | 22 - lib/credentials/widgets/oa_scope.dart | 81 - lib/design/adaptive/dialog.dart | 359 --- lib/design/adaptive/editor.dart | 515 ---- lib/design/adaptive/foundation.dart | 274 --- lib/design/adaptive/multiplatform.dart | 16 - lib/design/adaptive/swipe.dart | 86 - lib/design/animation/animated.dart | 27 - lib/design/animation/button.dart | 209 -- lib/design/animation/number.dart | 23 - lib/design/animation/progress.dart | 42 - lib/design/dash_decoration.dart | 115 - lib/design/widgets/app.dart | 68 - lib/design/widgets/button.dart | 46 - lib/design/widgets/capture.dart | 50 - lib/design/widgets/card.dart | 117 - lib/design/widgets/common.dart | 75 - lib/design/widgets/duration_picker.dart | 886 ------- lib/design/widgets/entry_card.dart | 386 --- lib/design/widgets/expansion_tile.dart | 189 -- lib/design/widgets/fab.dart | 117 - lib/design/widgets/grouped.dart | 158 -- lib/design/widgets/list_tile.dart | 51 - lib/design/widgets/multi_select.dart | 307 --- lib/design/widgets/navigation.dart | 30 - lib/design/widgets/tags.dart | 27 - lib/design/widgets/view.dart | 94 - lib/entity/campus.dart | 20 - lib/entity/campus.g.dart | 43 - lib/entity/version.dart | 45 - lib/files.dart | 55 - lib/game/2048/components/animated_tile.dart | 77 - lib/game/2048/components/button.dart | 39 - lib/game/2048/components/empty_board.dart | 45 - lib/game/2048/components/score_board.dart | 52 - lib/game/2048/components/tile_board.dart | 81 - lib/game/2048/const/colors.dart | 36 - lib/game/2048/game.dart | 167 -- lib/game/2048/index.dart | 28 - lib/game/2048/managers/board.dart | 301 --- lib/game/2048/managers/next_direction.dart | 23 - lib/game/2048/managers/round.dart | 21 - lib/game/2048/models/board.dart | 46 - lib/game/2048/models/board.g.dart | 25 - lib/game/2048/models/board_adapter.dart | 20 - lib/game/2048/models/tile.dart | 60 - lib/game/2048/models/tile.g.dart | 23 - lib/index.dart | 170 -- lib/init.dart | 116 - lib/l10n/common.dart | 144 -- lib/l10n/extension.dart | 53 - lib/l10n/lang.dart | 127 - lib/l10n/time.dart | 42 - lib/l10n/tr.dart | 45 - lib/l10n/yaml_assets_loader.dart | 51 - lib/life/electricity/entity/balance.dart | 60 - lib/life/electricity/entity/balance.g.dart | 59 - lib/life/electricity/entity/room.dart | 148 -- lib/life/electricity/i18n.dart | 25 - lib/life/electricity/index.dart | 188 -- lib/life/electricity/init.dart | 12 - lib/life/electricity/service/electricity.dart | 30 - lib/life/electricity/storage/electricity.dart | 60 - lib/life/electricity/widget/card.dart | 49 - lib/life/electricity/widget/search.dart | 87 - lib/life/event.dart | 3 - lib/life/expense_records/Description.md | 67 - lib/life/expense_records/entity/local.dart | 130 - lib/life/expense_records/entity/local.g.dart | 222 -- lib/life/expense_records/entity/remote.dart | 89 - lib/life/expense_records/entity/remote.g.dart | 27 - .../expense_records/entity/statistics.dart | 9 - lib/life/expense_records/i18n.dart | 58 - lib/life/expense_records/index.dart | 112 - lib/life/expense_records/init.dart | 12 - lib/life/expense_records/page/records.dart | 136 -- lib/life/expense_records/page/statistics.dart | 298 --- lib/life/expense_records/service/fetch.dart | 71 - lib/life/expense_records/service/parser.dart | 59 - lib/life/expense_records/storage/local.dart | 80 - lib/life/expense_records/utils.dart | 119 - lib/life/expense_records/widget/balance.dart | 55 - lib/life/expense_records/widget/chart.dart | 93 - lib/life/expense_records/widget/group.dart | 45 - lib/life/expense_records/widget/selector.dart | 76 - .../expense_records/widget/transaction.dart | 72 - lib/life/i18n.dart | 12 - lib/life/index.dart | 94 - lib/life/settings.dart | 45 - lib/login/aggregated.dart | 22 - lib/login/i18n.dart | 51 - lib/login/init.dart | 9 - lib/login/page/index.dart | 252 -- lib/login/service/authserver.dart | 32 - lib/login/utils.dart | 50 - lib/login/widgets/forgot_pwd.dart | 26 - lib/main.dart | 144 -- lib/me/edu_email/entity/email.dart | 1 - lib/me/edu_email/i18n.dart | 82 - lib/me/edu_email/index.dart | 70 - lib/me/edu_email/init.dart | 12 - lib/me/edu_email/page/details.dart | 71 - lib/me/edu_email/page/inbox.dart | 101 - lib/me/edu_email/page/login.dart | 169 -- lib/me/edu_email/page/outbox.dart | 25 - lib/me/edu_email/service/email.dart | 20 - lib/me/edu_email/storage/email.dart | 8 - lib/me/edu_email/widgets/item.dart | 33 - lib/me/i18n.dart | 12 - lib/me/index.dart | 126 - lib/me/widgets/greeting.dart | 103 - lib/migration/all/cache.dart | 12 - lib/migration/foundation.dart | 61 - lib/migration/migrations.dart | 48 - lib/network/checker.dart | 266 --- lib/network/dio.dart | 46 - lib/network/i18n.dart | 46 - lib/network/page/connected.dart | 92 - lib/network/page/disconnected.dart | 79 - lib/network/page/index.dart | 74 - lib/network/proxy.dart | 105 - lib/network/service/network.dart | 127 - lib/network/service/network.g.dart | 25 - lib/network/widgets/entry.dart | 18 - lib/network/widgets/quick_button.dart | 51 - lib/network/widgets/status.dart | 26 - lib/platform/desktop.dart | 54 - lib/platform/windows/win32.dart | 26 - lib/platform/windows/win32_web_mock.dart | 1 - lib/platform/windows/windows.dart | 7 - lib/qrcode/handle.dart | 27 - lib/qrcode/i18n.dart | 11 - lib/qrcode/page/scanner.dart | 139 -- lib/qrcode/page/view.dart | 120 - lib/qrcode/protocol.dart | 103 - lib/qrcode/widgets/overlay.dart | 142 -- lib/r.dart | 43 - lib/route.dart | 476 ---- lib/school/class2nd/entity/attended.dart | 271 --- lib/school/class2nd/entity/attended.g.dart | 211 -- lib/school/class2nd/entity/details.dart | 87 - lib/school/class2nd/entity/details.g.dart | 72 - lib/school/class2nd/entity/list.dart | 150 -- lib/school/class2nd/entity/list.g.dart | 127 - lib/school/class2nd/i18n.dart | 110 - lib/school/class2nd/index.dart | 174 -- lib/school/class2nd/init.dart | 21 - lib/school/class2nd/page/activity.dart | 202 -- lib/school/class2nd/page/attended.dart | 408 ---- lib/school/class2nd/page/details.dart | 330 --- lib/school/class2nd/service/activity.dart | 143 -- lib/school/class2nd/service/application.dart | 60 - lib/school/class2nd/service/points.dart | 231 -- lib/school/class2nd/storage/activity.dart | 56 - lib/school/class2nd/storage/points.dart | 32 - lib/school/class2nd/utils.dart | 46 - lib/school/class2nd/widgets/activity.dart | 36 - lib/school/class2nd/widgets/search.dart | 99 - lib/school/class2nd/widgets/summary.dart | 166 -- lib/school/entity/icon.dart | 104 - lib/school/entity/school.dart | 149 -- lib/school/entity/school.g.dart | 149 -- lib/school/entity/timetable.dart | 132 - lib/school/event.dart | 3 - lib/school/exam_arrange/entity/exam.dart | 148 -- lib/school/exam_arrange/entity/exam.g.dart | 42 - lib/school/exam_arrange/i18n.dart | 28 - lib/school/exam_arrange/index.dart | 153 -- lib/school/exam_arrange/init.dart | 13 - lib/school/exam_arrange/page/list.dart | 119 - lib/school/exam_arrange/service/exam.dart | 39 - lib/school/exam_arrange/storage/exam.dart | 31 - lib/school/exam_arrange/widgets/exam.dart | 99 - lib/school/exam_result/entity/gpa.dart | 64 - lib/school/exam_result/entity/result.pg.dart | 159 -- .../exam_result/entity/result.pg.g.dart | 69 - lib/school/exam_result/entity/result.ug.dart | 263 -- .../exam_result/entity/result.ug.g.dart | 331 --- lib/school/exam_result/events.dart | 5 - lib/school/exam_result/i18n.dart | 43 - lib/school/exam_result/index.pg.dart | 80 - lib/school/exam_result/index.ug.dart | 94 - lib/school/exam_result/init.dart | 18 - lib/school/exam_result/page/details.ug.dart | 102 - lib/school/exam_result/page/evaluation.dart | 111 - lib/school/exam_result/page/gpa.dart | 323 --- lib/school/exam_result/page/result.pg.dart | 93 - lib/school/exam_result/page/result.ug.dart | 144 -- lib/school/exam_result/service/result.pg.dart | 78 - lib/school/exam_result/service/result.ug.dart | 120 - lib/school/exam_result/storage/result.pg.dart | 22 - lib/school/exam_result/storage/result.ug.dart | 32 - lib/school/exam_result/utils.dart | 59 - lib/school/exam_result/widgets/pg.dart | 40 - lib/school/exam_result/widgets/ug.dart | 42 - lib/school/i18n.dart | 32 - lib/school/index.dart | 107 - lib/school/init.dart | 9 - lib/school/library/aggregated.dart | 55 - lib/school/library/api.dart | 21 - lib/school/library/entity/book.dart | 94 - lib/school/library/entity/book.g.dart | 88 - lib/school/library/entity/borrow.dart | 137 -- lib/school/library/entity/borrow.g.dart | 162 -- lib/school/library/entity/collection.dart | 288 --- lib/school/library/entity/collection.g.dart | 95 - .../library/entity/collection_preview.dart | 56 - .../library/entity/collection_preview.g.dart | 27 - lib/school/library/entity/image.dart | 30 - lib/school/library/entity/image.g.dart | 65 - lib/school/library/entity/search.dart | 145 -- lib/school/library/entity/search.g.dart | 56 - lib/school/library/i18n.dart | 118 - lib/school/library/index.dart | 104 - lib/school/library/init.dart | 52 - lib/school/library/page/borrowing.dart | 134 -- lib/school/library/page/details.dart | 262 -- lib/school/library/page/details.model.dart | 67 - lib/school/library/page/history.dart | 114 - lib/school/library/page/login.dart | 163 -- lib/school/library/page/search.dart | 289 --- lib/school/library/page/search_result.dart | 254 -- lib/school/library/service/auth.dart | 71 - lib/school/library/service/book.dart | 97 - lib/school/library/service/borrow.dart | 131 - lib/school/library/service/collection.dart | 87 - .../library/service/collection_preview.dart | 35 - lib/school/library/service/details.dart | 81 - lib/school/library/service/image_search.dart | 44 - lib/school/library/service/trends.dart | 45 - lib/school/library/storage/book.dart | 26 - lib/school/library/storage/borrow.dart | 28 - lib/school/library/storage/browse.dart | 26 - lib/school/library/storage/image.dart | 20 - lib/school/library/storage/search.dart | 39 - lib/school/library/utils.dart | 16 - lib/school/library/widgets/book.dart | 68 - lib/school/library/widgets/search.dart | 46 - lib/school/oa_announce/entity/announce.dart | 192 -- lib/school/oa_announce/entity/announce.g.dart | 136 -- lib/school/oa_announce/entity/page.dart | 23 - lib/school/oa_announce/i18n.dart | 43 - lib/school/oa_announce/index.dart | 31 - lib/school/oa_announce/init.dart | 13 - lib/school/oa_announce/page/details.dart | 235 -- lib/school/oa_announce/page/list.dart | 209 -- lib/school/oa_announce/service/announce.dart | 114 - lib/school/oa_announce/storage/announce.dart | 55 - lib/school/oa_announce/widget/article.dart | 37 - lib/school/oa_announce/widget/attachment.dart | 130 - lib/school/oa_announce/widget/tile.dart | 35 - lib/school/settings.dart | 50 - lib/school/utils.dart | 80 - lib/school/widgets/campus.dart | 31 - lib/school/widgets/course.dart | 24 - lib/school/widgets/semester.dart | 111 - lib/school/yellow_pages/entity/contact.dart | 46 - lib/school/yellow_pages/entity/contact.g.dart | 59 - lib/school/yellow_pages/i18n.dart | 12 - lib/school/yellow_pages/index.dart | 90 - lib/school/yellow_pages/init.dart | 9 - lib/school/yellow_pages/page/index.dart | 43 - lib/school/yellow_pages/storage/contact.dart | 40 - lib/school/yellow_pages/widgets/contact.dart | 71 - lib/school/yellow_pages/widgets/list.dart | 73 - lib/school/yellow_pages/widgets/search.dart | 59 - lib/school/ywb/entity/application.dart | 162 -- lib/school/ywb/entity/application.g.dart | 212 -- lib/school/ywb/entity/service.dart | 117 - lib/school/ywb/entity/service.g.dart | 150 -- lib/school/ywb/i18n.dart | 39 - lib/school/ywb/index.dart | 75 - lib/school/ywb/init.dart | 18 - lib/school/ywb/page/application.dart | 166 -- lib/school/ywb/page/details.dart | 113 - lib/school/ywb/page/form.dart | 52 - lib/school/ywb/page/service.dart | 100 - lib/school/ywb/service/application.dart | 69 - lib/school/ywb/service/service.dart | 50 - lib/school/ywb/storage/application.dart | 25 - lib/school/ywb/storage/service.dart | 25 - lib/school/ywb/widgets/application.dart | 56 - lib/school/ywb/widgets/detail.dart | 46 - lib/school/ywb/widgets/service.dart | 59 - lib/session/auth.dart | 24 - lib/session/class2nd.dart | 47 - lib/session/gms.dart | 53 - lib/session/jwxt.dart | 63 - lib/session/library.dart | 42 - lib/session/sso.dart | 405 ---- lib/session/widgets/scope.dart | 62 - lib/session/ywb.dart | 110 - lib/settings/i18n.dart | 219 -- lib/settings/meta.dart | 23 - lib/settings/page/about.dart | 105 - lib/settings/page/credentials.dart | 164 -- lib/settings/page/developer.dart | 224 -- lib/settings/page/index.dart | 330 --- lib/settings/page/life.dart | 74 - lib/settings/page/proxy.dart | 485 ---- lib/settings/page/school.dart | 71 - lib/settings/page/storage.dart | 473 ---- lib/settings/page/timetable.dart | 94 - lib/settings/settings.dart | 289 --- lib/settings/settings.g.dart | 43 - lib/storage/hive/adapter.dart | 99 - lib/storage/hive/builtin.dart | 82 - lib/storage/hive/cookie.dart | 23 - lib/storage/hive/init.dart | 118 - lib/storage/hive/table.dart | 177 -- lib/storage/hive/type_id.dart | 74 - lib/storage/prefs.dart | 43 - lib/timetable/entity/background.dart | 55 - lib/timetable/entity/background.g.dart | 87 - lib/timetable/entity/course.dart | 111 - lib/timetable/entity/course.g.dart | 21 - lib/timetable/entity/display.dart | 20 - lib/timetable/entity/platte.dart | 156 -- lib/timetable/entity/platte.g.dart | 21 - lib/timetable/entity/pos.dart | 68 - lib/timetable/entity/pos.g.dart | 57 - lib/timetable/entity/timetable.dart | 512 ---- lib/timetable/entity/timetable.g.dart | 208 -- lib/timetable/events.dart | 11 - lib/timetable/i18n.dart | 293 --- lib/timetable/init.dart | 12 - lib/timetable/page/background.dart | 193 -- lib/timetable/page/cell_style.dart | 138 -- lib/timetable/page/details.dart | 103 - lib/timetable/page/editor.dart | 135 -- lib/timetable/page/export.dart | 221 -- lib/timetable/page/import.dart | 248 -- lib/timetable/page/index.dart | 55 - lib/timetable/page/mine.dart | 359 --- lib/timetable/page/p13n.dart | 553 ----- lib/timetable/page/palette.dart | 333 --- lib/timetable/page/preview.dart | 61 - lib/timetable/page/screenshot.dart | 227 -- lib/timetable/page/timetable.dart | 298 --- lib/timetable/platte.dart | 161 -- lib/timetable/service/school.dart | 105 - lib/timetable/settings.dart | 94 - lib/timetable/storage/timetable.dart | 51 - lib/timetable/utils.dart | 485 ---- lib/timetable/widgets/course.dart | 37 - lib/timetable/widgets/free.dart | 149 -- lib/timetable/widgets/style.dart | 167 -- lib/timetable/widgets/style.g.dart | 126 - lib/timetable/widgets/timetable/board.dart | 106 - lib/timetable/widgets/timetable/daily.dart | 423 ---- lib/timetable/widgets/timetable/header.dart | 134 -- lib/timetable/widgets/timetable/weekly.dart | 539 ----- lib/utils/async_event.dart | 352 --- lib/utils/byte_io.dart | 201 -- lib/utils/collection.dart | 15 - lib/utils/color.dart | 23 - lib/utils/cookies.dart | 21 - lib/utils/date.dart | 35 - lib/utils/dio.dart | 44 - lib/utils/error.dart | 12 - lib/utils/format.dart | 29 - lib/utils/guard_launch.dart | 34 - lib/utils/iconfont.dart | 773 ------ lib/utils/json.dart | 47 - lib/utils/permission.dart | 10 - lib/utils/strings.dart | 5 - lib/utils/timer.dart | 10 - lib/utils/vibration.dart | 71 - lib/widgets/captcha_box.dart | 80 - lib/widgets/html.dart | 116 - lib/widgets/image.dart | 168 -- lib/widgets/lazy_list.dart | 86 - lib/widgets/markdown_widget.dart | 31 - lib/widgets/not_found.dart | 36 - lib/widgets/page_grouper.dart | 251 -- lib/widgets/placeholder_future_builder.dart | 82 - lib/widgets/search.dart | 223 -- lib/widgets/webview/injectable.dart | 166 -- lib/widgets/webview/page.dart | 197 -- linux/.gitignore | 1 - linux/CMakeLists.txt | 116 - linux/flutter/CMakeLists.txt | 87 - linux/main.cc | 6 - linux/my_application.cc | 104 - linux/my_application.h | 18 - macos/.gitignore | 7 - macos/Flutter/Flutter-Debug.xcconfig | 2 - macos/Flutter/Flutter-Release.xcconfig | 2 - macos/Podfile | 43 - macos/Podfile.lock | 146 -- macos/Runner.xcodeproj/project.pbxproj | 683 ------ .../xcshareddata/IDEWorkspaceChecks.plist | 8 - .../xcshareddata/xcschemes/Runner.xcscheme | 87 - .../contents.xcworkspacedata | 10 - .../xcshareddata/IDEWorkspaceChecks.plist | 8 - macos/Runner/AppDelegate.swift | 9 - .../AppIcon.appiconset/1024x1024.png | Bin 30646 -> 0 bytes .../AppIcon.appiconset/128x128.png | Bin 3359 -> 0 bytes .../AppIcon.appiconset/16x16.png | Bin 465 -> 0 bytes .../AppIcon.appiconset/256x256 1.png | Bin 6952 -> 0 bytes .../AppIcon.appiconset/256x256.png | Bin 6952 -> 0 bytes .../AppIcon.appiconset/32x32 1.png | Bin 934 -> 0 bytes .../AppIcon.appiconset/32x32.png | Bin 934 -> 0 bytes .../AppIcon.appiconset/512x512 1.png | Bin 14319 -> 0 bytes .../AppIcon.appiconset/512x512.png | Bin 14319 -> 0 bytes .../AppIcon.appiconset/64x64.png | Bin 1730 -> 0 bytes .../AppIcon.appiconset/Contents.json | 68 - macos/Runner/Assets.xcassets/Contents.json | 6 - macos/Runner/Base.lproj/MainMenu.xib | 344 --- macos/Runner/Configs/AppInfo.xcconfig | 14 - macos/Runner/Configs/Debug.xcconfig | 2 - macos/Runner/Configs/Release.xcconfig | 2 - macos/Runner/Configs/Warnings.xcconfig | 13 - macos/Runner/DebugProfile.entitlements | 18 - macos/Runner/Info.plist | 47 - macos/Runner/MainFlutterWindow.swift | 15 - macos/Runner/Release.entitlements | 16 - macos/Runner/zh-Hans.lproj/MainMenu.strings | 195 -- macos/Runner/zh-Hant.lproj/MainMenu.strings | 195 -- macos/RunnerTests/RunnerTests.swift | 12 - macos/en.lproj/InfoPlist.strings | 1 - macos/zh-Hans.lproj/InfoPlist.strings | 1 - macos/zh-Hant.lproj/InfoPlist.strings | 1 - pubspec.lock | 2127 ----------------- pubspec.yaml | 176 -- run_build_runner.bat | 1 - run_build_runner.sh | 1 - specifications/ABBREVIATION.md | 31 - specifications/CONTRIBUTION_GUIDE.md | 97 - static/.DS_Store | Bin 0 -> 6148 bytes static/bootstrap-4.4.1.min.css | 7 + static/img/.DS_Store | Bin 0 -> 6148 bytes static/img/favicon.png | Bin 0 -> 10653 bytes test/exam_result.dart | 21 - tool/args.py | 654 ----- tool/build.py | 327 --- tool/cmd.py | 280 --- tool/cmds/__init__.py | 20 - tool/cmds/add_module.py | 128 - tool/cmds/alias.py | 107 - tool/cmds/bg.py | 22 - tool/cmds/cli.py | 78 - tool/cmds/help.py | 107 - tool/cmds/l10n.py | 142 -- tool/cmds/lint.py | 29 - tool/cmds/native_cmd.py | 33 - tool/cmds/run.py | 83 - tool/cmds/shared.py | 9 - tool/convert.py | 87 - tool/coroutine.py | 61 - tool/dart.py | 55 - tool/filesystem.py | 321 --- tool/fuzzy.py | 16 - tool/indexed.py | 193 -- tool/kernal.py | 39 - tool/l10n/.gitignore | 4 - tool/l10n/README.md | 13 - tool/l10n/arb.py | 93 - tool/l10n/flutter.py | 9 - tool/l10n/main.py | 95 - tool/l10n/migration.py | 791 ------ tool/l10n/pair.py | 84 - tool/l10n/rearrange.py | 102 - tool/l10n/rename.py | 36 - tool/l10n/resort.py | 98 - tool/l10n/serve.py | 74 - tool/l10n/split.py | 31 - tool/l10n/tags.py | 102 - tool/l10n/test.py | 24 - tool/l10n/ui.py | 17 - tool/l10n/util.py | 202 -- tool/l10n/weights.py | 8 - tool/loader.py | 64 - tool/log.py | 43 - tool/main.py | 241 -- tool/mimir.py | 40 - tool/multiplatform.py | 13 - tool/project.py | 335 --- tool/requirements.txt | Bin 64 -> 0 bytes tool/runner.py | 25 - tool/serialize.py | 149 -- tool/settings.py | 53 - tool/shell.py | 0 tool/strings.py | 31 - tool/style.py | 22 - tool/tag-course/COURSE.txt | 975 -------- tool/tag-course/MAPPING.txt | 0 tool/tag-course/README.md | 6 - tool/tag-course/main.py | 82 - tool/tag-course/output.txt | 37 - tool/test.py | 14 - tool/tui/.gitignore | 2 - tool/tui/LICENSE | 21 - tool/tui/README.md | 7 - tool/tui/canvas/__init__.py | 0 tool/tui/colortxt/__init__.py | 6 - tool/tui/colortxt/colors.py | 92 - tool/tui/colortxt/txt.py | 125 - tool/tui/filesystem.py | 296 --- tool/tui/input/__init__.py | 2 - tool/tui/input/nonblocking.py | 0 tool/tui/input/terminal.py | 1 - tool/tui/keys/__init__.py | 0 tool/tui/keys/keys.py | 278 --- tool/tui/multiplatform.py | 32 - tool/tui/pyproject.toml | 22 - tool/tui/render/__init__.py | 69 - tool/tui/render/canvas.py | 70 - tool/tui/render/unix.py | 117 - tool/tui/render/win.py | 144 -- tool/tui/setup.py | 40 - tool/tui/test.py | 37 - tool/tui/timer.py | 38 - tool/ui.py | 145 -- tool/utils.py | 202 -- tool/yml.py | 7 - web/favicon.png | Bin 917 -> 0 bytes web/icons/Icon-192.png | Bin 5292 -> 0 bytes web/icons/Icon-512.png | Bin 8252 -> 0 bytes web/icons/Icon-maskable-192.png | Bin 5594 -> 0 bytes web/icons/Icon-maskable-512.png | Bin 20998 -> 0 bytes web/index.html | 59 - web/manifest.json | 35 - windows/.gitignore | 17 - windows/CMakeLists.txt | 95 - windows/flutter/CMakeLists.txt | 108 - windows/runner/CMakeLists.txt | 40 - windows/runner/Runner.rc | 121 - windows/runner/flutter_window.cpp | 61 - windows/runner/flutter_window.h | 33 - windows/runner/main.cpp | 48 - windows/runner/resource.h | 16 - windows/runner/resources/app_icon.ico | Bin 10246 -> 0 bytes windows/runner/runner.exe.manifest | 20 - windows/runner/utils.cpp | 64 - windows/runner/utils.h | 19 - windows/runner/win32_window.cpp | 286 --- windows/runner/win32_window.h | 103 - 688 files changed, 394 insertions(+), 61183 deletions(-) delete mode 100644 .editorconfig delete mode 100644 .gitattributes delete mode 100644 .github/workflows/release.yml delete mode 100644 .gitignore delete mode 100644 .gitmodules delete mode 100644 .metadata delete mode 100644 LICENSE delete mode 100644 Privacy Policy.md delete mode 100644 Term of use.md delete mode 100644 analysis_options.yaml delete mode 100644 android/.gitignore delete mode 100644 android/app/build.gradle delete mode 100644 android/app/proguard-rules.pro delete mode 100644 android/app/src/debug/AndroidManifest.xml delete mode 100644 android/app/src/main/AndroidManifest.xml delete mode 100644 android/app/src/main/kotlin/MainActivity.kt delete mode 100644 android/app/src/main/res/drawable-v21/launch_background.xml delete mode 100644 android/app/src/main/res/drawable/launch_background.xml delete mode 100644 android/app/src/main/res/drawable/splash.png delete mode 100644 android/app/src/main/res/mipmap/icon.png delete mode 100644 android/app/src/main/res/values-b+zh+CN/strings.xml delete mode 100644 android/app/src/main/res/values-b+zh+TW/strings.xml delete mode 100644 android/app/src/main/res/values-night/styles.xml delete mode 100644 android/app/src/main/res/values-v31/styles.xml delete mode 100644 android/app/src/main/res/values/strings.xml delete mode 100644 android/app/src/main/res/values/styles.xml delete mode 100644 android/app/src/main/res/xml/network_security_config.xml delete mode 100644 android/app/src/profile/AndroidManifest.xml delete mode 100644 android/build.gradle delete mode 100644 android/gradle.properties delete mode 100644 android/gradle/wrapper/gradle-wrapper.properties create mode 100644 android/index.html delete mode 100644 android/settings.gradle delete mode 100644 assets/course/art.png delete mode 100644 assets/course/biological.png delete mode 100644 assets/course/building.png delete mode 100644 assets/course/business.png delete mode 100644 assets/course/chemical.png delete mode 100644 assets/course/circuit.png delete mode 100644 assets/course/computer.png delete mode 100644 assets/course/control.png delete mode 100644 assets/course/curriculum.png delete mode 100644 assets/course/design.png delete mode 100644 assets/course/economic.png delete mode 100644 assets/course/electricity.png delete mode 100644 assets/course/engineering.png delete mode 100644 assets/course/experiment.png delete mode 100644 assets/course/generality.png delete mode 100644 assets/course/geography.png delete mode 100644 assets/course/history.png delete mode 100644 assets/course/ideological.png delete mode 100644 assets/course/internship.png delete mode 100644 assets/course/language.png delete mode 100644 assets/course/literature.png delete mode 100644 assets/course/management.png delete mode 100644 assets/course/mathematics.png delete mode 100644 assets/course/mechanical.png delete mode 100644 assets/course/music.png delete mode 100644 assets/course/physical.png delete mode 100644 assets/course/political .png delete mode 100644 assets/course/practice.png delete mode 100644 assets/course/principle.png delete mode 100644 assets/course/reading.png delete mode 100644 assets/course/running.png delete mode 100644 assets/course/social.png delete mode 100644 assets/course/sports.png delete mode 100644 assets/course/statistical.png delete mode 100644 assets/course/technology.png delete mode 100644 assets/course/training.png delete mode 100644 assets/fonts/ywb_iconfont.ttf delete mode 100644 assets/icon.svg delete mode 100644 assets/l10n/en.yaml delete mode 100644 assets/l10n/zh-Hans.yaml delete mode 100644 assets/l10n/zh-Hant.yaml delete mode 100644 assets/room_list.json delete mode 100644 assets/user_agent.json delete mode 100644 assets/webview/dark.js delete mode 100644 assets/yellow_pages.json delete mode 100644 distribute_options.yaml create mode 100644 index.html delete mode 100644 ios-build-tools/addBuildNumber.py delete mode 100644 ios-build-tools/toDistribution.py delete mode 100644 ios/.gitignore delete mode 100644 ios/ExportOptions.plist delete mode 100644 ios/Flutter/AppFrameworkInfo.plist delete mode 100644 ios/Flutter/Debug.xcconfig delete mode 100644 ios/Flutter/Release.xcconfig delete mode 100644 ios/Gemfile delete mode 100644 ios/Podfile delete mode 100644 ios/Podfile.lock delete mode 100644 ios/Runner.xcodeproj/project.pbxproj delete mode 100644 ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata delete mode 100644 ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist delete mode 100644 ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings delete mode 100644 ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme delete mode 100644 ios/Runner.xcworkspace/contents.xcworkspacedata delete mode 100644 ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist delete mode 100644 ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings delete mode 100644 ios/Runner/AppDelegate.swift delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/1024x1024.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/120x120 1.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/120x120.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/152x152.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/167x167.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/180x180.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/20x20.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/29x29 1.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/29x29.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/40x40 1.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/40x40 2.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/40x40.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/58x58 1.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/58x58.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/60x60.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/76x76.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/80x80 1.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/80x80.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/87x87.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 ios/Runner/Assets.xcassets/BrandingImage.imageset/256x256.png delete mode 100644 ios/Runner/Assets.xcassets/BrandingImage.imageset/Contents.json delete mode 100644 ios/Runner/Assets.xcassets/Contents.json delete mode 100644 ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json delete mode 100644 ios/Runner/Assets.xcassets/LaunchImage.imageset/256x256.png delete mode 100644 ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json delete mode 100644 ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md delete mode 100644 ios/Runner/Base.lproj/LaunchScreen.storyboard delete mode 100644 ios/Runner/Base.lproj/Main.storyboard delete mode 100644 ios/Runner/Info.plist delete mode 100644 ios/Runner/Runner-Bridging-Header.h delete mode 100644 ios/Runner/Runner.entitlements delete mode 100644 ios/Runner/RunnerProfile.entitlements delete mode 100644 ios/Runner/en.lproj/InfoPlist.strings delete mode 100644 ios/Runner/zh-Hans.lproj/InfoPlist.strings delete mode 100644 ios/Runner/zh-Hant.lproj/InfoPlist.strings delete mode 100644 ios/RunnerDebug.entitlements delete mode 100644 ios/RunnerTests/RunnerTests.swift create mode 100644 ios/index.html delete mode 100644 lib/app.dart delete mode 100644 lib/credentials/entity/credential.dart delete mode 100644 lib/credentials/entity/credential.g.dart delete mode 100644 lib/credentials/entity/login_status.dart delete mode 100644 lib/credentials/entity/login_status.g.dart delete mode 100644 lib/credentials/entity/user_type.dart delete mode 100644 lib/credentials/entity/user_type.g.dart delete mode 100644 lib/credentials/error.dart delete mode 100644 lib/credentials/i18n.dart delete mode 100644 lib/credentials/init.dart delete mode 100644 lib/credentials/storage/credential.dart delete mode 100644 lib/credentials/utils.dart delete mode 100644 lib/credentials/widgets/oa_scope.dart delete mode 100644 lib/design/adaptive/dialog.dart delete mode 100644 lib/design/adaptive/editor.dart delete mode 100644 lib/design/adaptive/foundation.dart delete mode 100644 lib/design/adaptive/multiplatform.dart delete mode 100644 lib/design/adaptive/swipe.dart delete mode 100644 lib/design/animation/animated.dart delete mode 100644 lib/design/animation/button.dart delete mode 100644 lib/design/animation/number.dart delete mode 100644 lib/design/animation/progress.dart delete mode 100644 lib/design/dash_decoration.dart delete mode 100644 lib/design/widgets/app.dart delete mode 100644 lib/design/widgets/button.dart delete mode 100644 lib/design/widgets/capture.dart delete mode 100644 lib/design/widgets/card.dart delete mode 100644 lib/design/widgets/common.dart delete mode 100644 lib/design/widgets/duration_picker.dart delete mode 100644 lib/design/widgets/entry_card.dart delete mode 100644 lib/design/widgets/expansion_tile.dart delete mode 100644 lib/design/widgets/fab.dart delete mode 100644 lib/design/widgets/grouped.dart delete mode 100644 lib/design/widgets/list_tile.dart delete mode 100644 lib/design/widgets/multi_select.dart delete mode 100644 lib/design/widgets/navigation.dart delete mode 100644 lib/design/widgets/tags.dart delete mode 100644 lib/design/widgets/view.dart delete mode 100644 lib/entity/campus.dart delete mode 100644 lib/entity/campus.g.dart delete mode 100644 lib/entity/version.dart delete mode 100644 lib/files.dart delete mode 100644 lib/game/2048/components/animated_tile.dart delete mode 100644 lib/game/2048/components/button.dart delete mode 100644 lib/game/2048/components/empty_board.dart delete mode 100644 lib/game/2048/components/score_board.dart delete mode 100644 lib/game/2048/components/tile_board.dart delete mode 100644 lib/game/2048/const/colors.dart delete mode 100644 lib/game/2048/game.dart delete mode 100644 lib/game/2048/index.dart delete mode 100644 lib/game/2048/managers/board.dart delete mode 100644 lib/game/2048/managers/next_direction.dart delete mode 100644 lib/game/2048/managers/round.dart delete mode 100644 lib/game/2048/models/board.dart delete mode 100644 lib/game/2048/models/board.g.dart delete mode 100644 lib/game/2048/models/board_adapter.dart delete mode 100644 lib/game/2048/models/tile.dart delete mode 100644 lib/game/2048/models/tile.g.dart delete mode 100644 lib/index.dart delete mode 100644 lib/init.dart delete mode 100644 lib/l10n/common.dart delete mode 100644 lib/l10n/extension.dart delete mode 100644 lib/l10n/lang.dart delete mode 100644 lib/l10n/time.dart delete mode 100644 lib/l10n/tr.dart delete mode 100644 lib/l10n/yaml_assets_loader.dart delete mode 100644 lib/life/electricity/entity/balance.dart delete mode 100644 lib/life/electricity/entity/balance.g.dart delete mode 100644 lib/life/electricity/entity/room.dart delete mode 100644 lib/life/electricity/i18n.dart delete mode 100644 lib/life/electricity/index.dart delete mode 100644 lib/life/electricity/init.dart delete mode 100644 lib/life/electricity/service/electricity.dart delete mode 100644 lib/life/electricity/storage/electricity.dart delete mode 100644 lib/life/electricity/widget/card.dart delete mode 100644 lib/life/electricity/widget/search.dart delete mode 100644 lib/life/event.dart delete mode 100644 lib/life/expense_records/Description.md delete mode 100644 lib/life/expense_records/entity/local.dart delete mode 100644 lib/life/expense_records/entity/local.g.dart delete mode 100644 lib/life/expense_records/entity/remote.dart delete mode 100644 lib/life/expense_records/entity/remote.g.dart delete mode 100644 lib/life/expense_records/entity/statistics.dart delete mode 100644 lib/life/expense_records/i18n.dart delete mode 100644 lib/life/expense_records/index.dart delete mode 100644 lib/life/expense_records/init.dart delete mode 100644 lib/life/expense_records/page/records.dart delete mode 100644 lib/life/expense_records/page/statistics.dart delete mode 100644 lib/life/expense_records/service/fetch.dart delete mode 100644 lib/life/expense_records/service/parser.dart delete mode 100644 lib/life/expense_records/storage/local.dart delete mode 100644 lib/life/expense_records/utils.dart delete mode 100644 lib/life/expense_records/widget/balance.dart delete mode 100644 lib/life/expense_records/widget/chart.dart delete mode 100644 lib/life/expense_records/widget/group.dart delete mode 100644 lib/life/expense_records/widget/selector.dart delete mode 100644 lib/life/expense_records/widget/transaction.dart delete mode 100644 lib/life/i18n.dart delete mode 100644 lib/life/index.dart delete mode 100644 lib/life/settings.dart delete mode 100644 lib/login/aggregated.dart delete mode 100644 lib/login/i18n.dart delete mode 100644 lib/login/init.dart delete mode 100644 lib/login/page/index.dart delete mode 100644 lib/login/service/authserver.dart delete mode 100644 lib/login/utils.dart delete mode 100644 lib/login/widgets/forgot_pwd.dart delete mode 100644 lib/main.dart delete mode 100644 lib/me/edu_email/entity/email.dart delete mode 100644 lib/me/edu_email/i18n.dart delete mode 100644 lib/me/edu_email/index.dart delete mode 100644 lib/me/edu_email/init.dart delete mode 100644 lib/me/edu_email/page/details.dart delete mode 100644 lib/me/edu_email/page/inbox.dart delete mode 100644 lib/me/edu_email/page/login.dart delete mode 100644 lib/me/edu_email/page/outbox.dart delete mode 100644 lib/me/edu_email/service/email.dart delete mode 100644 lib/me/edu_email/storage/email.dart delete mode 100644 lib/me/edu_email/widgets/item.dart delete mode 100644 lib/me/i18n.dart delete mode 100644 lib/me/index.dart delete mode 100644 lib/me/widgets/greeting.dart delete mode 100644 lib/migration/all/cache.dart delete mode 100644 lib/migration/foundation.dart delete mode 100644 lib/migration/migrations.dart delete mode 100644 lib/network/checker.dart delete mode 100644 lib/network/dio.dart delete mode 100644 lib/network/i18n.dart delete mode 100644 lib/network/page/connected.dart delete mode 100644 lib/network/page/disconnected.dart delete mode 100644 lib/network/page/index.dart delete mode 100644 lib/network/proxy.dart delete mode 100644 lib/network/service/network.dart delete mode 100644 lib/network/service/network.g.dart delete mode 100644 lib/network/widgets/entry.dart delete mode 100644 lib/network/widgets/quick_button.dart delete mode 100644 lib/network/widgets/status.dart delete mode 100644 lib/platform/desktop.dart delete mode 100644 lib/platform/windows/win32.dart delete mode 100644 lib/platform/windows/win32_web_mock.dart delete mode 100644 lib/platform/windows/windows.dart delete mode 100644 lib/qrcode/handle.dart delete mode 100644 lib/qrcode/i18n.dart delete mode 100644 lib/qrcode/page/scanner.dart delete mode 100644 lib/qrcode/page/view.dart delete mode 100644 lib/qrcode/protocol.dart delete mode 100644 lib/qrcode/widgets/overlay.dart delete mode 100644 lib/r.dart delete mode 100644 lib/route.dart delete mode 100644 lib/school/class2nd/entity/attended.dart delete mode 100644 lib/school/class2nd/entity/attended.g.dart delete mode 100644 lib/school/class2nd/entity/details.dart delete mode 100644 lib/school/class2nd/entity/details.g.dart delete mode 100644 lib/school/class2nd/entity/list.dart delete mode 100644 lib/school/class2nd/entity/list.g.dart delete mode 100644 lib/school/class2nd/i18n.dart delete mode 100644 lib/school/class2nd/index.dart delete mode 100644 lib/school/class2nd/init.dart delete mode 100644 lib/school/class2nd/page/activity.dart delete mode 100644 lib/school/class2nd/page/attended.dart delete mode 100644 lib/school/class2nd/page/details.dart delete mode 100644 lib/school/class2nd/service/activity.dart delete mode 100644 lib/school/class2nd/service/application.dart delete mode 100644 lib/school/class2nd/service/points.dart delete mode 100644 lib/school/class2nd/storage/activity.dart delete mode 100644 lib/school/class2nd/storage/points.dart delete mode 100644 lib/school/class2nd/utils.dart delete mode 100644 lib/school/class2nd/widgets/activity.dart delete mode 100644 lib/school/class2nd/widgets/search.dart delete mode 100644 lib/school/class2nd/widgets/summary.dart delete mode 100644 lib/school/entity/icon.dart delete mode 100644 lib/school/entity/school.dart delete mode 100644 lib/school/entity/school.g.dart delete mode 100644 lib/school/entity/timetable.dart delete mode 100644 lib/school/event.dart delete mode 100644 lib/school/exam_arrange/entity/exam.dart delete mode 100644 lib/school/exam_arrange/entity/exam.g.dart delete mode 100644 lib/school/exam_arrange/i18n.dart delete mode 100644 lib/school/exam_arrange/index.dart delete mode 100644 lib/school/exam_arrange/init.dart delete mode 100644 lib/school/exam_arrange/page/list.dart delete mode 100644 lib/school/exam_arrange/service/exam.dart delete mode 100644 lib/school/exam_arrange/storage/exam.dart delete mode 100644 lib/school/exam_arrange/widgets/exam.dart delete mode 100644 lib/school/exam_result/entity/gpa.dart delete mode 100644 lib/school/exam_result/entity/result.pg.dart delete mode 100644 lib/school/exam_result/entity/result.pg.g.dart delete mode 100644 lib/school/exam_result/entity/result.ug.dart delete mode 100644 lib/school/exam_result/entity/result.ug.g.dart delete mode 100644 lib/school/exam_result/events.dart delete mode 100644 lib/school/exam_result/i18n.dart delete mode 100644 lib/school/exam_result/index.pg.dart delete mode 100644 lib/school/exam_result/index.ug.dart delete mode 100644 lib/school/exam_result/init.dart delete mode 100644 lib/school/exam_result/page/details.ug.dart delete mode 100644 lib/school/exam_result/page/evaluation.dart delete mode 100644 lib/school/exam_result/page/gpa.dart delete mode 100644 lib/school/exam_result/page/result.pg.dart delete mode 100644 lib/school/exam_result/page/result.ug.dart delete mode 100644 lib/school/exam_result/service/result.pg.dart delete mode 100644 lib/school/exam_result/service/result.ug.dart delete mode 100644 lib/school/exam_result/storage/result.pg.dart delete mode 100644 lib/school/exam_result/storage/result.ug.dart delete mode 100644 lib/school/exam_result/utils.dart delete mode 100644 lib/school/exam_result/widgets/pg.dart delete mode 100644 lib/school/exam_result/widgets/ug.dart delete mode 100644 lib/school/i18n.dart delete mode 100644 lib/school/index.dart delete mode 100644 lib/school/init.dart delete mode 100644 lib/school/library/aggregated.dart delete mode 100644 lib/school/library/api.dart delete mode 100644 lib/school/library/entity/book.dart delete mode 100644 lib/school/library/entity/book.g.dart delete mode 100644 lib/school/library/entity/borrow.dart delete mode 100644 lib/school/library/entity/borrow.g.dart delete mode 100644 lib/school/library/entity/collection.dart delete mode 100644 lib/school/library/entity/collection.g.dart delete mode 100644 lib/school/library/entity/collection_preview.dart delete mode 100644 lib/school/library/entity/collection_preview.g.dart delete mode 100644 lib/school/library/entity/image.dart delete mode 100644 lib/school/library/entity/image.g.dart delete mode 100644 lib/school/library/entity/search.dart delete mode 100644 lib/school/library/entity/search.g.dart delete mode 100644 lib/school/library/i18n.dart delete mode 100644 lib/school/library/index.dart delete mode 100644 lib/school/library/init.dart delete mode 100644 lib/school/library/page/borrowing.dart delete mode 100644 lib/school/library/page/details.dart delete mode 100644 lib/school/library/page/details.model.dart delete mode 100644 lib/school/library/page/history.dart delete mode 100644 lib/school/library/page/login.dart delete mode 100644 lib/school/library/page/search.dart delete mode 100644 lib/school/library/page/search_result.dart delete mode 100644 lib/school/library/service/auth.dart delete mode 100644 lib/school/library/service/book.dart delete mode 100644 lib/school/library/service/borrow.dart delete mode 100644 lib/school/library/service/collection.dart delete mode 100644 lib/school/library/service/collection_preview.dart delete mode 100644 lib/school/library/service/details.dart delete mode 100644 lib/school/library/service/image_search.dart delete mode 100644 lib/school/library/service/trends.dart delete mode 100644 lib/school/library/storage/book.dart delete mode 100644 lib/school/library/storage/borrow.dart delete mode 100644 lib/school/library/storage/browse.dart delete mode 100644 lib/school/library/storage/image.dart delete mode 100644 lib/school/library/storage/search.dart delete mode 100644 lib/school/library/utils.dart delete mode 100644 lib/school/library/widgets/book.dart delete mode 100644 lib/school/library/widgets/search.dart delete mode 100644 lib/school/oa_announce/entity/announce.dart delete mode 100644 lib/school/oa_announce/entity/announce.g.dart delete mode 100644 lib/school/oa_announce/entity/page.dart delete mode 100644 lib/school/oa_announce/i18n.dart delete mode 100644 lib/school/oa_announce/index.dart delete mode 100644 lib/school/oa_announce/init.dart delete mode 100644 lib/school/oa_announce/page/details.dart delete mode 100644 lib/school/oa_announce/page/list.dart delete mode 100644 lib/school/oa_announce/service/announce.dart delete mode 100644 lib/school/oa_announce/storage/announce.dart delete mode 100644 lib/school/oa_announce/widget/article.dart delete mode 100644 lib/school/oa_announce/widget/attachment.dart delete mode 100644 lib/school/oa_announce/widget/tile.dart delete mode 100644 lib/school/settings.dart delete mode 100644 lib/school/utils.dart delete mode 100644 lib/school/widgets/campus.dart delete mode 100644 lib/school/widgets/course.dart delete mode 100644 lib/school/widgets/semester.dart delete mode 100644 lib/school/yellow_pages/entity/contact.dart delete mode 100644 lib/school/yellow_pages/entity/contact.g.dart delete mode 100644 lib/school/yellow_pages/i18n.dart delete mode 100644 lib/school/yellow_pages/index.dart delete mode 100644 lib/school/yellow_pages/init.dart delete mode 100644 lib/school/yellow_pages/page/index.dart delete mode 100644 lib/school/yellow_pages/storage/contact.dart delete mode 100644 lib/school/yellow_pages/widgets/contact.dart delete mode 100644 lib/school/yellow_pages/widgets/list.dart delete mode 100644 lib/school/yellow_pages/widgets/search.dart delete mode 100644 lib/school/ywb/entity/application.dart delete mode 100644 lib/school/ywb/entity/application.g.dart delete mode 100644 lib/school/ywb/entity/service.dart delete mode 100644 lib/school/ywb/entity/service.g.dart delete mode 100644 lib/school/ywb/i18n.dart delete mode 100644 lib/school/ywb/index.dart delete mode 100644 lib/school/ywb/init.dart delete mode 100644 lib/school/ywb/page/application.dart delete mode 100644 lib/school/ywb/page/details.dart delete mode 100644 lib/school/ywb/page/form.dart delete mode 100644 lib/school/ywb/page/service.dart delete mode 100644 lib/school/ywb/service/application.dart delete mode 100644 lib/school/ywb/service/service.dart delete mode 100644 lib/school/ywb/storage/application.dart delete mode 100644 lib/school/ywb/storage/service.dart delete mode 100644 lib/school/ywb/widgets/application.dart delete mode 100644 lib/school/ywb/widgets/detail.dart delete mode 100644 lib/school/ywb/widgets/service.dart delete mode 100644 lib/session/auth.dart delete mode 100644 lib/session/class2nd.dart delete mode 100644 lib/session/gms.dart delete mode 100644 lib/session/jwxt.dart delete mode 100644 lib/session/library.dart delete mode 100644 lib/session/sso.dart delete mode 100644 lib/session/widgets/scope.dart delete mode 100644 lib/session/ywb.dart delete mode 100644 lib/settings/i18n.dart delete mode 100644 lib/settings/meta.dart delete mode 100644 lib/settings/page/about.dart delete mode 100644 lib/settings/page/credentials.dart delete mode 100644 lib/settings/page/developer.dart delete mode 100644 lib/settings/page/index.dart delete mode 100644 lib/settings/page/life.dart delete mode 100644 lib/settings/page/proxy.dart delete mode 100644 lib/settings/page/school.dart delete mode 100644 lib/settings/page/storage.dart delete mode 100644 lib/settings/page/timetable.dart delete mode 100644 lib/settings/settings.dart delete mode 100644 lib/settings/settings.g.dart delete mode 100644 lib/storage/hive/adapter.dart delete mode 100644 lib/storage/hive/builtin.dart delete mode 100644 lib/storage/hive/cookie.dart delete mode 100644 lib/storage/hive/init.dart delete mode 100644 lib/storage/hive/table.dart delete mode 100644 lib/storage/hive/type_id.dart delete mode 100644 lib/storage/prefs.dart delete mode 100644 lib/timetable/entity/background.dart delete mode 100644 lib/timetable/entity/background.g.dart delete mode 100644 lib/timetable/entity/course.dart delete mode 100644 lib/timetable/entity/course.g.dart delete mode 100644 lib/timetable/entity/display.dart delete mode 100644 lib/timetable/entity/platte.dart delete mode 100644 lib/timetable/entity/platte.g.dart delete mode 100644 lib/timetable/entity/pos.dart delete mode 100644 lib/timetable/entity/pos.g.dart delete mode 100644 lib/timetable/entity/timetable.dart delete mode 100644 lib/timetable/entity/timetable.g.dart delete mode 100644 lib/timetable/events.dart delete mode 100644 lib/timetable/i18n.dart delete mode 100644 lib/timetable/init.dart delete mode 100644 lib/timetable/page/background.dart delete mode 100644 lib/timetable/page/cell_style.dart delete mode 100644 lib/timetable/page/details.dart delete mode 100644 lib/timetable/page/editor.dart delete mode 100644 lib/timetable/page/export.dart delete mode 100644 lib/timetable/page/import.dart delete mode 100644 lib/timetable/page/index.dart delete mode 100644 lib/timetable/page/mine.dart delete mode 100644 lib/timetable/page/p13n.dart delete mode 100644 lib/timetable/page/palette.dart delete mode 100644 lib/timetable/page/preview.dart delete mode 100644 lib/timetable/page/screenshot.dart delete mode 100644 lib/timetable/page/timetable.dart delete mode 100644 lib/timetable/platte.dart delete mode 100644 lib/timetable/service/school.dart delete mode 100644 lib/timetable/settings.dart delete mode 100644 lib/timetable/storage/timetable.dart delete mode 100644 lib/timetable/utils.dart delete mode 100644 lib/timetable/widgets/course.dart delete mode 100644 lib/timetable/widgets/free.dart delete mode 100644 lib/timetable/widgets/style.dart delete mode 100644 lib/timetable/widgets/style.g.dart delete mode 100644 lib/timetable/widgets/timetable/board.dart delete mode 100644 lib/timetable/widgets/timetable/daily.dart delete mode 100644 lib/timetable/widgets/timetable/header.dart delete mode 100644 lib/timetable/widgets/timetable/weekly.dart delete mode 100644 lib/utils/async_event.dart delete mode 100644 lib/utils/byte_io.dart delete mode 100644 lib/utils/collection.dart delete mode 100644 lib/utils/color.dart delete mode 100644 lib/utils/cookies.dart delete mode 100644 lib/utils/date.dart delete mode 100644 lib/utils/dio.dart delete mode 100644 lib/utils/error.dart delete mode 100644 lib/utils/format.dart delete mode 100644 lib/utils/guard_launch.dart delete mode 100644 lib/utils/iconfont.dart delete mode 100644 lib/utils/json.dart delete mode 100644 lib/utils/permission.dart delete mode 100644 lib/utils/strings.dart delete mode 100644 lib/utils/timer.dart delete mode 100644 lib/utils/vibration.dart delete mode 100644 lib/widgets/captcha_box.dart delete mode 100644 lib/widgets/html.dart delete mode 100644 lib/widgets/image.dart delete mode 100644 lib/widgets/lazy_list.dart delete mode 100644 lib/widgets/markdown_widget.dart delete mode 100644 lib/widgets/not_found.dart delete mode 100644 lib/widgets/page_grouper.dart delete mode 100644 lib/widgets/placeholder_future_builder.dart delete mode 100644 lib/widgets/search.dart delete mode 100644 lib/widgets/webview/injectable.dart delete mode 100644 lib/widgets/webview/page.dart delete mode 100644 linux/.gitignore delete mode 100644 linux/CMakeLists.txt delete mode 100644 linux/flutter/CMakeLists.txt delete mode 100644 linux/main.cc delete mode 100644 linux/my_application.cc delete mode 100644 linux/my_application.h delete mode 100644 macos/.gitignore delete mode 100644 macos/Flutter/Flutter-Debug.xcconfig delete mode 100644 macos/Flutter/Flutter-Release.xcconfig delete mode 100644 macos/Podfile delete mode 100644 macos/Podfile.lock delete mode 100644 macos/Runner.xcodeproj/project.pbxproj delete mode 100644 macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist delete mode 100644 macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme delete mode 100644 macos/Runner.xcworkspace/contents.xcworkspacedata delete mode 100644 macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist delete mode 100644 macos/Runner/AppDelegate.swift delete mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/1024x1024.png delete mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/128x128.png delete mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/16x16.png delete mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/256x256 1.png delete mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/256x256.png delete mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/32x32 1.png delete mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/32x32.png delete mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/512x512 1.png delete mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/512x512.png delete mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/64x64.png delete mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 macos/Runner/Assets.xcassets/Contents.json delete mode 100644 macos/Runner/Base.lproj/MainMenu.xib delete mode 100644 macos/Runner/Configs/AppInfo.xcconfig delete mode 100644 macos/Runner/Configs/Debug.xcconfig delete mode 100644 macos/Runner/Configs/Release.xcconfig delete mode 100644 macos/Runner/Configs/Warnings.xcconfig delete mode 100644 macos/Runner/DebugProfile.entitlements delete mode 100644 macos/Runner/Info.plist delete mode 100644 macos/Runner/MainFlutterWindow.swift delete mode 100644 macos/Runner/Release.entitlements delete mode 100644 macos/Runner/zh-Hans.lproj/MainMenu.strings delete mode 100644 macos/Runner/zh-Hant.lproj/MainMenu.strings delete mode 100644 macos/RunnerTests/RunnerTests.swift delete mode 100644 macos/en.lproj/InfoPlist.strings delete mode 100644 macos/zh-Hans.lproj/InfoPlist.strings delete mode 100644 macos/zh-Hant.lproj/InfoPlist.strings delete mode 100644 pubspec.lock delete mode 100644 pubspec.yaml delete mode 100644 run_build_runner.bat delete mode 100644 run_build_runner.sh delete mode 100644 specifications/ABBREVIATION.md delete mode 100644 specifications/CONTRIBUTION_GUIDE.md create mode 100644 static/.DS_Store create mode 100644 static/bootstrap-4.4.1.min.css create mode 100644 static/img/.DS_Store create mode 100644 static/img/favicon.png delete mode 100644 test/exam_result.dart delete mode 100644 tool/args.py delete mode 100644 tool/build.py delete mode 100644 tool/cmd.py delete mode 100644 tool/cmds/__init__.py delete mode 100644 tool/cmds/add_module.py delete mode 100644 tool/cmds/alias.py delete mode 100644 tool/cmds/bg.py delete mode 100644 tool/cmds/cli.py delete mode 100644 tool/cmds/help.py delete mode 100644 tool/cmds/l10n.py delete mode 100644 tool/cmds/lint.py delete mode 100644 tool/cmds/native_cmd.py delete mode 100644 tool/cmds/run.py delete mode 100644 tool/cmds/shared.py delete mode 100644 tool/convert.py delete mode 100644 tool/coroutine.py delete mode 100644 tool/dart.py delete mode 100644 tool/filesystem.py delete mode 100644 tool/fuzzy.py delete mode 100644 tool/indexed.py delete mode 100644 tool/kernal.py delete mode 100644 tool/l10n/.gitignore delete mode 100644 tool/l10n/README.md delete mode 100644 tool/l10n/arb.py delete mode 100644 tool/l10n/flutter.py delete mode 100644 tool/l10n/main.py delete mode 100644 tool/l10n/migration.py delete mode 100644 tool/l10n/pair.py delete mode 100644 tool/l10n/rearrange.py delete mode 100644 tool/l10n/rename.py delete mode 100644 tool/l10n/resort.py delete mode 100644 tool/l10n/serve.py delete mode 100644 tool/l10n/split.py delete mode 100644 tool/l10n/tags.py delete mode 100644 tool/l10n/test.py delete mode 100644 tool/l10n/ui.py delete mode 100644 tool/l10n/util.py delete mode 100644 tool/l10n/weights.py delete mode 100644 tool/loader.py delete mode 100644 tool/log.py delete mode 100644 tool/main.py delete mode 100644 tool/mimir.py delete mode 100644 tool/multiplatform.py delete mode 100644 tool/project.py delete mode 100644 tool/requirements.txt delete mode 100644 tool/runner.py delete mode 100644 tool/serialize.py delete mode 100644 tool/settings.py delete mode 100644 tool/shell.py delete mode 100644 tool/strings.py delete mode 100644 tool/style.py delete mode 100644 tool/tag-course/COURSE.txt delete mode 100644 tool/tag-course/MAPPING.txt delete mode 100644 tool/tag-course/README.md delete mode 100644 tool/tag-course/main.py delete mode 100644 tool/tag-course/output.txt delete mode 100644 tool/test.py delete mode 100644 tool/tui/.gitignore delete mode 100644 tool/tui/LICENSE delete mode 100644 tool/tui/README.md delete mode 100644 tool/tui/canvas/__init__.py delete mode 100644 tool/tui/colortxt/__init__.py delete mode 100644 tool/tui/colortxt/colors.py delete mode 100644 tool/tui/colortxt/txt.py delete mode 100644 tool/tui/filesystem.py delete mode 100644 tool/tui/input/__init__.py delete mode 100644 tool/tui/input/nonblocking.py delete mode 100644 tool/tui/input/terminal.py delete mode 100644 tool/tui/keys/__init__.py delete mode 100644 tool/tui/keys/keys.py delete mode 100644 tool/tui/multiplatform.py delete mode 100644 tool/tui/pyproject.toml delete mode 100644 tool/tui/render/__init__.py delete mode 100644 tool/tui/render/canvas.py delete mode 100644 tool/tui/render/unix.py delete mode 100644 tool/tui/render/win.py delete mode 100644 tool/tui/setup.py delete mode 100644 tool/tui/test.py delete mode 100644 tool/tui/timer.py delete mode 100644 tool/ui.py delete mode 100644 tool/utils.py delete mode 100644 tool/yml.py delete mode 100644 web/favicon.png delete mode 100644 web/icons/Icon-192.png delete mode 100644 web/icons/Icon-512.png delete mode 100644 web/icons/Icon-maskable-192.png delete mode 100644 web/icons/Icon-maskable-512.png delete mode 100644 web/index.html delete mode 100644 web/manifest.json delete mode 100644 windows/.gitignore delete mode 100644 windows/CMakeLists.txt delete mode 100644 windows/flutter/CMakeLists.txt delete mode 100644 windows/runner/CMakeLists.txt delete mode 100644 windows/runner/Runner.rc delete mode 100644 windows/runner/flutter_window.cpp delete mode 100644 windows/runner/flutter_window.h delete mode 100644 windows/runner/main.cpp delete mode 100644 windows/runner/resource.h delete mode 100644 windows/runner/resources/app_icon.ico delete mode 100644 windows/runner/runner.exe.manifest delete mode 100644 windows/runner/utils.cpp delete mode 100644 windows/runner/utils.h delete mode 100644 windows/runner/win32_window.cpp delete mode 100644 windows/runner/win32_window.h diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 8ac1a5317..000000000 --- a/.editorconfig +++ /dev/null @@ -1,12 +0,0 @@ -root = true - -[*] -indent_style = space -indent_size = 2 -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true - -[*.svg] -insert_final_newline = false diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 371816b49..000000000 --- a/.gitattributes +++ /dev/null @@ -1,3 +0,0 @@ -text=auto - -*.json eol=lf diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index ba85d2b34..000000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,147 +0,0 @@ -name: Flutter_build - -on: workflow_dispatch -permissions: write-all - -env: - flutter_version: '3.16.3' - -jobs: - build_android: - runs-on: ubuntu-latest - - steps: - - name: Checkout the code - uses: actions/checkout@v3 - - - name: Change version info - run: | - python3 ./ios-build-tools/addBuildNumber.py - - - name: Install and set Flutter version - uses: subosito/flutter-action@v2 - with: - flutter-version: ${{ env.flutter_version }} - channel: stable - cache: true - - - name: Restore packages - run: | - flutter pub get - - - name: Build APK - run: | - flutter build apk - - - name: Sign APK - uses: r0adkll/sign-android-release@v1 - with: - releaseDirectory: build/app/outputs/flutter-apk - signingKeyBase64: ${{ secrets.APK_SIGN_JKS_BASE64 }} - alias: ${{ secrets.APK_SIGN_ALIAS }} - keyStorePassword: ${{ secrets.APK_SIGN_PASS }} - keyPassword: ${{ secrets.APK_SIGN_ALIAS_PASS }} - - - name: Rename APK file - run: | - cd build/app/outputs/flutter-apk/ - mv app-release-signed.apk SITLife-release-signed.apk - - - name: Publish Android Artifact - uses: actions/upload-artifact@v3 - with: - name: SITLife-Android-release - path: build/app/outputs/flutter-apk/SITLife-release-signed.apk - - build_ios: - runs-on: macos-latest - - steps: - - name: Checkout the code - uses: actions/checkout@v3 - - - name: Change version info - run: | - python3 ./ios-build-tools/addBuildNumber.py - - - name: Change Develop to Distribution - run: | - python3 ./ios-build-tools/toDistribution.py - - - name: Install Apple Certificate - uses: apple-actions/import-codesign-certs@v2 - with: - p12-file-base64: ${{ secrets.P12_BASE64 }} - p12-password: ${{ secrets.P12_PASSWORD }} - - - name: Install the provisioning profile - env: - PROVISIONING_CERTIFICATE_BASE64: ${{ secrets.PROVISIONING_PROFILE_BASE64 }} - run: | - PP_PATH=$RUNNER_TEMP/build_pp.mobileprovision - - echo -n "$PROVISIONING_CERTIFICATE_BASE64" | base64 --decode --output $PP_PATH - - mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles - cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles - - - name: Install and set Flutter version - uses: subosito/flutter-action@v2 - with: - flutter-version: ${{ env.flutter_version }} - channel: stable - cache: true - - - name: Restore packages - run: | - flutter pub get - - - name: Build iOS - run: | - flutter build ios --release --no-codesign - - - name: Build resolve Swift dependencies - run: | - xcodebuild -resolvePackageDependencies -workspace ios/Runner.xcworkspace -scheme Runner -configuration Release - - - name: Build xArchive - run: | - xcodebuild -workspace ios/Runner.xcworkspace -scheme Runner -configuration Release DEVELOPMENT_TEAM="M5APZD5CKA" -sdk 'iphoneos' -destination 'generic/platform=iOS' -archivePath build-output/app.xcarchive PROVISIONING_PROFILE="eb8b1f5f-3329-42a5-a18f-8254a2e85b41" clean archive CODE_SIGN_IDENTITY="Apple Distribution: ziqi wei" - - - name: Export ipa - run: | - xcodebuild -exportArchive -archivePath build-output/app.xcarchive -exportPath build-output/ios -exportOptionsPlist ios/ExportOptions.plist - - # Debug only - # - name: Publish iOS Artifact - # uses: actions/upload-artifact@v3 - # with: - # name: SITLife-iOS-release - # path: build-output/ios - - # Release - - name: Deploy to App Store (Testflight) - uses: apple-actions/upload-testflight-build@v1 - with: - app-path: ${{ github.workspace }}/build-output/ios/life.mysit.SITLife.ipa - issuer-id: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }} - api-key-id: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }} - api-private-key: ${{ secrets.APP_STORE_CONNECT_API_PRIVATE_KEY }} - - after_build: - runs-on: ubuntu-latest - needs: [build_android, build_ios] - steps: - - name: Checkout the code - uses: actions/checkout@v3 - - - name: Change version info - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - python3 ./ios-build-tools/addBuildNumber.py ${{ github.server_url }} ${{ github.repository }} ${{ github.run_id }} ${{ github.run_attempt }} - - - name: Push changes - uses: ad-m/github-push-action@master - with: - branch: ${{ github.ref }} diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 0f7b23678..000000000 --- a/.gitignore +++ /dev/null @@ -1,145 +0,0 @@ -# See https://www.dartlang.org/guides/libraries/private-files - -# Files and directories created by pub -.dart_tool/ -.packages -build/ - -# Directory created by dartdoc -# If you don't generate documentation locally you can remove this line. -doc/api/ - -# Avoid committing generated Javascript files: -*.dart.js -*.info.json # Produced by the --dump-info flag. -*.js # When generated by dart2js. Don't specify *.js if your - # project includes source files written in JavaScript. -*.js_ -*.js.deps -*.js.map - -# IDE -.idea/ -.vs/ -.vscode/ - -# Flutter -.flutter-plugins-dependencies -.flutter-plugins - -# iOS/XCode related -**/ios/**/*.mode1v3 -**/ios/**/*.mode2v3 -**/ios/**/*.moved-aside -**/ios/**/*.pbxuser -**/ios/**/*.perspectivev3 -**/ios/**/*sync/ -**/ios/**/.sconsign.dblite -**/ios/**/.tags* -**/ios/**/.vagrant/ -**/ios/**/DerivedData/ -**/ios/**/Icon? -**/ios/**/Pods/ -**/ios/**/.symlinks/ -**/ios/**/profile -**/ios/**/xcuserdata -**/ios/.generated/ -**/ios/Flutter/.last_build_id -**/ios/Flutter/App.framework -**/ios/Flutter/Flutter.framework -**/ios/Flutter/Flutter.podspec -**/ios/Flutter/Generated.xcconfig -**/ios/Flutter/ephemeral -**/ios/Flutter/app.flx -**/ios/Flutter/app.zip -**/ios/Flutter/flutter_assets/ -**/ios/Flutter/flutter_export_environment.sh -**/ios/ServiceDefinitions.json -**/ios/Runner/GeneratedPluginRegistrant.* - - -# Miscellaneous -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.buildlog/ -.history -.svn/ - -# IntelliJ related -*.iml -*.ipr -*.iws - -# Visual Studio Code related -.classpath -.project -.settings/ -.VSCodeCounter - -# Flutter repo-specific -/bin/cache/ -/bin/internal/bootstrap.bat -/bin/internal/bootstrap.sh -/bin/mingit/ -/dev/benchmarks/mega_gallery/ -/dev/bots/.recipe_deps -/dev/bots/android_tools/ -/dev/devicelab/ABresults*.json -/dev/docs/doc/ -/dev/docs/flutter.docs.zip -/dev/docs/lib/ -/dev/docs/pubspec.yaml -/dev/integration_tests/**/xcuserdata -/dev/integration_tests/**/Pods -/packages/flutter/coverage/ -version -analysis_benchmark.json - -# packages file containing multi-root paths -.packages.generated - -# Flutter/Dart/Pub related -**/doc/api/ -**/generated_plugin_registrant.dart -.pub-cache/ -.pub/ -flutter_*.png -linked_*.ds -unlinked.ds -unlinked_spec.ds - -# Android related -**/android/**/gradle-wrapper.jar -.gradle/ -**/android/captures/ -**/android/gradlew -**/android/gradlew.bat -**/android/local.properties -**/android/**/GeneratedPluginRegistrant.java -**/android/key.properties -*.jks - - -# macOS -**/macos/Flutter/GeneratedPluginRegistrant.swift -**/macos/Flutter/ephemeral - -# Coverage -coverage/ - -# Symbols -app.*.symbols - -# Exceptions to above rules. -!**/ios/**/default.mode1v3 -!**/ios/**/default.mode2v3 -!**/ios/**/default.pbxuser -!**/ios/**/default.perspectivev3 -!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages -!/dev/ci/**/Gemfile.lock -generated_plugin* -.cookies diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index f18c82db8..000000000 --- a/.gitmodules +++ /dev/null @@ -1,6 +0,0 @@ -[submodule "tool/tui"] - path = tool/tui - url = https://github.com/liplum/pytui.git -[submodule "tool/l10n"] - path = tool/l10n - url = https://github.com/liplum/L10nArbTool.git diff --git a/.metadata b/.metadata deleted file mode 100644 index a778330bd..000000000 --- a/.metadata +++ /dev/null @@ -1,45 +0,0 @@ -# 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: "d211f42860350d914a5ad8102f9ec32764dc6d06" - channel: "stable" - -project_type: app - -# Tracks metadata for the flutter migrate command -migration: - platforms: - - platform: root - create_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 - base_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 - - platform: android - create_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 - base_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 - - platform: ios - create_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 - base_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 - - platform: linux - create_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 - base_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 - - platform: macos - create_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 - base_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 - - platform: web - create_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 - base_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 - - platform: windows - create_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 - base_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 - - # 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' diff --git a/LICENSE b/LICENSE deleted file mode 100644 index f288702d2..000000000 --- a/LICENSE +++ /dev/null @@ -1,674 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. diff --git a/Privacy Policy.md b/Privacy Policy.md deleted file mode 100644 index 4de14d597..000000000 --- a/Privacy Policy.md +++ /dev/null @@ -1,46 +0,0 @@ -# 小应生活 隐私政策 -更新时间:【2023】年【10】月【10】日 -生效时间:【2023】年【10】月【10】日 - -欢迎您使用我们的产品。小应生活(包括App等产品提供的服务,以下简称“产品和服务”)是由小应生活管理团队(以下简称“我们”)开发并运营的。确保用户的数据安全和隐私保护是我们的首要任务,本隐私政策载明了您访问和使用我们的产品和服务时所收集的数据及其处理方式。 - -请您在继续使用我们的产品前务必认真仔细阅读并确认充分理解本隐私政策全部规则和要点,一旦您选择使用本软件,即视为您同意本隐私政策的全部内容,同意我们按其收集和使用您的相关信息。如您在阅读过程中,对本政策有任何疑问,请通过产品中的反馈方式与我们取得联系。如您不同意相关协议或其中的任何条款的,您应选择使用产品的离线模式,或停止使用我们的产品和服务。 - -本隐私政策帮助您了解以下内容: -一、我们如何收集和使用您的个人信息; -二、我们如何存储和保护您的个人信息; -三、我们如何共享、转让、公开披露您的个人信息; -四、我们如何使用Cookie和其他追踪技术。 - -## 一、我们如何收集和使用您的个人信息 -个人信息是指以电子或者其他方式记录的能够单独或者与其他信息,结合识别特定自然人身份或者反映特定自然人活动情况的各种信息。我们根据《中华人民共和国网络安全法》和《信息安全技术个人信息安全规范》(GB/T 35273-2017)以及其它相关法律法规的要求,并严格遵循正当、合法、必要的原则,出于您使用我们提供的服务和/或产品等过程中而收集和使用您的个人信息,包括但不限于学号、电子邮箱等。 - -若我们的产品或服务需要使用您的个人信息时,将仅会在您明示同意或您明确需要使用所对应的产品或服务后,将在所需的最小范围内使用您的个人信息。 - -## 二、我们如何存储和保护您的个人信息 -作为一般规则,我们仅在实现信息收集目的所需的时间内保留您的个人信息。出于遵守法律义务或为证明某项权利或合同满足适用的诉讼时效要求的目的,我们可能需要在上述期限到期后保留您存档的个人信息。当您的个人信息对于我们的法定义务或法定时效对应的目的或档案不再必要时,我们确保将其完全删除。当您要求抹除数据并退出账户时,我们确保存储在您本地上的数据将即时完全删除,存储在服务器的数据将在24小时内完全删除。 - -涉及到您隐私的相关个人信息数据,例如密码等,将会加密存储至本地。任何自然人或机构将无法读取。 - -关于需存储在本地上的个人信息,请见下方列表。如果您不同意存储您的个人信息至本地,您将可能无法使用部分或所有产品及服务。 -- 学号 -- 密码 -- 根据上述信息,利用相关服务,所获得或生成的相关用户数据 - -关于需存储于远程服务器上的个人信息,请见下方列表。如果您不同意存储您的个人信息至远程服务器,您将可能无法使用部分或所有产品及服务。 -- 学号 -- 电子邮箱 -- 根据上述信息所生成的用户生成数据(user-generated data) - -## 三、我们如何共享、转让、公开披露您的个人信息 -我们可能会根据法律法规规定,或按政府主管部门的强制性要求,对外共享您的个人信息。在符合法律法规的前提下,当我们收到上述披露信息的请求时,我们会要求必须出具与之相应的法律文件,如传票或调查函后,将会按照对应的需求提供相应信息。 - -除上述情形以外,我们将不会把您的个人信息以任何方式共享、转让、公开给第三方,并且也将无任何自然人或任何机构可读取您的个人信息,但不包含下述情形: -1. 用于维护所提供的产品或服务的安全稳定运行所必需的,例如发现、处置产品或服务的故障; -2. 您自行把个人信息提供给其他人的; -3. 法律法规规定的其他情形。 - -## 四、我们如何使用Cookie和其他追踪技术 -为确保产品正常运转,我们会在您的计算机或移动设备上存储名为Cookie的小数据文件。Cookie通常包含标识符、产品名称以及一些号码和字符。借助于Cookie,我们能够存储您的登录信息等数据,提升服务和产品质量及优化用户体验。 - -我们不会将Cookie用于本政策所述目的之外的任何用途。您可根据自己的偏好管理或删除Cookie。阻止或禁用Cookie功能后,可能影响您使用或不能充分使用我们的产品和服务。 diff --git a/README.md b/README.md index aa2dfa4ae..d8b3627ed 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,6 @@ -
+# SITLife download -# SIT Life +本分支在Vercel和Github Pages上提供部署服务,请输入 https://g.mysit.life 或 https://get.mysit.life 以访问站点。 -Icon +此处提供小应生活app的下载服务。 -### A multiplatform app for SIT students. - -*Android, iOS, Windows, macOS, and Linux* - -## Features - -| Timetable | School Life | Daily Life | Featured | -|-----------------|:----------------:|:-------------------:|:---------------:| -| Importing | Yellow Pages | Expense Records | Network Tool | -| Sharing | Exam Results | Electricity Balance | QR Code Sharing | -| Personalization | Exam Arrangement | Edu Email | HTTP proxy | -| Screenshot | OA Announcement | | Localization | -| | Second Class | | | -| | SIT YWB | | | - -## Contribution - -If you met any bug, feel free to [make an issue](https://github.com/liplum/mimir/issues/new). - -Welcome to contribute SIT Life, please read the [contribution guide](specifications/CONTRIBUTION_GUIDE.md). - -### License - -The source codes and configurations are open source under [GPL v3](LICENSE). -
diff --git a/Term of use.md b/Term of use.md deleted file mode 100644 index 2a528cec1..000000000 --- a/Term of use.md +++ /dev/null @@ -1,31 +0,0 @@ -# 小应生活 用户服务协议及免责条款 -更新时间:【2023】年【10】月【10】日 -生效时间:【2023】年【10】月【10】日 - -《小应生活 用户服务协议及免责条款》(以下简称“协议”)及其条款,系您下载、安装及使用“小应生活”(以下简称“本软件”)所订立的、描述您与本软件之间权利义务的协议。 - -在使用本软件前,您应认真阅读本协议的内容、充分理解各条款内容,如有异议,您可选择不使用本软件。一旦您确认本协议后,本协议即在您和本软件之间产生法律效力,意味着您完全同意并接受协议的全部条款。请您审慎阅读并选择接受或不接受协议。 - -## 1. 服务说明 -用户进入本软件,选择登入或进入离线模式后,即成为本软件的用户。 - -本软件依赖于上海应用技术大学(以下简称“官方”)的相关平台数据读取,进行二次整合或包装,以提供相关数据的展示服务。用户在使用本软件时,应在正常范围内使用本软件,不可对应用或应用内的相关服务器作出包含但不限于超批量读取数据、压测等可视为对服务器有攻击的行为。 - -用户在登入时所使用的账号全部来自官方,本软件不提供账户方面的支持。 - -## 2. 法律责任与免责 -本软件不是官方服务。与官方的相关开发团队没有关系。 - -因用户使用本软件,导致账户被官方冻结或出现其他的异常行为,由用户自行承担。 - -本软件平台对平台进行停机维护、定期检查、更新软硬件、针对突发事件、不可抗力、电脑病毒、系统故障等因素导致的正常服务中断、中止,本软件不承担责任,本软件平台将尽力避免服务中断并将中断时间限制在最短时间内。 - -用户因第三方如电信部门的通讯线路故障、技术问题、网络、电脑故障、系统不稳定性及其他各种不可抗力原因而遭受的一切损失,本软件不承担责任。 - -## 3. 知识产权 -本软件的一切知识产权,以及与软件相关的所有信息内容,包括但不限于:文字表述及其组合、图标、图饰、图像、图表、色彩、界面设计、版面框架、有关数据、附加程序、印刷材料或电子文档等均归本软件所有,受著作权法和国际著作权条约以及其他知识产权法律法规的保护。 - -未经本软件书面同意,用户不得为任何营利性或非营利性的目的自行实施、利用、转让或许可任何三方实施、利用、转让上述知识产权。出现上述未经许可之行为时,本软件保留追究相关责任人法律责任之权利。 - -## 4. 修改与解释权 -根据互联网的发展和有关法律、法规及规范性文件的变化,或者因业务发展需要,本软件有权对本协议的条款作出修改或变更,一旦本协议的内容发生变动,您可在本软件官方网站查阅最新版协议条款,该公布行为视为本软件已经通知用户修改内容,而不另行对用户进行个别通知。在本软件修改协议条款后,如果您不接受修改后的条款,请立即停止使用本软件提供的服务,您继续使用本软件提供的服务将被视为已接受了修改后的协议。 diff --git a/analysis_options.yaml b/analysis_options.yaml deleted file mode 100644 index 50889b73b..000000000 --- a/analysis_options.yaml +++ /dev/null @@ -1,34 +0,0 @@ -# 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 - -analyzer: - exclude: - - lib/**/*.g.dart - - -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-lang.github.io/linter/lints/index.html. - # - # 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 diff --git a/android/.gitignore b/android/.gitignore deleted file mode 100644 index 6f568019d..000000000 --- a/android/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -gradle-wrapper.jar -/.gradle -/captures/ -/gradlew -/gradlew.bat -/local.properties -GeneratedPluginRegistrant.java - -# Remember to never publicly share your keystore. -# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app -key.properties -**/*.keystore -**/*.jks diff --git a/android/app/build.gradle b/android/app/build.gradle deleted file mode 100644 index 64a9a8a70..000000000 --- a/android/app/build.gradle +++ /dev/null @@ -1,88 +0,0 @@ -def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') -if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> - localProperties.load(reader) - } -} - -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - -def flutterVersionCode = localProperties.getProperty('flutter.versionCode') -if (flutterVersionCode == null) { - flutterVersionCode = '1' -} - -def flutterVersionName = localProperties.getProperty('flutter.versionName') -if (flutterVersionName == null) { - flutterVersionName = '1.0' -} - -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - -def keystoreProperties = new Properties() -def keystorePropertiesFile = rootProject.file('key.properties') -if (keystorePropertiesFile.exists()) { - keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) -} - -android { - compileSdkVersion 33 - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - kotlinOptions { - jvmTarget = '1.8' - } - - sourceSets { - main.java.srcDirs += 'src/main/kotlin' - } - - defaultConfig { - applicationId "life.mysit.sit_life" - minSdkVersion 21 - targetSdkVersion 33 - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName - } - - if (keystorePropertiesFile.exists()) { - signingConfigs { - release { - keyAlias keystoreProperties['keyAlias'] - keyPassword keystoreProperties['keyPassword'] - storeFile file(keystoreProperties['storeFile']) - storePassword keystoreProperties['storePassword'] - } - } - } - - buildTypes { - release { - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - if (keystorePropertiesFile.exists()) { - signingConfig signingConfigs.release - } else { - signingConfig signingConfigs.debug - } - } - } - namespace 'life.mysit.sit_life' -} - -flutter { - source '../..' -} - -dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" -} diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro deleted file mode 100644 index e16d1f50c..000000000 --- a/android/app/proguard-rules.pro +++ /dev/null @@ -1,3 +0,0 @@ -# Flutter -# fix by https://github.com/juliansteenbakker/mobile_scanner/issues/614#issuecomment-1665473831 --keep public class androidx.camera.core.impl.CameraCaptureMetaData$** { *; } diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml deleted file mode 100644 index 17a44c787..000000000 --- a/android/app/src/debug/AndroidManifest.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index cc66b5c3c..000000000 --- a/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/android/app/src/main/kotlin/MainActivity.kt b/android/app/src/main/kotlin/MainActivity.kt deleted file mode 100644 index c1ba0ec0e..000000000 --- a/android/app/src/main/kotlin/MainActivity.kt +++ /dev/null @@ -1,6 +0,0 @@ -package life.mysit.sit_life - -import io.flutter.embedding.android.FlutterActivity - -class MainActivity: FlutterActivity() { -} diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml deleted file mode 100644 index bad28179d..000000000 --- a/android/app/src/main/res/drawable-v21/launch_background.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml deleted file mode 100644 index bad28179d..000000000 --- a/android/app/src/main/res/drawable/launch_background.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - diff --git a/android/app/src/main/res/drawable/splash.png b/android/app/src/main/res/drawable/splash.png deleted file mode 100644 index d0041a6f26254a9699e9eb46aa0314dc26d43c4b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4440 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 H!#?UiVS`hF diff --git a/android/app/src/main/res/mipmap/icon.png b/android/app/src/main/res/mipmap/icon.png deleted file mode 100644 index 67c2ee91e24b0189017a8c136dfa60857dd00f59..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30646 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 - - 小应生活 - diff --git a/android/app/src/main/res/values-b+zh+TW/strings.xml b/android/app/src/main/res/values-b+zh+TW/strings.xml deleted file mode 100644 index f2cc16863..000000000 --- a/android/app/src/main/res/values-b+zh+TW/strings.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - 小鷹生活 - diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml deleted file mode 100644 index 449a9f930..000000000 --- a/android/app/src/main/res/values-night/styles.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - diff --git a/android/app/src/main/res/values-v31/styles.xml b/android/app/src/main/res/values-v31/styles.xml deleted file mode 100644 index 67c0c69f3..000000000 --- a/android/app/src/main/res/values-v31/styles.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml deleted file mode 100644 index cf1677d44..000000000 --- a/android/app/src/main/res/values/strings.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - SIT Life - diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml deleted file mode 100644 index 919efdaa8..000000000 --- a/android/app/src/main/res/values/styles.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - diff --git a/android/app/src/main/res/xml/network_security_config.xml b/android/app/src/main/res/xml/network_security_config.xml deleted file mode 100644 index f18e1f040..000000000 --- a/android/app/src/main/res/xml/network_security_config.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml deleted file mode 100644 index f880684a6..000000000 --- a/android/app/src/profile/AndroidManifest.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - diff --git a/android/build.gradle b/android/build.gradle deleted file mode 100644 index fa68e9254..000000000 --- a/android/build.gradle +++ /dev/null @@ -1,29 +0,0 @@ -buildscript { - ext.kotlin_version = '1.9.10' - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:7.4.2' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - -allprojects { - repositories { - google() - mavenCentral() - } -} - -rootProject.buildDir = '../build' -subprojects { - project.buildDir = "${rootProject.buildDir}/${project.name}" - project.evaluationDependsOn(':app') -} - -tasks.register("clean", Delete) { - delete rootProject.buildDir -} diff --git a/android/gradle.properties b/android/gradle.properties deleted file mode 100644 index c70f915bc..000000000 --- a/android/gradle.properties +++ /dev/null @@ -1,4 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M --add-exports=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED -android.useAndroidX=true -android.enableJetifier=true -android.jetifier.ignorelist=bcprov-jdk15on diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 6b665338b..000000000 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Fri Jun 23 08:50:38 CEST 2017 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip diff --git a/android/index.html b/android/index.html new file mode 100644 index 000000000..b35241d9a --- /dev/null +++ b/android/index.html @@ -0,0 +1,126 @@ + + + + + + + 获取 小应生活 + + + + + + + + + + +
+ +

最新版本 - Android下载

+ +
+ 正在获取版本信息... +
+ + + +
+
+ + + + + + + + + + + + + + + +
文件名$fileName
版本号$version
发布时间$releaseTime
+ 官方源下载(可能被墙) + 镜像源下载 +
+
+

*此页面仅展示最新版本的应用下载渠道。若需下载以往版本,请前往Github的Release页自行下载。

+ +
+ + + + + + + + diff --git a/android/settings.gradle b/android/settings.gradle deleted file mode 100644 index 44e62bcf0..000000000 --- a/android/settings.gradle +++ /dev/null @@ -1,11 +0,0 @@ -include ':app' - -def localPropertiesFile = new File(rootProject.projectDir, "local.properties") -def properties = new Properties() - -assert localPropertiesFile.exists() -localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } - -def flutterSdkPath = properties.getProperty("flutter.sdk") -assert flutterSdkPath != null, "flutter.sdk not set in local.properties" -apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/assets/course/art.png b/assets/course/art.png deleted file mode 100644 index 5847dc2cd00bfae9fb4434c93ac8933a8ff9ffce..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3074 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 diff --git a/assets/course/chemical.png b/assets/course/chemical.png deleted file mode 100644 index db96fdca94f7fd3bba0545d9a48aa960ef307a7e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2838 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 diff --git a/assets/course/computer.png b/assets/course/computer.png deleted file mode 100644 index 62c77f2d36881b4d398ab245b7fc78a003b9ef38..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2299 zcmVh=_=Yh=_=Yh=_=Yh=_=Yh=_=Yh=_>ZYG6{0&T8(hTMt7G7U<5ucW*?+ z1_gR`Vq@iv!#3BC(#;79>TgxmjZ;-YfkpId)bD&X6}F0|=DZ?hQwwLY_{tRzkBA6_ zF6LdbZDbS`uf0z4Zp>S6FYR+vA7h_|D z{QNFozJDM6Hl01|?hZ!y^#)kbkf)FC-J3IK-MV}C5Yo`_^+kwKR*oCjy}P9)LcYG2 zF2%(B#|GHEIW!cwVM9oWEU&k;jEDg0==l3H*#N_bcj{zfg3#i{{rj7lA;jTKp6uX2 zpSrp;W;i(^#N+MSb@wg<3}9y$9SuBiAR*yL-lwHS4g84{$;sG2Ofjwq`U0kSk6O*tops8ti_?O#7!|TSz zU<@1ya65AhFl<=Ij&R3Ln9!z;o*qIoXLjvsXh@&q$G2<=pEGNgk`k;g3JP*_1p+yq z0yrFMz>Xh>0mM0g>{G{{J+ZM&&H=`ZQC7CFfC%Q{aqAXBv9VE685#7MkPs7-nTe28 zDi#+PGjJ8aX47~bGf7qfgpdR(D43cebno89i%_V+3JJM(4JNl^$L#GrJis95&OxmF zR6L%C$MNF`9Xe!VGZjzAJID9xUk;!y$ z;PXiiKu8SmXLA7hgaXswG5|3Ei2%d^US1C#0N1Y%4hEW;g@@Bw@B8?~$1|xS2$eu% zW2gkSYqx$qRP>aTpfX68bLMpK4yEO%PqVUebLH3ocwi46q&9)9ts^6GJHAfm2i4W3 zrVt)4B?TMmf2#y$W#MI_92)><%XnM&({^kCO-<@fK655D^~3ga=hD*f-_$>lYRAUH z@4J0FJDW@a42S@@fP<4jyjw^&iNu>s>(ry1CJ(fx+CluqpKF)w5>>1&lBN*$srf06RN<{i8?W zXqUIQpC5gmKY!o=OrA_R55RbmDZs1l6kUh7xRE2+ie&okHnVPO|8Ky9zM_{kIa!jh7gFM*|{`T4-IGLeWn z$pWbf(LZN^xVYQ5;RK6V4Cne~Ayu%ENT}1mQYi#PFc6e(KQz*_XCE6XH}_*>6%|nw z3Mqq0B-m)Ov9$EZSW<%DkH3%lez6!EouWX1`$zS+un_l~VqP9_$&!HsZEW5fKq{3; z;2Vw|^YXfV8#cw(tl75@DyA+j*RR9xCKO7g@??NnO|0}nb8~Op@bQU?Lg>W{K3^ap zl$9%13?6J}2c>UIOUMB2)nB8G5!sKix@XWq*w88g$f2C@&vAyip@PJ=k4q-rUGY7E;;w z(4h?)z#~+@svpCKHEgJ>3){R@e})fl-1u7@KfX3fb z%@8THwSiJ8hXb5HzinH1to-_QI7Ipj48U~tU0z*HBWBAMqYC5MyH&;F{QNt2AXtBo z+1VE^Kv3@2uX=Sh8=+o^#ae~x*Qec&k=1%=q-$$y#EdL*y(JKwIpgXIKe(*Ac=7OI zc)hy1f&v&65B#TGx2{JIXy-EZFfXAD3Lkd>8^^87hOhWhojwGj#pojVsI10MJX z4;C$2zaHua4H`gud_13@naLOf5Cg~_2@V~CQtq%}lo2LOXwn3xlp#av)ZuU-IrQ{| zgCkgsGk`LW#ab}GYSkgT>uTsk95^uQOykvtOF~2Ga%9y;-3)@VvZkc8UAjfDtV~^f z$PiaoguX`=m4O4Do#Bq6{;QUecN#Y~>Jt$`w;vWXQ0C^|bXWNTpov`t?viVmhs?7A%0x zoUzu;dmH7Hm(w!Dg5hKR9HqAELBog6}%yeG6W@iUyn#Nc*>j?%pal+o- z&ksWSfdj$8f1gK=SX@XTI$`QN8V6dMct>joIGpe9d?*Sz__;F@+? z8bw7o3Hw(##Hdb7?YQ!Cx$Wnk8m%-3mzDM1R`r&q_Vnqjzoo%hES6zVjT-$gRkP)A zI(B3{Mb#g0UbSh1R}=Eu&jHzWHfx7AYV>|2h=_=Yh=_=Yh=_=Yh=_=Yh=_=YUIUSN V64;;qMmqoi002ovPDHLkV1fl(P!RwC diff --git a/assets/course/control.png b/assets/course/control.png deleted file mode 100644 index 1394f074f551b07bb6d62bc8362f036826a3ad52..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1634 zcmV-o2A%ndP)JiB}rN{4c6+qSf`h%NlKZQHhO+cw|z>!dTA+gWoq`>MXmNZ+?l-81Rq zFo!wJVGipuf47oQKi860@^@BMhPtY(wW#4mOHMSjVNNWjFHM!e)-lM4{)Bj4UqHxbZ;1Q@t;k6|dFtwK z2*85itS$hY9`rtm(b@PwR>FL^-0x$n2mlvbekBiZm;kJdyKt`MPjd@EP`saDJt8VV zvPYJwcv+gG09$}8OYu4?fZL1DrZ03BAYU;F6s?OykLbD;43wA70`!}62&U|RBzk0Q z1cXBXCP1$WfkBH;7%YHYASGjv;R1{pzmBf2*m@R$%c4R--e8jmfEPmaP67H4T?#Q_ zH4qk126c@yaNVpZDH_fM@5ZX_=dB|^S=j`eh~v`I@zxE2F#j@ynOzp|dyL-qK(ZX( zwmHz;KG!G#W-i_brA0$w_n8Oc5}>MPO1EoQlVxQ>GT9hG0n$_2 zVdBhf-DD?_Wp<6sgl60>1_5~T{X(R$Do%#WlyrP#Jzb9QltO(V2M@C-d5S5KQb_jZ z=>Bm3G8Bz2=lOCd6gh`uBG|S7=*EmZBgaHY_Lbo>Zc~eE844;6CGR8Y7U^`Vf{9>x z0i@h0ShM?*P6D8d)ArhzJv>iJd!5YG4rmVIRD{FI5Fc5G*JnVF zunhtDhJ}aRzbA=XnF_kSu3?5oAu;dZaT^olf{9QW`MHB_LIAA&tg~ZK`SrIa%IPuF z7VOq2z>X7l>2agOr9!2VT_1Y{n-f4&DZ|W<$LKt`Q7{h@26~vd1lZZ-0dtlg(nx;i zseAOe@exw-2ry#YT9x?Ze?h**#qAZ96EzAjc;rfY+`4!XWc z+@I#d!P9JkDiBepd>^a=;_U-QOwq3TBh>w zc>%PG(@Gp?llb`rUcg9CQVGJU#WN>}#Fgg+<09+maRtCT$41k6l2S7ZU)j%M&Ei@dM)C1IeO3 g4s)2p9M+rdKO1LnWsfe1MF0Q*07*qoM6N<$f)k(tiU0rr diff --git a/assets/course/curriculum.png b/assets/course/curriculum.png deleted file mode 100644 index 66b8e7a130231d434397b796f6e1fbbc82dbc5b4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4175 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 Z006}AvqVuU{n!8i002ovPDHLkV1lRH5$gZ| diff --git a/assets/course/design.png b/assets/course/design.png deleted file mode 100644 index a6104583429e5083a32bd4dcb85b00469b6e0c48..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1763 zcmV<91|0c`P)!SpBX+No-d-v*N?50VV#4<2>7h*>-#(C zgN8p4!3ggK29Ky20!|@(X8g@Q3f%Zl!tr53U}p`lU3>2a{;A=|@R_j|opqqF4Wg~R z75rxgIpN${Lf|BwU2Ai11pm3wL*9`{fKUDu59$9c07a2t z9YAo15}*64rOt+6a@bIZ->h<;7C_;D@_?VtqX1y<2+eH5*JV->T>HS_JP3rZ_MhYs zWA8%(!174R6u#$W^5N5`v`(8R*mbf*&G5DZNL#voKLDuM%9rr7_z(^}GvcJYK=^FW zxjj$-tSL5#Lk9qr)OQeqL&>}_TrYD91iP#qHZN>2xh25)Oo?*4&Lqt${5{s6yxT zJfR~1pi&pEuIGh%t=KUTh%LDt0R(spJ`Kz3pmmNnb{Uci2hq7Ruj_3Bu+~OsxC-K% z-?5q(8h1fZbe4Rj0xvjISGN(~6I`B)aMe~h-hxT=@2JR^n+ zug|IN3BkN)05s^tSRT$EfVAZs7HuZ>evUoG-;D*c?RLuxy;cEGB5sE*+FbVBxXK;? z(4oa{1qmUk94AN?d13A{gk`s2off0Sp@|Rya0{!Tqh-cIGUGVpaN6zK11P6HdRzk; zv3v;f6Jpn=%?q*_yq20Hg!6CdZCM<3q%bofn206^XN$N)xx5bHxi5L9=yB!$eSz|_ap#HL$xyUmdI^|)R~Kxd7=AL%nQL@Y*_A5eBl6~UYb-v zWB>=kqco7v@zHh&2qrv-oG)~iN%#9LK(FI3>p+;x2R~O9#F(G5nimWL;lpgQJhm?Y zP-@ak@EQ@kVfwsK@dB0?r10UBCp8Z=4DKfYsKqV?nH*CqA$jc7b_jU=um}}|`*Gl@ zA?2e_go3Zt z8zPPefWi&cs6Z}6#_1uc|BwZQg!T_1JW>VGL0ouYW0OT_*DbN|;yw&6N-;;iuX+)F zXj+2*g{dJcf$vBFsDsaGai|RNr(DvFTr9gm5dH?pv5{mD2&(6G|RG4Iya_*U;Dhu`+Eq z2ZZA?PK@HSJAvDF2z*^xAk4vgdkX-!`d7eZ`qUPA=$0%$6OVyw-=|wXbM1H!+*;nE ztBcQ%TGx}^M}1)>kD3s>2|-^KIVQ*Fbdl5Kd9?)qVr0PKk<#y2SeJTFiKZ?Nsi;k# zVI3RmqJAYDBq6x5XT1KI_nk+9@H`=i4&>s6G#uPAw*asoiOvs#T9s+3~7y7)|2x4lGiVarU`h4mOdTak||E8Uj)!J#dI z@1H|b%rQCUpd@otmN_iV9G7Q)Fv0v}hB3w%V~jDz{tviY1WqsV>?;5O002ovPDHLk FV1h_@L}ma0 diff --git a/assets/course/economic.png b/assets/course/economic.png deleted file mode 100644 index 7fce6222be304bbf6f561ec89ee335f7f236326e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3746 zcmV;T4qfqyP)LP$+ED#C-W5SpxI6 zZ7l{F9CE$cxN-aT6)Osbl`F$%r_sOus>h0UiRp2JdZNf?&G=!4tI6)(-krx(SVf&W zbjU`+*+A7!Pv`Ru4M~!w5p6<@N0Zm{^YQr??_c2-l76pW+b#O@2oSOj5IN^ z3o)KVKD=Xw(c-nP>e#lw=4H*ZuZSi_i;Ls5VS4-Ogx1d&{6K16aQH`mIP zLs6A-a?C1oZcUsRNt zNf2r%T;q7<%9t@;UNk*?evKm0vg~Y|b)2JAmFw5}Ji{PD(4YaU%O4azPfw2!{PBlP zSI%C_Zp)Tlib^Y}nA}`2<*>lG;Qv+3&7~+H=JZ(|ddPms&$sTvIY%bEeQP4qe@&ij z-kjC23K=yD{`~fvO*mIU?&UtyZ(~UjJlY92)vBC)N z55p@&(U>uITbZ=%(Ib2N$&-J6p)rb&jGQn56nFHfQaNoJynpxZym|BodGG)*KmXRP zxpQfH>(*Jbe0^y;J^k|Kpdfs|6konPd2;q_nvRZUh;j1dgb8K&U5M}Axw-l)cibqpu^_UYxcFN35P98A8)3fY~4-0d31qJ2j-@XkB{#>E^_N`tG zgpkSl^ntWaBI(&vp`huYAZKSF%+jS&DJbak;}EZ2jf$E%lctlC$B$Qs2wR7Ao9X_v zq{RA_@jj~Sy?Z)aPq0bi3aRny)=8Y3H3HLEwgrV93Of@_?cKXyQ#>N>cvn|DJGq>u z*rn3L;IiV(55FA>KS2u zsSW{-A7?6!HY1rdQD<>*oh);HFJ*zFW6;?FtBh?-?{m6BbNTso8F6 zy{s&=Iz|;Kb2qngO8~Pz1{TEOMT=$!n=Fy_FD}*;0oJY+3V{Fz53XAWrWqCZ_f`b9KZwG{6Zt-Hk`1l12K+%YY4y{`UmI1SioSf^|fnb|9&729A z1}uO&!7E}wMU%s_y+uX5>jphYzl96SEde@b8m}S)cI^rc?c5pia7_^)Bt$F*0$jbC zlmt6EIyxu_6qucT?OJJCS9ZK~DL#JYOxW=O0dU@zFHfCq)ZSY{Q zBsSGKhp18S-r?X%`6EEZ3Ra}-`0=Qy9z8U%aEQy6IXlBn#OOXR@8(U|u{(DzTL!*5 zHg?`TP~7g_D^|dBBnn!Hd-qaOz@(o#wRtn_c(ed`9QuAF1ULe)0P*pkrGaXqV0>e_ zctdu!Wd=8u6s=lS{s^$ULNzkxn|nK@ofR z0zuFcL`FhT3Be$^79gCzKUje1Xs9egEWx}V7Y989cs_0%5Chu6Woc$@9?*5V+vAcF z>lDrt7)irfMePp)%?-swic&dendx%P1%Y6ap%4VU06#xa3}yqU*mv(BuSX(a5C}os zqD2@4mLd`ex{2T-u!M}3;CnFxmXv`gE-paKckdue`T6c4`A3s6FxqB4hf|p{+!6z85V@M1+GwS?UGPo-yt0PH}$?ukJT&OZwT{YjO9_Xb2&&w6v_s_&7}!fv2qe3tZM1tVwKgUIjAg%n;WnlU@Y*@>d5(!ownQQ|T-K$rfIwBD? zi`K3!O-CVSV1m;VC(73IIDB~UU~nmx`#8f%YiJ(n69mz!mDUlU3k~!3zyEGkiL;!H z%*Z%;bjT1*MA55OQ4}mJ24|Z$&zRxst0JiTE+SD>Sq3Bkx)QJyOf$;K$;wJDSJg}c z;;A>;G;aqpS!CD7xPZKP>lUo118r!ieZ7E@+%aUr`t|6a z;K-5CP}qrRx>2#XLrBvOBh8O}Co z5?DOEu*Gb(!Eb`Ze*LtL2)5xL7~Cs99{b{DU67!@@&WpVPdwX+pnGD*@y?VL1;p<2!JPu<8AOOrJmZj2Z zYpYN|uxnufE(S}-JxOL-nm63=AdJmxUs^cvk4w~^cI|N9|MKP0qsy{DScr*;fCYw9 z?THhmI}jC)6+NsFqRGc}5W0fl;b1w=o{fqEmw=T)JP!ticpjWDm_8lyetaE+LOc$# z5r&AbUX^Xg6|(@d%7!sSd_n^6vOypj7!<_k8&)R>2M3I;w6{%sf^!Js?mp+LBn#{0 z(7>BVnVOH!bIaEO-S<=qes=7 zB1W_x!|g%27ZmV#9F9=P=a=76vCZ7ZuI*~pw0g`*6Y9Tu7521qvfBtXDux=y_wSjp z?p?dURRK{P9pQOW5;Ml0k^-;eq>tvl7waZ?ZDjes2(+=$Hi69C+-g3j5176)XGV-D ztrzGXT0|JtrL2mJF=o=8vGK0PJ>c3vrPA73q0n6d;LM#}G?^ zk)gFlif&|N@Lm}1h3+OcYY_2*4}PlQBr+u=#A3S93(9!S8oRT`XGuCV6cO)`!s8hP z63+;t!+brxNj11!Em0Rs06x#ikPyI~j#yy{zzRzMR{o;|$jc)LNUxqh@7ovh>qn2e zba8eD?A+PK1tyAm_7n(UvS;vMu^8t3Cr)&Chv-}=^zr$}3BVFy^=fNtxmTp3AC`VxBqcWO|`NDM1rKHjUEkA{(iq^~j0d(%3YVMBXo&W#< M07*qoM6N<$f<~$Pz5oCK diff --git a/assets/course/electricity.png b/assets/course/electricity.png deleted file mode 100644 index ca1a1c601adc2a619a52169ab344959bbd71f707..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2386 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 Bd`$oV diff --git a/assets/course/experiment.png b/assets/course/experiment.png deleted file mode 100644 index fe0ad164e45f47de9be6d244ba747599ade24fee..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3686 zcmV-s4w>hh#SLzL|OR-ppbM1OkCTAP@)yq8#R@dFErL)UQvJu2v^4 z)TvY8BSVKSU7DK<0Aa&(m8WPlIBxnaMfK@}A!;`GdfqdcEH2JU)%bZ#bCoJ6=-|P^ zLdNkGt>32j_h+UW48$Yee)*FRA4IwAcH#xinqi2Vg=GVi`Ld%udcYnXGESozHf+U; zf&#|TRoem2J?gFTSF6d{f?c6$XV1cGjt5_Ui6Q>mblHJnfdRE@k%0nQY5nk_hxw?n zdyngMjAM|mOG+e? zqer#c@(woM4jHm|aZU~Zgp%tW@=C3S%SPGj=FKt0e~a$j1p=9@c5Nbtw@e`{DjGa^ z?%aF#jBvD(w*Pm|s#OEjYVwg;X{gK2ofzW31=*K>T;Z@}NuPOkKJz!1lrWC_yX@%E zd-lA23jpcJ_x@>>V9AW+&0^O`8&5ajj)Mt#-tS4I7?3VI24OTlXJ= zIvl94*OOlgCWk≥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 Eg5eB2Bme*a diff --git a/assets/course/generality.png b/assets/course/generality.png deleted file mode 100644 index b5eb8f6af301cf972b136b4dced926e2f03020cb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3981 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 diff --git a/assets/course/geography.png b/assets/course/geography.png deleted file mode 100644 index 998a250d12b72bdabe8d3d4b740f416bdf896f55..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4591 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 jKp+qZ1OkCTG%gxs) diff --git a/assets/course/ideological.png b/assets/course/ideological.png deleted file mode 100644 index eb37b400864398c11bf20983f36ffba0a3f3fb61..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2605 zcmV+|3exq7P)Nrz^7~HEWnzIqBuT#McLim0>gGFc4LCV$i27G-3;4_iQNh+ z-C%bh1`T$1C!JT%H=na`vzX{TU;LN%@y_|(I_I)HdZzG2vt|SQmE6fAM~?I>We+0f zJbd_3J(UKM`yM~$a=P@Ix8$$kay8>?E#&VSI#ibS3ahn~?4+Zkb&W=| z-lMEs;Q;hiV+o1oC4;>3wgP6$yN6r`i$=!g)H=kNdE0YU_E{5V4L zE04EuVNw!8`}U0*<={ZITX}TH4sC6FdxUx$GaUq8xw+soA5^vKmwwdHU|bgP=FAy- zr^QV>)xtvkR{iUEjLpxNrQ@1(% zEG^^X5n8@{^5n&f5z5OG2)^a8w{Hmo9Omn*tGi?gLi+lKh5!TxpppXc>=~aA4#>>p z@w~i#+<|A#=<9oUAe5U+l9eX#7yzso0IV1QtQY{rC0(Jn6VdTi{hS+Hw}kDou^(-Wb%I4>{I$VhNoHD_7#;=FFi|qVnt8x9RB%79bQJ?%)9Jo}o*oh5>I%=x%5L3Sw5a?5-?(w;(7bsF?cTj|rMI_iAkxwT182{MOyta&&dz@g z02qK5FC+=1&`<{lPS`d&sxXef**iHU}WE-nfO z00R&lY+wLAvb6NZjqmk%O0%-!;^xjps9!%NC2+K%VZ(+5fzZ;Wd_J@zFF?`Jo}RFV z$j*k8bcF+e0l0ftGUO*DY}goBgYE33*WI^IECxWJYFVKG zU;ymxL8(ActA1Z!Q&Z@V(Xm-GJ|7`n-L`ElEn%B^ z_UxxqL~`=3U9)Dvl*Hq~bt?=241k@T6o4;5fF3Uy!iNr3RRxD0Ki<7NfFN(+?d_X3 zg}_gm1c5JqZE)^fKmbe)4<5jpq{0Be07OTVBsg5wV$I!s;zUPB$yC7S%K`wv>fG7H z1R-thI(39XgkHW(O>Q7yuZ6xHw6wwy^NZmG5o;JUw-F;Xj4M z;ubB;%wz*_>Qs*&uomdkhs_oUs6}S^?MFshSt(!(<2AQd1gF(l3O85-8FPY}|cEfyOa!}mf%VF-ssW0NKp76?tA+@Jvj zVDMmNWzd5M*RTIN&j8nj8UZ!*D+~Y(0ChNY#}0yUaiIbvU9@pHOeO?eB$_hC(GeDF z2?<{>=BTLz+7XGM-62Ej)e{O~`uO}f z01qFYIyHYj)Eb0B0P5mJBO}mCRu0AoLT=oU1THFS#tg}U5J=sEy1GgZgm`;{Mn}V( z4xfWW66^-_^+Ah^|KQ(;0lAAT4znYMstLyA68vsr#_8rSx+qO-eI&R#?jZoFJum}&AtvsHA!Ooosaad|&ZQ^d; zR2TrWXLs*zV}sDxv32S|kw#04!GIgq)@HLSvvRo~RK#vbrk0fr@?tk6lHcn=Rx6TI zT1x4_d{$R-Ye@+xvGKk3Vycr>i;F4k@6Q}Tq!t!xsJ{n*U!%sCe$-H}6-n^T&3O}h z9TnN<=gT|7ERUS2x^i!6sb)LnK9)`a0Tm8Fe|J^@X>kv9 z&Zh@5uyUpv-f(j(&ySr$X*7#aHd`ZuZEJBXKmXb_XXoY1%MW~RuB|Q9LbzUaoUB(A z6{(M8v=z4P)2G4#=vR|DkX&@QDrx)n71}d|D@T@;ln226 z{dRV1*CNDOpgP)S@%8I8MOpu0%=)C@{CPjW)~!uVaiD)0fOhb!;#yi`jOMFUhThz@sim3`0@X)ef=0o#J%)%iMOg$ zk-vk{#RR{3bIoHabFDV++NG&Rj}Wx(-~ZR5g$)}vY}l}2!-fqTHf-+zVd8`EU3cXk P00000NkvXXu0mjf(Z<}V diff --git a/assets/course/internship.png b/assets/course/internship.png deleted file mode 100644 index a00dcf728834085ec76a0e2e45dd16668a24654c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3286 zcmV;{3@P)8P)JBEeWEp5?p^&Wg@9_9$o zz&Q73e8$_`Irsm+eZPw#5C{YUfj}S-2*m$^!sy8)=MQRXN8t?1%P6DjQQkzef^9i-;%3YMMcs9`qxfOKY3ElK%@JC93LNQ6c=Rd%FCM^ z8S4Ddpte?1Gcob&R{#v^!@j;NEGtvgq+9watX+#C{%+Lv&e^!{RdlqOS$cYXJpe3L zW+wQKnOSBg{JWA;baZ|`0D}e{Idb8=VnoD~Cjb~2q^4F?5ufA7Mdp@f ziN~Kklj}3gd&Bs_gE7S4g`)aQYmb5j3l`tBdXf@aQK6@|b7x*2A)sJfvLrF_%NGFD z)nj9eiU2S*g$5CP&dBKW>C#g8ysd3&DgbI;YF_*D!^1Je-^Ki~+KW!eoH;`Xw{qp7 zL$6)|uxOFFc|rmJhK46kmXruL1iz(7ORK8`KvUDn>B0K&oVg@!#I(WlFPeqnQ(c?%a?+}W@JF&w6r`tZry@{IyzpuLcr5ycQM4DMg7j4SqD6llG@t< z%o3x}v$uFwB&m)G{f6JrP!6VxXAIG{P$=T!=H}GzkZ0kR)z#l48OH%&Fnag4u>n9?nZfY) z2Vm~prAx!Z;i%r)Iwb`Fef?d#9zO0@Z<+qVELTh=iBn?GMq zFCqc}RaHgBojU=Lkr_VR!2y6D-^-7CNgwf9&dwO3ZJ}-;6|jo4viJhDE;1NBds1oa@|{1MUxSef@2S%Ex`sdH#Sb-iY zF^Vl+S0~&E7{+3Wi(?4UKzT-{;l>9K3iRciVUe8e`T2xE?G@FQ4B~Qn)zkyh2Df!E3U3W4h_>1M5!UBcS3iCXg=uR8eEyu1 z&P#mFo(cuWM*CQvO%C-9Pj0$~e3rKPuTw`EtLp)qERg@tef!0ljM zMo|$%v<;MW(i=SE^=n}Q7#T@R+t?5*cA^u0-@bM0_y<9RN7^{CO>8EEfg#!ksyT^^ zziob7R$F`ebSo{%V1)^9pd4wG%!{a38O5N&f7u(g7O zg&7(`0~8j<#>&dV79l%ZPY-~~$~$-9Ie@Av9uFP`D=WKp&BO#wCleD@Q~==dcI;4A z{*CSCouni9Bo7Y^AzCO2Bt<-z!)dJ`+1X}hrlwF}4-Z2_07^>2!sg6@LPta>DK$hW zbZKcw$btoMp(h|fR~Jqz7cGK@Xte=0l1H*Wrlw*D(Lkk0-e8&a?F-k~+05Kr;rso4 z`oO`Tl+>U>>(&tw3dv@#Spz_At%ZeygYZ~WV{Q)P`}cF_!uP#=sjhBp%zqDq(XXGK zT?@K^Jq=wHtrz(ed$gP!hWI^ZJ?nZ5@40XxO;OUDv!kqx#+Dq&xnEx|tl*a{VX>m4 z0XT9bBm`#lzIx^8xNRF>;Oc5N8_wTLOSQD%;4nXb!2&iL3O;XMgW&S=Mn=#uix<7Zlbbr))vFgg#vK>8awS{|ynfxw>*!Gc9zEK;dG~I@08m&pHB(a< zH8h~W`S~_B+S)Lhnd$2TP*C9G!(;-Gm9=~K^5yW5XmBtToW*i+frir7_V(uST5f>( z^R=~4oB%*}&S;&@FUrg5jP6EO3!|ek4AP+kQo0c|gDyOF;?$|MBa$YZWtEjgja#;C z!-lLZ;RP)tBOw86vYg!b@mwwd=H~E97+AAyZJ{Ax&9}FQV|;aW_}u;b;o)j(Q1Iwz z2177O)>U?YMa}f(`YKV*-A~UDC0w)qWjvwE%2Y!;M`JX@Q>Dk)C zrr*`|*f9WneeLb{?iCb1K7RXl*fLza7!mR88Q%bbfzHm*0GgUEF4wQO+yK(jLxybF zKx7{AGn1$}k~W--iVCS?5~EC~4j6!hCpLNZiw}oKtc;A#o%{J>hBjjf?&i_a@$%yFge7MZ z5gLZc3=Re$Dk>lVHrY6CZ=aC?-?Mcq9L!l+IXlD1Vy#(|lm!3o5Tza;tX;KobkqZ}^n#RNcK%>Ee=L^APG8jHSQ1IQmMUgIU zViFSryQIa76%>Mk1cg^trqliWU{>+r!|&cT|GpI~_UwVz(a~ai^so#sDZv^*23W8_ zPcJgE^#Z|k?l*7vqwe|h_4WDh5na^P4G4Jf004RU2@|+n;ubOD5-Q?35g7m(;6IGW z0D>R`AOj!+AOj!+AOj!+AOpzC+S$PcpQ%%$qMkkbeqXfy|hloaCn z_Ww6dm=G3r|2{wlARsbn=p6mHW6b*cDO2|CgDZajYot&-J>g~5bi*FXmZgP-SOe&+ zAKj*xna#snw-}v!U$srn%lmJmTkmq)J-NBG!4k_k4rnt7fj}S-2m}IwK(sgC0MIo% Ut%XgcsQ>@~07*qoM6N<$g0MU^3;+NC diff --git a/assets/course/language.png b/assets/course/language.png deleted file mode 100644 index c9d181602f22f7df340a60a72df481e224a5cc78..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3277 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 LNkvXXu0mjf{+uEk diff --git a/assets/course/literature.png b/assets/course/literature.png deleted file mode 100644 index d2383e1b983aef9278ab46bfd94ca372bf35a94f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2801 zcmV&iEaisa~* zU!-r<&g^)4&6=jK8lLFdL!~y)Z2YSGMLW9|Bu71n!Dwn~*w{ID&z_2J@}H9Jn=>b2 zmG7G;gPwoh>VN8$S8ls)-MTyOSopQIhttTsdE1xGb_@`?yE9|9*d^?XjrFgiqvREF zaqRWm9!Y#vAYeVcHIoybm`HzlWm?qRp`knK=PCnjS*%r#U#xTgaOci1%OCgKx$wRB ztXr(Cw&mP#gVl6X*fu&k(R}BfmDlFQOILsQUF*Awk9wcAv+G&ixpU~7Lx%>|^j)uf zy0%uUP*>~P^m;Tw8I2%>`cry79Qj!$!@wjx=l9N;sJrg68l$;$Uqf{C`s)syb+clt zlah|H5|sJ7+S|Lj+S&#N(EpZJtI@!>sask8MT@(^kn(EG%A5l&E#c3&gdC$46u5Tm zypPKl2sY_|j`QtwaryD>=VSJDeEjiSZ(3QcUUkhiR+ggpco&x=NA9?zx~q7TV&$*D zcHPvH)<56ddmyBzd}MiHp|)OArEk~i&?S~~_s|t8u}oG}m@28yeDA$Ylpk;QiCwei zbcyiG;^ekAbcr;2UI480SIP5j`}KNmOVE}y)Y#~A&;HHAt`jG=FPl3DBG69{ytIN^ z_5S zyha{AIvU8ee>TOvzkk+|*FIx#va;TK>w4ze2Ll2wH~??Xy48BayWQQ(mTlO;;UJ4u zU0r#3b2CC!RYgS#h57c}Tplkc2chKTojb$A5E6;jtnu=K>$bK|P6(YnYhy#FBUD;y zW3zWJLXC|S%I@6=6&Ej9ux}qir%&71Fc|Q73dPwO?Aoecx8oB)F zQMf)hD3?z=07wKd8Vv?`f1R$aEjJe-wOS(KaDH#4(xfCrU6T#~0RXSV`Fsim z9E2U#-MwN3RQtH<$EyI52v!APmn>nkp?IWF;Cfoxf(0}hLKPLZw($J^{*^2D>@mL{ zhqGdZmlsI^m+dl8CWG6-vF`4N4{+PRIh@}|j>JSN6;i`w z128(O(e(C0Xl`viejJBpZ1L?lm*d;V=XS{MFl(5g2ms;X3`RfzET=@G_uexfC0hR z_4>iVnwlvLz=;zq)`A6a#M|3%*#eQk+1b=XcXKlpgAB&TjR-Y2^LWh$>n_h_b<=qBf+u${xBpudNe8uLS=sb>C;f%x3OPKuv9t?0U$eq01BvG`ui&?%tf9`C6n=ZzuyPY+gnnC zBM;s%G7SJATf!j4fa8@)_?#FNeEpIEKmZsW)$8$k9vsuxS5{`)u~SrJa!^YPew@V? zfdl}&MGNO@SdS+sn~Om>BCD$Ki5=MSIAr6Hj*CDH%H<9K0RT5-#_j`{uyC2%W$08#_s@h^@rV~0!tOk*PkaM}Psia;p+R4P-$Ng}~12d5yMdZq{f1OQS45C8}O z1ONg60e}F|-rn6kG6HAgN~N$dLMW`P?CeA+G__7o5l7?;jWlJA*PZ&YY>PMyR8s zxf#Bv(FmG?0O0w2O-*YnEO$7Z@^aW8zi(e!8g$Tp{PEBs_#s|;DLNV&B=_GR6H`$E zHa51ha`Nmeuf)Z{>+afh_AIQ!OG{f?;PZ?HAR&RrgHGP!#ryZe28`EVr_;gV_un5G z37>DGpW}fCqNCw+JoJ#47o0Bs>Z_*L|Kf|?yW#zoEn_f_9RtwZ+|HaqC^{Oz!>5_! z@%Lo}z^qwbUN9~_cC5CxyW4z(QYeQH!yNqOm*e8_4{-TdZ~!<0fG`1UV>4?O>_d3$ zF%J*1_;UKZ03HDB;sRUZe)wVEKIjjuU+>@mpfH&p9stTW->hHHF z_}ow5`T4(KdG`5VzGQnnW;DJy=c9ve_gU`ucwyGZG|xwj#)}5P+t*(|d+qH94!l`? z-REl#y1Ns5d08tWA|fIpA|fIpA|fIpA|fIpqCeAbk>&?(6K~4T00000NkvXXu0mjf DwaHn# diff --git a/assets/course/management.png b/assets/course/management.png deleted file mode 100644 index 83e5cf4435d38a7045ee6b89d011d776c9af57b9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4821 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 diff --git a/assets/course/mathematics.png b/assets/course/mathematics.png deleted file mode 100644 index d2b4571af2be27267e09ca3496a24813f3fa9394..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2629 zcmV-L3cB@)P))E69X?wfQoi%UNsPFvY)~((HD-Pw( zNloq9vs^g`2STXkPkWcE(1TkkEv=t>)yK{k0|R@ED`)E%+Q0u_f`(0f3LskC8xaO63N;N12)3T*`zw z+-cqVp8+5=^zPmGctX~$b#^{;h7c#G5hK>EBgDaB@Zi;}*;9eQ#AN4ALfqXqZ3+m; z`-I=Vec-^;r-VqQ9M10D__>;z%a^mKJl>!|D_0V-bg8ZF(W8XCeS7t4WTXlM5FUR2 zeo_)4dU~x|35A5XxELD3@kfqq+T`s`$cYo%xBK|8r(3r!TzLLGAw!2YXfS;`A$#_S zL>?YFJO0w8b?Y8JB&2`;>eZc`0P4`85($2;iOIr+?CG&%#>NX45F!-z?Th&sJh)b^ zDN_h}{o30*B0>cL*u7gQbayAj&Ti;Xtboi+7%6n~^SgQT*)uq_W=$6tLgM3tgEKM| z12B4Yn>KLBg$ouIix=Y~GTD|bg|A|EWzmJarqj&G} zfRWMs`Gf+Hm>3_A6tZ0I>-*-7VjVN4Ygefh_wy@Py16O-x;Jm0KK=ZeeE>dum@^0O z%jJ_NuUb`5g+ry%4I3x`IT_soC;$`y3IGLw0w5AuTJG3ENMPXIyDwkRK)~zZCKuE~BbLeEz)T~hhMVZs5ckQ}y0}lD}B_;;HU{q8>0(<)6MMwy)$K`5i z;nY2Irh&otiB+u2muHn{&COf1m@xz6c5zv`5l|%G%89W!Cio-=fsJ~{SO*c zqXupdeSCK9lFL=NAHdst+qR$}LX3?&c9crd*@EH=`^}V0^-m-+HATb;3%hqOF)=rd z7Xbm-3YILfwmy6qj;UF5^5nv6`qa~F+H}?|EQsaH@7z(@egMT98R_eroQzO??;chz zTbhu7>K+>{S#o)1Cfnmvv=yl?090v97*z@YRSJMye*Qd8=ToO@YvVrK$f#}GIdcfn z*KgGdhatAq+`L;ii3GP-GiF@4KuBC%P!PtI+dco#p?UMJT_eQQv}aG5jF6EdTecJm z*;gBzL4($;A>{7eg9o2IQ(*w!yg}D1PSv$*<8;1ejgu1`@8@^o#JqU`GSbcI@`;uU{i0xqbVa zHw9Jvye(Uh&@wRKaxYy%qN;jz%u7JPsZ;p8$^o!>^R#JLi8E&C>7ks9{@0~T3twNo z^72CWZQs6?Dsj1(NPmBfQxO2ehBa*p$8$I{Oh=7yE77=Wr(Cr!$I zEx^huZvdE@E?nsAi&V0Y$cZlO}NN!Gm+>3WY@l0LE=;dFm9}&+FAg3fatT<;su{3V;Ft za$eCm&7*p^aN)#>I5RLH;4t~4<5#WXa%azGzF#Q-u&)$=oI&98M~}u!ytK8OHs$eh zay}w*RC!uOhix~ z02&&tT1h1A6@j$0IGjU=C;$qT3H8j1U$9{Ocx(mw`Yl`H{a?0*LI=Rc=HkV~L^dOU zhX<}l0m#AeD_1%?BGmt~H7Ee#pFIOm0Fsj*KZXMt$FEt#<7KzUzkBE715k8ynl;1M z@7W_33j{?40LC$H-0|b+Z>UlQXBG};*|MM@3P7Io0Qc@4K0J4B?)UaDU+&yNL|`(0 zMFjwdBa;OLAP=Cci-gz2iH8n_hEf22$pj)LJbn6{IsaWIkY)n0@-JRQ6;MYN7{&6_AMuU;*ev!`Zet5!kv_3iA?Ca`7_bS{E@bu~B z%Lu`w~E-g^X(9h1r6cxF##yAMEHyLD@^ z7+uRFMxZ;`&+qv0SFdPC5Y>ML0OmoFy>^su=%|*F5gzX8`TaqW767)K$nML_4dGcdZCb3Tte>o^z+AU z?rF}R#Ua{I*s`AJV{&qjR%Ju&Bdn~7{Vmr0WtoEx)%5k%n^)1|9slx$-B-|1Rnx2F z&C`mB={2CN+)-q0UF>f*DJfB+S7Ny!Zbak8z0Q|i%;mOdK{bEM?C}E$=CxF*0=R`5 nHEPtTQKLqU8Z~Ovs8Rb0U_1g8WcI1%00000NkvXXu0mjf$-*8F diff --git a/assets/course/mechanical.png b/assets/course/mechanical.png deleted file mode 100644 index c7dddcfbfc4031ca956e4095a6e4d9b1304060bb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2923 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 Vi;9I0lE?r6002ovPDHLkV1iChpkDw0 diff --git a/assets/course/music.png b/assets/course/music.png deleted file mode 100644 index fcecc1c103ecfe272c293db196de045be5431a8e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3480 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 diff --git a/assets/course/physical.png b/assets/course/physical.png deleted file mode 100644 index ced43160af5e41d2861a6546765877573432619e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4479 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 R>#qO+002ovPDHLkV1lwQqci{j diff --git a/assets/course/political .png b/assets/course/political .png deleted file mode 100644 index ec160358f7f55d0a43c7c1e26c10b33b39d7fc58..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3130 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 diff --git a/assets/course/reading.png b/assets/course/reading.png deleted file mode 100644 index c283b2181c77a0d733392b9bd89c716a914363d2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3943 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 diff --git a/assets/course/running.png b/assets/course/running.png deleted file mode 100644 index 11405a7d6e2deba82ce9a55ffea942a4fb07a791..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5378 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 diff --git a/assets/course/social.png b/assets/course/social.png deleted file mode 100644 index 4005456a635f877ee33e8daa49a6997adaed0c7f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4403 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 diff --git a/assets/course/sports.png b/assets/course/sports.png deleted file mode 100644 index 909b809476ce57b2fe5acdaf0b938f7f1e8d6f86..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3837 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 diff --git a/assets/course/statistical.png b/assets/course/statistical.png deleted file mode 100644 index 5e7d67d1c374afd767cf74c0c9f375176527a2c4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2465 zcmV;S310SzP)#m73iWc6U>9IolYW1F2d_H;+TgvF|#) z=T;4|CVsysUIWfoFW>o}USd^Vd(WwF51K*|SKSi95W{5c#_JD~`j%Hh040U)6O zkWc{2qmds7mXwv5Y3?j9sO56syM@VSZk327l2sGDG!v3iQVc1JDaVYNOl$PSSJi8H zyg@I3frWv^(M$}R?Ko-8_9ID2@0$R6m*-wS^mM^j#(byhmR?;QcFs}NS0go*d{rrw zwp1jti9cW0;9XS(1sJ46E~=b#tLO8ZxLHer+cGl&5C|9y00QGy@1xVRvX(5gD!uB- zVnG208J!O$KI!Wei?t4_ulHaS7Y94r#Q3t)(kkx9CkN?rIC`}4dtH^<+VE2&6kw3h z-ui}8`}M{~o%7@L=tFaJwSrYU-E}G|W{>$!umx~9TGgY&y>D>2%R>7#e^3CYF)=H; z_K8F*uX;qsCv!N)jobK{JF~OpZQ;!{88j}P%SFy8R=ET=G~f&Fa3Ut6`+P^o3my*& z;1nCr?77p?p|wc0)-ArWa+aRP5tsEPCFD-URy4N0e!fKK-k$j^*4wV`7lM`G^kpU! zL5P;<+Q*Llp$_{Kq5qc| z84gSDI&^&2((-pfD!r_JloowYN?nJO1pg2V(;(s^UXB(tSmC;a#tFxRsAI{IeF&Pua0f}JS7F+8SqmF-oBMtq`xqt zy!=1gi@<=iYPT!Ag0_sVt;K(m@nQ|35uH$8M~C69MRrkn80I_;n=N_L*Y{sLL{+bC zkbi7?x>)Sy78nSCEPAZAiiQHb0ykGSVZ-LsR8k+&2p!(p ziC5+Nz>pGgnaNyZ?5n}-Oid*(CPvc^G&PZFilb;;i3D+1p5ij5rUw6adfcF9L_Zm_zodMg7nM?qLKQJTlU#$^`5M)aOGs@$lKoz{LUJ_(AMZF z@0vg!4=)Pw{~vq;KmlGop#V6c04X7wMz&@g4sVfGyfs@Sl0}-C(fY{z*x1)B02ILP z#?{TOt_=;>atxyB{KQ1=eiI!(6&6bt&3tK$JK-4CUjfo*X*$~mrKeX`YHQor03eGl z32Vke0p5TBHp9-@HJCnp*jnnS9iYhN_V@U*eMBn?Se&yr2M3fKsJpK^_gUv2B(Vw&$3% zG+DHqW!-VH6bdkC1mMz1y0#21x4*}?oMq0tun`IX1$a>bpa6^PA_FlDDIuPvrL?qY zJ|)DVde6%y8y(boBT5+RGJtTo%^V{onDW52s8m_Dm>BEGT$q z1aMe-dxnVE(t^MF0y!|^kFD`{B;l`yLB3r93~w!SKCv0Y@N+7pd~h*^LOFk#$$aes z;P2!?$|qmD0ENj}p#mc|8^Fj_c#6f_>ZZQ#QvehI3Xn47W=2u(y?cG3qMHEZfZ z>;<5%4m^uQ0$>KW0J)Q|R!Vn(uCC%@@HD^kZt(%>-?~amj{-?WMYlx-1%UMQ9&6zy z09dTv)3qT$f?;B{=H&n+CG~B2qz6EDwxqDDRQmOjW^oz-ZEXPN-4fuHlUHmdeH7De zQ+yLhnwq+*N}_TVF({2LMGy1LnTRqX8%=z>kZ!_j^A^x7-7O!}+~` z!y48-A4lz_=i{i+l#T-*SC6sK_!*rAM$)hE@#mXLzrQE6a-8&dnW7u{ygAX)uT201 zAudPL;^J=W?QzL)z_7M?+01TkQ4yqkFn;;rPjeG73_%D2G&(8i)$af#LI|~%N=42P zZlVht8jw?j5LBdeb=-&>;Ue1F*oa&qD5EAG9t!Vkre7hPMJMR%uS)%vw#7pzY;R9Y6pH~EC@tKk zb_emw%BJ&1oJTd5FIRwp0>D54V4wgnaxK8hv)>qQUh-4`LE=Vnw?r~fn$Me(wfz

^Xkssj2t}Btj012Wz~I0w!=c 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 diff --git a/assets/course/training.png b/assets/course/training.png deleted file mode 100644 index 1aa62377f8776e483172e0f1987fc0e0ffb3934a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4985 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

+ + +
+ +

最新版本

+ +
+ 正在获取版本信息... +
+ + + +
+
+ + + + + + + + + + + +
版本号$version
发布时间$releaseTime
+ Android下载 + iOS下载 +
+
+

*此页面仅展示最新版本的应用下载渠道。若需下载以往版本,请前往Github的Release页自行下载。

+ +
+ + + + + + + + diff --git a/ios-build-tools/addBuildNumber.py b/ios-build-tools/addBuildNumber.py deleted file mode 100644 index 5f9e44f9f..000000000 --- a/ios-build-tools/addBuildNumber.py +++ /dev/null @@ -1,44 +0,0 @@ -import re -import subprocess -import sys - -# 从pubspec.yaml文件中读取版本号,格式为x.x.x+x -with open('pubspec.yaml', 'r') as file: - filedata = file.read() -version = re.findall(r'version: (\d+.\d+.\d+)', filedata)[0] -oldBuildNumber = re.findall(r'version: \d+.\d+.\d+(\+\d+)', filedata) -if len(oldBuildNumber) == 0: - buildNumber = 0 -else: - buildNumber = int(oldBuildNumber[0][1:]) - oldBuildNumber = buildNumber - -# 版本号加1 -buildNumber += 1 -oldVersion = version + '+' + str(oldBuildNumber) -newVersion = version + '+' + str(buildNumber) -print('newVersion: ' + newVersion) -print('buildNumber: ' + str(oldBuildNumber) + ' -> ' + str(buildNumber)) - -# 将新的版本号写入pubspec.yaml文件 -filedata = filedata.replace(f'version: ' + oldVersion, 'version: ' + newVersion) -with open('pubspec.yaml', 'w') as file: - file.write(filedata) - -# 接收参数,如果传值参数无输入,则跳过 -if len(sys.argv) != 1: - cmd = f'git add .' - subprocess.run(cmd, shell=True) - cmd = f'git commit -m "build: {newVersion}"' - subprocess.run(cmd, shell=True) - - # {{ github.server_url }} - server_url = sys.argv[1] - # {{ github.repository }} - repository = sys.argv[2] - # {{ github.run_id }} - run_id = sys.argv[3] - # {{ github.run_attempt }} SHOULD BE 1 - run_attempt = sys.argv[4] - cmd = f'git tag -a v{newVersion} -m "v{newVersion}\nrun id: {run_id}\nrun_attempt(should be 1): {run_attempt}\n{server_url}/{repository}/actions/runs/{run_id}"' - subprocess.run(cmd, shell=True) diff --git a/ios-build-tools/toDistribution.py b/ios-build-tools/toDistribution.py deleted file mode 100644 index b4dad9b16..000000000 --- a/ios-build-tools/toDistribution.py +++ /dev/null @@ -1,18 +0,0 @@ -with open('ios/Runner.xcodeproj/project.pbxproj', 'r') as file: - filedata = file.read() - -# CODE_SIGN_IDENTITY所在行进行替换更改,变为CODE_SIGN_IDENTITY = "Apple Distribution"; -filedata = filedata.replace('CODE_SIGN_IDENTITY = "Apple Development";', 'CODE_SIGN_IDENTITY = "Apple Distribution";') - -# CODE_SIGN_STYLE所在行进行替换更改,变为CODE_SIGN_STYLE = Manual; -filedata = filedata.replace('CODE_SIGN_STYLE = Automatic;', 'CODE_SIGN_STYLE = Manual;') - -# DEVELOPMENT_TEAM所在行进行替换更改,变为DEVELOPMENT_TEAM = "M5APZD5CKA"; -filedata = filedata.replace('DEVELOPMENT_TEAM = "";', 'DEVELOPMENT_TEAM = "M5APZD5CKA";') - -# PROVISIONING_PROFILE_SPECIFIER所在行进行替换更改,变为PROVISIONING_PROFILE_SPECIFIER = "SITLife-Distribution-AppStore"; -filedata = filedata.replace('PROVISIONING_PROFILE_SPECIFIER = "";', 'PROVISIONING_PROFILE_SPECIFIER = "SITLife-Distribution-AppStore";') - -# 文件替换完成后,写回到文件中 -with open('ios/Runner.xcodeproj/project.pbxproj', 'w') as file: - file.write(filedata) diff --git a/ios/.gitignore b/ios/.gitignore deleted file mode 100644 index 7a7f9873a..000000000 --- a/ios/.gitignore +++ /dev/null @@ -1,34 +0,0 @@ -**/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 diff --git a/ios/ExportOptions.plist b/ios/ExportOptions.plist deleted file mode 100644 index c153e169e..000000000 --- a/ios/ExportOptions.plist +++ /dev/null @@ -1,17 +0,0 @@ - - - - - method - app-store - teamID - M5APZD5CKA - signingStyle - manual - provisioningProfiles - - life.mysit.SITLife - eb8b1f5f-3329-42a5-a18f-8254a2e85b41 - - - diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index 7c5696400..000000000 --- a/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,26 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - MinimumOSVersion - 12.0 - - diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig deleted file mode 100644 index ec97fc6f3..000000000 --- a/ios/Flutter/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" -#include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig deleted file mode 100644 index c4855bfe2..000000000 --- a/ios/Flutter/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" -#include "Generated.xcconfig" diff --git a/ios/Gemfile b/ios/Gemfile deleted file mode 100644 index 7a118b49b..000000000 --- a/ios/Gemfile +++ /dev/null @@ -1,3 +0,0 @@ -source "https://rubygems.org" - -gem "fastlane" diff --git a/ios/Podfile b/ios/Podfile deleted file mode 100644 index dc00e7ab0..000000000 --- a/ios/Podfile +++ /dev/null @@ -1,51 +0,0 @@ -# Uncomment this line to define a global platform for your project -platform :ios, '12.0' - -# CocoaPods analytics sends network stats synchronously affecting flutter build latency. -ENV['COCOAPODS_DISABLE_STATS'] = 'true' - -project 'Runner', { - 'Debug' => :debug, - 'Profile' => :release, - 'Release' => :release, -} - -def flutter_root - generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) - unless File.exist?(generated_xcode_build_settings_path) - raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" - end - - File.foreach(generated_xcode_build_settings_path) do |line| - matches = line.match(/FLUTTER_ROOT\=(.*)/) - return matches[1].strip if matches - end - raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" -end - -require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) - -flutter_ios_podfile_setup - -target 'Runner' do - use_frameworks! - use_modular_headers! - - flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) -end - -post_install do |installer| - installer.pods_project.targets.each do |target| - flutter_additional_ios_build_settings(target) - # https://github.com/CocoaPods/CocoaPods/issues/12012 - target.build_configurations.each do |config| - xcconfig_path = config.base_configuration_reference.real_path - xcconfig = File.read(xcconfig_path) - xcconfig_mod = xcconfig.gsub(/DT_TOOLCHAIN_DIR/, "TOOLCHAIN_DIR") - File.open(xcconfig_path, "w") { |file| file << xcconfig_mod } - config.build_settings['CODE_SIGNING_REQUIRED'] = "NO" - config.build_settings['CODE_SIGNING_ALLOWED'] = "NO" - config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0' - end - end -end diff --git a/ios/Podfile.lock b/ios/Podfile.lock deleted file mode 100644 index 15ebbcb6c..000000000 --- a/ios/Podfile.lock +++ /dev/null @@ -1,293 +0,0 @@ -PODS: - - add_2_calendar (0.0.1): - - Flutter - - app_settings (5.1.1): - - Flutter - - audio_session (0.0.1): - - Flutter - - connectivity_plus (0.0.1): - - Flutter - - ReachabilitySwift - - device_info_plus (0.0.1): - - Flutter - - DKImagePickerController/Core (4.3.4): - - DKImagePickerController/ImageDataManager - - DKImagePickerController/Resource - - DKImagePickerController/ImageDataManager (4.3.4) - - DKImagePickerController/PhotoGallery (4.3.4): - - DKImagePickerController/Core - - DKPhotoGallery - - DKImagePickerController/Resource (4.3.4) - - DKPhotoGallery (0.0.17): - - DKPhotoGallery/Core (= 0.0.17) - - DKPhotoGallery/Model (= 0.0.17) - - DKPhotoGallery/Preview (= 0.0.17) - - DKPhotoGallery/Resource (= 0.0.17) - - SDWebImage - - SwiftyGif - - DKPhotoGallery/Core (0.0.17): - - DKPhotoGallery/Model - - DKPhotoGallery/Preview - - SDWebImage - - SwiftyGif - - DKPhotoGallery/Model (0.0.17): - - SDWebImage - - SwiftyGif - - DKPhotoGallery/Preview (0.0.17): - - DKPhotoGallery/Model - - DKPhotoGallery/Resource - - SDWebImage - - SwiftyGif - - DKPhotoGallery/Resource (0.0.17): - - SDWebImage - - SwiftyGif - - file_picker (0.0.1): - - DKImagePickerController/PhotoGallery - - Flutter - - fk_user_agent (2.0.0): - - Flutter - - Flutter (1.0.0) - - FMDB (2.7.5): - - FMDB/standard (= 2.7.5) - - FMDB/standard (2.7.5) - - GoogleDataTransport (9.2.5): - - GoogleUtilities/Environment (~> 7.7) - - nanopb (< 2.30910.0, >= 2.30908.0) - - PromisesObjC (< 3.0, >= 1.2) - - GoogleMLKit/BarcodeScanning (4.0.0): - - GoogleMLKit/MLKitCore - - MLKitBarcodeScanning (~> 3.0.0) - - GoogleMLKit/MLKitCore (4.0.0): - - MLKitCommon (~> 9.0.0) - - GoogleToolboxForMac/DebugUtils (2.3.2): - - GoogleToolboxForMac/Defines (= 2.3.2) - - GoogleToolboxForMac/Defines (2.3.2) - - GoogleToolboxForMac/Logger (2.3.2): - - GoogleToolboxForMac/Defines (= 2.3.2) - - "GoogleToolboxForMac/NSData+zlib (2.3.2)": - - GoogleToolboxForMac/Defines (= 2.3.2) - - "GoogleToolboxForMac/NSDictionary+URLArguments (2.3.2)": - - GoogleToolboxForMac/DebugUtils (= 2.3.2) - - GoogleToolboxForMac/Defines (= 2.3.2) - - "GoogleToolboxForMac/NSString+URLArguments (= 2.3.2)" - - "GoogleToolboxForMac/NSString+URLArguments (2.3.2)" - - GoogleUtilities/Environment (7.11.5): - - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/Logger (7.11.5): - - GoogleUtilities/Environment - - GoogleUtilities/UserDefaults (7.11.5): - - GoogleUtilities/Logger - - GoogleUtilitiesComponents (1.1.0): - - GoogleUtilities/Logger - - GTMSessionFetcher/Core (2.3.0) - - image_picker_ios (0.0.1): - - Flutter - - just_audio (0.0.1): - - Flutter - - MLImage (1.0.0-beta4) - - MLKitBarcodeScanning (3.0.0): - - MLKitCommon (~> 9.0) - - MLKitVision (~> 5.0) - - MLKitCommon (9.0.0): - - GoogleDataTransport (~> 9.0) - - GoogleToolboxForMac/Logger (~> 2.1) - - "GoogleToolboxForMac/NSData+zlib (~> 2.1)" - - "GoogleToolboxForMac/NSDictionary+URLArguments (~> 2.1)" - - GoogleUtilities/UserDefaults (~> 7.0) - - GoogleUtilitiesComponents (~> 1.0) - - GTMSessionFetcher/Core (< 3.0, >= 1.1) - - MLKitVision (5.0.0): - - GoogleToolboxForMac/Logger (~> 2.1) - - "GoogleToolboxForMac/NSData+zlib (~> 2.1)" - - GTMSessionFetcher/Core (< 3.0, >= 1.1) - - MLImage (= 1.0.0-beta4) - - MLKitCommon (~> 9.0) - - mobile_scanner (3.5.2): - - Flutter - - GoogleMLKit/BarcodeScanning (~> 4.0.0) - - nanopb (2.30909.0): - - nanopb/decode (= 2.30909.0) - - nanopb/encode (= 2.30909.0) - - nanopb/decode (2.30909.0) - - nanopb/encode (2.30909.0) - - open_file (0.0.1): - - Flutter - - package_info_plus (0.4.5): - - Flutter - - path_provider_foundation (0.0.1): - - Flutter - - FlutterMacOS - - permission_handler_apple (9.1.1): - - Flutter - - PromisesObjC (2.3.1) - - quick_actions_ios (0.0.1): - - Flutter - - ReachabilitySwift (5.0.0) - - SDWebImage (5.18.0): - - SDWebImage/Core (= 5.18.0) - - SDWebImage/Core (5.18.0) - - share_plus (0.0.1): - - Flutter - - shared_preferences_foundation (0.0.1): - - Flutter - - FlutterMacOS - - sqflite (0.0.3): - - Flutter - - FMDB (>= 2.7.5) - - SwiftyGif (5.4.4) - - url_launcher_ios (0.0.1): - - Flutter - - vibration (1.7.5): - - Flutter - - video_player_avfoundation (0.0.1): - - Flutter - - FlutterMacOS - - wakelock_plus (0.0.1): - - Flutter - - webview_flutter_wkwebview (0.0.1): - - Flutter - -DEPENDENCIES: - - add_2_calendar (from `.symlinks/plugins/add_2_calendar/ios`) - - app_settings (from `.symlinks/plugins/app_settings/ios`) - - audio_session (from `.symlinks/plugins/audio_session/ios`) - - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - - file_picker (from `.symlinks/plugins/file_picker/ios`) - - fk_user_agent (from `.symlinks/plugins/fk_user_agent/ios`) - - Flutter (from `Flutter`) - - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - - just_audio (from `.symlinks/plugins/just_audio/ios`) - - mobile_scanner (from `.symlinks/plugins/mobile_scanner/ios`) - - open_file (from `.symlinks/plugins/open_file/ios`) - - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - - quick_actions_ios (from `.symlinks/plugins/quick_actions_ios/ios`) - - share_plus (from `.symlinks/plugins/share_plus/ios`) - - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - - sqflite (from `.symlinks/plugins/sqflite/ios`) - - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - - vibration (from `.symlinks/plugins/vibration/ios`) - - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`) - - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) - - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`) - -SPEC REPOS: - trunk: - - DKImagePickerController - - DKPhotoGallery - - FMDB - - GoogleDataTransport - - GoogleMLKit - - GoogleToolboxForMac - - GoogleUtilities - - GoogleUtilitiesComponents - - GTMSessionFetcher - - MLImage - - MLKitBarcodeScanning - - MLKitCommon - - MLKitVision - - nanopb - - PromisesObjC - - ReachabilitySwift - - SDWebImage - - SwiftyGif - -EXTERNAL SOURCES: - add_2_calendar: - :path: ".symlinks/plugins/add_2_calendar/ios" - app_settings: - :path: ".symlinks/plugins/app_settings/ios" - audio_session: - :path: ".symlinks/plugins/audio_session/ios" - connectivity_plus: - :path: ".symlinks/plugins/connectivity_plus/ios" - device_info_plus: - :path: ".symlinks/plugins/device_info_plus/ios" - file_picker: - :path: ".symlinks/plugins/file_picker/ios" - fk_user_agent: - :path: ".symlinks/plugins/fk_user_agent/ios" - Flutter: - :path: Flutter - image_picker_ios: - :path: ".symlinks/plugins/image_picker_ios/ios" - just_audio: - :path: ".symlinks/plugins/just_audio/ios" - mobile_scanner: - :path: ".symlinks/plugins/mobile_scanner/ios" - open_file: - :path: ".symlinks/plugins/open_file/ios" - package_info_plus: - :path: ".symlinks/plugins/package_info_plus/ios" - path_provider_foundation: - :path: ".symlinks/plugins/path_provider_foundation/darwin" - permission_handler_apple: - :path: ".symlinks/plugins/permission_handler_apple/ios" - quick_actions_ios: - :path: ".symlinks/plugins/quick_actions_ios/ios" - share_plus: - :path: ".symlinks/plugins/share_plus/ios" - shared_preferences_foundation: - :path: ".symlinks/plugins/shared_preferences_foundation/darwin" - sqflite: - :path: ".symlinks/plugins/sqflite/ios" - url_launcher_ios: - :path: ".symlinks/plugins/url_launcher_ios/ios" - vibration: - :path: ".symlinks/plugins/vibration/ios" - video_player_avfoundation: - :path: ".symlinks/plugins/video_player_avfoundation/darwin" - wakelock_plus: - :path: ".symlinks/plugins/wakelock_plus/ios" - webview_flutter_wkwebview: - :path: ".symlinks/plugins/webview_flutter_wkwebview/ios" - -SPEC CHECKSUMS: - add_2_calendar: 5eee66d5a3b99cd5e1487a7e03abd4e3ac4aff11 - app_settings: 017320c6a680cdc94c799949d95b84cb69389ebc - audio_session: 4f3e461722055d21515cf3261b64c973c062f345 - connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d - device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 - DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac - DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 - file_picker: 15fd9539e4eb735dc54bae8c0534a7a9511a03de - fk_user_agent: 1f47ec39291e8372b1d692b50084b0d54103c545 - Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 - FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a - GoogleDataTransport: 54dee9d48d14580407f8f5fbf2f496e92437a2f2 - GoogleMLKit: 2bd0dc6253c4d4f227aad460f69215a504b2980e - GoogleToolboxForMac: 8bef7c7c5cf7291c687cf5354f39f9db6399ad34 - GoogleUtilities: 13e2c67ede716b8741c7989e26893d151b2b2084 - GoogleUtilitiesComponents: 679b2c881db3b615a2777504623df6122dd20afe - GTMSessionFetcher: 3a63d75eecd6aa32c2fc79f578064e1214dfdec2 - image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5 - just_audio: baa7252489dbcf47a4c7cc9ca663e9661c99aafa - MLImage: 7bb7c4264164ade9bf64f679b40fb29c8f33ee9b - MLKitBarcodeScanning: 04e264482c5f3810cb89ebc134ef6b61e67db505 - MLKitCommon: c1b791c3e667091918d91bda4bba69a91011e390 - MLKitVision: 8baa5f46ee3352614169b85250574fde38c36f49 - mobile_scanner: 5090a13b7a35fc1c25b0d97e18e84f271a6eb605 - nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431 - open_file: 02eb5cb6b21264bd3a696876f5afbfb7ca4f4b7d - package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 - path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 - permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 - PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4 - quick_actions_ios: 9e80dcfadfbc5d47d9cf8f47bcf428b11cf383d4 - ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 - SDWebImage: 182830bcddc30cde95fbc60dfe4badc3553d94ba - share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5 - shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 - sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a - SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f - url_launcher_ios: 68d46cc9766d0c41dbdc884310529557e3cd7a86 - vibration: 7d883d141656a1c1a6d8d238616b2042a51a1241 - video_player_avfoundation: 8563f13d8fc8b2c29dc2d09e60b660e4e8128837 - wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47 - webview_flutter_wkwebview: 2e2d318f21a5e036e2c3f26171342e95908bd60a - -PODFILE CHECKSUM: 2dd2fa37aad0529d8a419ffb4dbab67a5a6dd9b9 - -COCOAPODS: 1.12.1 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index 78451207b..000000000 --- a/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,605 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 54; - objects = { - -/* Begin PBXBuildFile section */ - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.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 */; }; - EA915BBF27B52FC0003004DD /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = EA915BC127B52FC0003004DD /* InfoPlist.strings */; }; - F50330E48F3AE3E0400D160C /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 11DB5EC50A928A20CBD3F2E8 /* Pods_Runner.framework */; }; -/* End PBXBuildFile 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 */ - 11DB5EC50A928A20CBD3F2E8 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 4D53D58D2ADC321E00255A67 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; - 53E1F9106D320232BBE160D0 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; - 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 89872809EE6E03F14F4DDD73 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; - 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 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 = ""; }; - 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - A4B337E0717EC4077C02BECA /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - EA915BC027B52FC0003004DD /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; - EA915BC227B53039003004DD /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = ""; }; - EAC24D4728F0287200618B0C /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/InfoPlist.strings"; sourceTree = ""; }; - EACA95CD27BBDB80002FCB96 /* RunnerDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerDebug.entitlements; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - F50330E48F3AE3E0400D160C /* Pods_Runner.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 4A45559DBC458EC0E40B88E0 /* Pods */ = { - isa = PBXGroup; - children = ( - A4B337E0717EC4077C02BECA /* Pods-Runner.debug.xcconfig */, - 89872809EE6E03F14F4DDD73 /* Pods-Runner.release.xcconfig */, - 53E1F9106D320232BBE160D0 /* Pods-Runner.profile.xcconfig */, - ); - path = Pods; - sourceTree = ""; - }; - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 9740EEB31CF90195004384FC /* Generated.xcconfig */, - ); - name = Flutter; - sourceTree = ""; - }; - 97C146E51CF9000F007C117D = { - isa = PBXGroup; - children = ( - 9740EEB11CF90186004384FC /* Flutter */, - EACA95CD27BBDB80002FCB96 /* RunnerDebug.entitlements */, - 97C146F01CF9000F007C117D /* Runner */, - 97C146EF1CF9000F007C117D /* Products */, - 4A45559DBC458EC0E40B88E0 /* Pods */, - D1AAE1668A8C767130408518 /* Frameworks */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 4D53D58D2ADC321E00255A67 /* Runner.entitlements */, - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - EA915BC127B52FC0003004DD /* InfoPlist.strings */, - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, - 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, - 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, - ); - path = Runner; - sourceTree = ""; - }; - D1AAE1668A8C767130408518 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 11DB5EC50A928A20CBD3F2E8 /* Pods_Runner.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - B3A58F7A51D1F11234C53B0C /* [CP] Check Pods Manifest.lock */, - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 8D031BFA1068FBF3F0D98A42 /* [CP] Embed Pods Frameworks */, - ); - 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 = { - LastUpgradeCheck = 1430; - ORGANIZATIONNAME = ""; - TargetAttributes = { - 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, - "zh-Hans", - "zh-Hant", - ); - mainGroup = 97C146E51CF9000F007C117D; - productRefGroup = 97C146EF1CF9000F007C117D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 97C146ED1CF9000F007C117D /* Runner */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 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 */, - EA915BBF27B52FC0003004DD /* InfoPlist.strings 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"; - }; - 8D031BFA1068FBF3F0D98A42 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 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"; - }; - B3A58F7A51D1F11234C53B0C /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; - EA915BC127B52FC0003004DD /* InfoPlist.strings */ = { - isa = PBXVariantGroup; - children = ( - EA915BC027B52FC0003004DD /* en */, - EA915BC227B53039003004DD /* zh-Hans */, - EAC24D4728F0287200618B0C /* zh-Hant */, - ); - name = InfoPlist.strings; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 249021D3217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = 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 = "iPhone Distribution"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development: ziqi wei (U243T4LN6L)"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - 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 = 12.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; - CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = M5APZD5CKA; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = "SIT Life"; - INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education"; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)"; - PRODUCT_BUNDLE_IDENTIFIER = life.mysit.SITLife; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Profile; - }; - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = 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 = "iPhone Distribution"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development: ziqi wei (U243T4LN6L)"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - 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 = 12.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; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = 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 = "iPhone Distribution"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development: ziqi wei (U243T4LN6L)"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - 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 = 12.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - 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; - CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = M5APZD5CKA; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = "SIT Life"; - INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education"; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)"; - PRODUCT_BUNDLE_IDENTIFIER = life.mysit.SITLife; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - 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; - CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = M5APZD5CKA; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = "SIT Life"; - INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education"; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)"; - PRODUCT_BUNDLE_IDENTIFIER = life.mysit.SITLife; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - 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 */ - 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 */; -} diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 919434a62..000000000 --- a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d981003..000000000 --- a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index f9b0d7c5e..000000000 --- a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - PreviewsEnabled - - - diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index b52b2e698..000000000 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 21a3cc14c..000000000 --- a/ios/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d981003..000000000 --- a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index f9b0d7c5e..000000000 --- a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - PreviewsEnabled - - - diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift deleted file mode 100644 index 70693e4a8..000000000 --- a/ios/Runner/AppDelegate.swift +++ /dev/null @@ -1,13 +0,0 @@ -import UIKit -import Flutter - -@UIApplicationMain -@objc class AppDelegate: FlutterAppDelegate { - override func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - GeneratedPluginRegistrant.register(with: self) - return super.application(application, didFinishLaunchingWithOptions: launchOptions) - } -} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/1024x1024.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/1024x1024.png deleted file mode 100644 index faf83fabac7db8aaf61f65929618515eff645f5d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24170 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 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/167x167.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/167x167.png deleted file mode 100644 index 61a9585bacae1f774b00a2e459118347a801f9de..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3494 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 a5&)D7)}^*EPVt9K0h+3MDwPjyL;nS2xBDdk diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/20x20.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/20x20.png deleted file mode 100644 index daabe8106462b0d7a50b599d1c62063dc4d1df00..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 525 zcmV+o0`mQdP)s5A5H+|3A5a ziGc!uPxI94Hv^U*efjYRIhuJ`S@7G8E`8&}mm42He*63T&;NfwZlICG&L__p?=v#| zW@P-$peHS$Coh0g^Tr3SZ+w3J`sddl|Ni{@|DW+EBja}z5Md%{-5 zrG_w;06TjB?LQs{24)aDW5-FLY9M3n{f9lfZU7m_Z@&Hd^N*4QH}=r}&&K$Vhfz-! zWbK^~pI?6X31ocv^XC=Vl-u9FipX<_3vl4@Gs}N&er5>MT1r%0fE~zC;p62~12Haq z_@J+f+s}8teA1KQgRmRz^r5DRi!q2Zy!`&-1?!hMSy}9wUwr$1^!2MULoJ{?cRqdk z<>z0=tZ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/29x29 1.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/29x29 1.png deleted file mode 100644 index 7e013938a1ce15064110010212ab1436a5485db3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 701 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 DT>dhs diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/80x80.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/80x80.png deleted file mode 100644 index 73b5b719783f25ad3c54c936521bc5cebe2808d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1709 zcmV;e22%NnP)6V<*|iOYE>R+Yq`+tu5;j9Q`0{_ zdCp`xkYOjD#_s!qw=bQlTa}(oB<{{Hl=k>nlJ_QpbqnkZhqrbHqv^T@cA3qG%SwCg zc9AbP9DN>J-2x-pug{(l8#b7)zSq;|X{u){-ED3B?%e@@B*{P^H4CDF>Ktn0hmnN6 z4%?wv!}SnprStiRE0I4#k*u6wM-|F>Qe_Bb20{kC@O4v>Q7^gT1^{xgEdv0Hjg3w` z23<)HPW#rsC!Omf33@l?>Sk8=RPo|zPvTbgl zD`^wXvL4(3*T0{-^ZA>!ReCO-{^{vTCNI<1OwF;1cHpcGA1uadwG^E;6K&vU@l3WB zvQ^4Zzmqob@@lSDv|Uc&;(1TataKujP3N(>LRR{i!(ueD2Wq8R2Ieilm}2XG3p*DPYcYZyYHSZSx#NMQl{7NgKmv>lM?mG->*rx6%p31j1|x+SO8 zL;=_zPL%@py<(#Tj5Uf=G6Q^9y6t+f5?+Zh2A~3?w25N!z!qeSeS<@wLAxA-4IAPW z4ZL(jq$gp>6@lT4iK1QRDvQDwB*5gMXf^d6)3SZP50x&JaI5$tfX&r_qXl0;l-k=) z5g4Bvu*3zE8AvTVsp84dpzv+G#2tN7 zcVpuhr%v2|9Q2-OuAEVITN}SRe|lmeI^a37t#}~|L2>H@nMuhm&;t&vt`$`pibh^) zu0%52*^cUNKGy4OMs?e_W@AZ)hpJ!bYSm~aH3H8@ld!1LU3MBKdOQ+=1{v>c;TQnv za}zyaq|g3%LJ98BtKUiESZq8Vg2p^Pg1|xsUab$+0z9_#Z-JKN4Fm4h0}rqg$#h=k zSO8fiU6K1+DFTl!1XL4^jfd^PGvU9(Q6tOZ4P1zW_ugjhI>qnZT%~=Xu&U3)ooy8K zc_x;yuzc}ojyc_8rw9Da^jxVvU+6wc0r~LJxuV-*10VLHfjN8#Od1fZliQL_o{nyl zn=?ZqTO{P3W~+tgXaJAQ`0=I$tArcy`WxP@S>cVz$evaef>6bx4^z|WW?`Rlo`qPv)5krwv>lNV=F7Wr3FRWip6MVkH5a5 zy(XUfKGRcXNamLS=#Y@@GXY}4c z4y*&~z&fxFtOM)7I%jF_{uf{XAUf5e1v4=~00000NkvXXu0mjf DT>dhs diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/87x87.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/87x87.png deleted file mode 100644 index b4fc304141dcaa96e97a1f7c86791c930964914b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1869 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 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard deleted file mode 100644 index 76a4babe3..000000000 --- a/ios/Runner/Base.lproj/Main.storyboard +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist deleted file mode 100644 index c0f6b3481..000000000 --- a/ios/Runner/Info.plist +++ /dev/null @@ -1,89 +0,0 @@ - - - - - CADisableMinimumFrameDurationOnPhone - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - SIT Life - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIcons - - CFBundleIcons~ipad - - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - life.mysit.SITLife - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleURLTypes - - - CFBundleTypeRole - Editor - CFBundleURLName - URL.life.mysit.SITLife - CFBundleURLSchemes - - life.mysit - - - - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - ITSAppUsesNonExemptEncryption - - LSRequiresIPhoneOS - - NSAppTransportSecurity - - NSAllowsArbitraryLoads - - NSCalendarsUsageDescription - INSERT_REASON_HERE - - NSCalendarsUsageDescription - Used for Calendar Task. - NSCameraUsageDescription - Used for Image Picker plugin for Flutter - NSMicrophoneUsageDescription - Used for Image Picker plugin for Flutter - NSPhotoLibraryUsageDescription - Used for Image Picker plugin for Flutter - UIApplicationSupportsIndirectInputEvents - - UIBackgroundModes - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UIStatusBarHidden - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - - UIViewControllerBasedStatusBarAppearance - - io.flutter.embedded_views_preview - YES - - diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h deleted file mode 100644 index 308a2a560..000000000 --- a/ios/Runner/Runner-Bridging-Header.h +++ /dev/null @@ -1 +0,0 @@ -#import "GeneratedPluginRegistrant.h" diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements deleted file mode 100644 index df1b84c30..000000000 --- a/ios/Runner/Runner.entitlements +++ /dev/null @@ -1,24 +0,0 @@ - - - - - aps-environment - development - com.apple.developer.associated-domains - - https://mysit.life - - com.apple.developer.icloud-container-identifiers - - iCloud.life.mysit.SITLife - - com.apple.developer.icloud-services - - CloudDocuments - - com.apple.developer.ubiquity-container-identifiers - - iCloud.life.mysit.SITLife - - - diff --git a/ios/Runner/RunnerProfile.entitlements b/ios/Runner/RunnerProfile.entitlements deleted file mode 100644 index 903def2af..000000000 --- a/ios/Runner/RunnerProfile.entitlements +++ /dev/null @@ -1,8 +0,0 @@ - - - - - aps-environment - development - - diff --git a/ios/Runner/en.lproj/InfoPlist.strings b/ios/Runner/en.lproj/InfoPlist.strings deleted file mode 100644 index f0e3d4168..000000000 --- a/ios/Runner/en.lproj/InfoPlist.strings +++ /dev/null @@ -1,6 +0,0 @@ -CFBundleDisplayName = "SIT Life"; - -NSCalendarsUsageDescription = "Requires calendar premission"; -NSCameraUsageDescription = "Requires camera premission"; -NSPhotoLibraryUsageDescription = "Requires album premission"; -NFCReaderUsageDescription = "Requires NFC premission"; diff --git a/ios/Runner/zh-Hans.lproj/InfoPlist.strings b/ios/Runner/zh-Hans.lproj/InfoPlist.strings deleted file mode 100644 index 9a3e8bd14..000000000 --- a/ios/Runner/zh-Hans.lproj/InfoPlist.strings +++ /dev/null @@ -1,6 +0,0 @@ -CFBundleDisplayName = "小应生活"; - -NSCalendarsUsageDescription = "需要日历权限"; -NSCameraUsageDescription = "需要相机权限"; -NSPhotoLibraryUsageDescription = "需要照片权限"; -NFCReaderUsageDescription = "需要NFC权限"; diff --git a/ios/Runner/zh-Hant.lproj/InfoPlist.strings b/ios/Runner/zh-Hant.lproj/InfoPlist.strings deleted file mode 100644 index 5117a4b15..000000000 --- a/ios/Runner/zh-Hant.lproj/InfoPlist.strings +++ /dev/null @@ -1,6 +0,0 @@ -CFBundleDisplayName = "小鷹生活"; - -NSCalendarsUsageDescription = "需要行事曆权限"; -NSCameraUsageDescription = "需要相機權限"; -NSPhotoLibraryUsageDescription = "需要相簿權限"; -NFCReaderUsageDescription = "需要NFC權限"; diff --git a/ios/RunnerDebug.entitlements b/ios/RunnerDebug.entitlements deleted file mode 100644 index 903def2af..000000000 --- a/ios/RunnerDebug.entitlements +++ /dev/null @@ -1,8 +0,0 @@ - - - - - aps-environment - development - - diff --git a/ios/RunnerTests/RunnerTests.swift b/ios/RunnerTests/RunnerTests.swift deleted file mode 100644 index 86a7c3b1b..000000000 --- a/ios/RunnerTests/RunnerTests.swift +++ /dev/null @@ -1,12 +0,0 @@ -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. - } - -} diff --git a/ios/index.html b/ios/index.html new file mode 100644 index 000000000..e5b24f227 --- /dev/null +++ b/ios/index.html @@ -0,0 +1,139 @@ + + + + + + + 获取 小应生活 + + + + + + + + + + + +
+ +

最新版本 - iOS下载

+ +
+ 正在获取版本信息... +
+ + + +
+
+ + + + + + + + + + + + + + + +
文件名$fileName
版本号$version
发布时间$releaseTime
+ 官方源下载(可能被墙) + 镜像源下载 + 从App Store下载 + 从TestFlight下载 +
+
+

*此页面仅展示最新版本的应用下载渠道。若需下载以往版本,请前往Github的Release页自行下载。

+ +
+ + + + + + + + diff --git a/lib/app.dart b/lib/app.dart deleted file mode 100644 index e57cddc3e..000000000 --- a/lib/app.dart +++ /dev/null @@ -1,115 +0,0 @@ -import 'dart:ui'; - -import 'package:animations/animations.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:sit/credentials/widgets/oa_scope.dart'; -import 'package:sit/files.dart'; -import 'package:sit/r.dart'; -import 'package:sit/route.dart'; -import 'package:sit/session/widgets/scope.dart'; -import 'package:sit/settings/settings.dart'; -import 'package:sit/utils/color.dart'; -import 'package:system_theme/system_theme.dart'; - -class MimirApp extends StatefulWidget { - const MimirApp({super.key}); - - @override - State createState() => _MimirAppState(); -} - -class _MimirAppState extends State { - final $theme = Settings.theme.listenThemeChange(); - final $routingConfig = ValueNotifier( - Settings.focusTimetable ? buildTimetableFocusRouter() : buildCommonRoutingConfig(), - ); - final $focusMode = Settings.listenFocusTimetable(); - late final router = buildRouter($routingConfig); - - @override - void initState() { - super.initState(); - $theme.addListener(refresh); - $focusMode.addListener(refreshFocusMode); - } - - @override - void didChangeDependencies() { - if (Settings.timetable.backgroundImage?.enabled == true) { - precacheImage(FileImage(Files.timetable.backgroundFile), context); - } - super.didChangeDependencies(); - } - - @override - void dispose() { - $theme.removeListener(refresh); - $focusMode.removeListener(refreshFocusMode); - super.dispose(); - } - - void refresh() { - setState(() {}); - } - - void refreshFocusMode() { - $routingConfig.value = Settings.focusTimetable ? buildTimetableFocusRouter() : buildCommonRoutingConfig(); - } - - @override - Widget build(BuildContext context) { - final themeColor = Settings.theme.themeColor ?? SystemTheme.accentColor.maybeAccent; - - ThemeData bakeTheme(ThemeData origin) { - return origin.copyWith( - platform: R.debugCupertino ? TargetPlatform.iOS : null, - colorScheme: themeColor == null - ? null - : ColorScheme.fromSeed( - seedColor: themeColor, - brightness: origin.brightness, - ), - visualDensity: VisualDensity.comfortable, - pageTransitionsTheme: const PageTransitionsTheme( - builders: { - TargetPlatform.android: ZoomPageTransitionsBuilder(), - TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), - TargetPlatform.macOS: CupertinoPageTransitionsBuilder(), - TargetPlatform.linux: SharedAxisPageTransitionsBuilder(transitionType: SharedAxisTransitionType.vertical), - TargetPlatform.windows: SharedAxisPageTransitionsBuilder(transitionType: SharedAxisTransitionType.vertical), - }, - ), - ); - } - - return MaterialApp.router( - title: R.appName, - routerConfig: router, - localizationsDelegates: context.localizationDelegates, - supportedLocales: context.supportedLocales, - locale: context.locale, - themeMode: Settings.theme.themeMode, - theme: bakeTheme(ThemeData.light( - useMaterial3: true, - )), - darkTheme: bakeTheme(ThemeData.dark( - useMaterial3: true, - )), - builder: (ctx, child) => OaAuthManager( - child: OaOnlineManager( - child: child ?? const SizedBox(), - ), - ), - scrollBehavior: const MaterialScrollBehavior().copyWith( - dragDevices: { - PointerDeviceKind.mouse, - PointerDeviceKind.touch, - PointerDeviceKind.stylus, - PointerDeviceKind.trackpad, - PointerDeviceKind.unknown - }, - ), - ); - } -} diff --git a/lib/credentials/entity/credential.dart b/lib/credentials/entity/credential.dart deleted file mode 100644 index a2436cf08..000000000 --- a/lib/credentials/entity/credential.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:copy_with_extension/copy_with_extension.dart'; -import 'package:sit/storage/hive/type_id.dart'; - -part 'credential.g.dart'; - -@HiveType(typeId: CoreHiveType.credentials) -@CopyWith(skipFields: true) -class Credentials { - @HiveField(0) - final String account; - @HiveField(1) - final String password; - - Credentials({ - required this.account, - required this.password, - }); - - @override - String toString() => 'account:"$account", password:"$password"'; - - @override - bool operator ==(Object other) { - return other is Credentials && - runtimeType == other.runtimeType && - account == other.account && - password == other.password; - } - - @override - int get hashCode => toString().hashCode; -} diff --git a/lib/credentials/entity/credential.g.dart b/lib/credentials/entity/credential.g.dart deleted file mode 100644 index 1b82fbc3a..000000000 --- a/lib/credentials/entity/credential.g.dart +++ /dev/null @@ -1,96 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'credential.dart'; - -// ************************************************************************** -// CopyWithGenerator -// ************************************************************************** - -abstract class _$CredentialsCWProxy { - /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. - /// - /// Usage - /// ```dart - /// Credentials(...).copyWith(id: 12, name: "My name") - /// ```` - Credentials call({ - String? account, - String? password, - }); -} - -/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfCredentials.copyWith(...)`. -class _$CredentialsCWProxyImpl implements _$CredentialsCWProxy { - const _$CredentialsCWProxyImpl(this._value); - - final Credentials _value; - - @override - - /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. - /// - /// Usage - /// ```dart - /// Credentials(...).copyWith(id: 12, name: "My name") - /// ```` - Credentials call({ - Object? account = const $CopyWithPlaceholder(), - Object? password = const $CopyWithPlaceholder(), - }) { - return Credentials( - account: account == const $CopyWithPlaceholder() || account == null - ? _value.account - // ignore: cast_nullable_to_non_nullable - : account as String, - password: password == const $CopyWithPlaceholder() || password == null - ? _value.password - // ignore: cast_nullable_to_non_nullable - : password as String, - ); - } -} - -extension $CredentialsCopyWith on Credentials { - /// Returns a callable class that can be used as follows: `instanceOfCredentials.copyWith(...)`. - // ignore: library_private_types_in_public_api - _$CredentialsCWProxy get copyWith => _$CredentialsCWProxyImpl(this); -} - -// ************************************************************************** -// TypeAdapterGenerator -// ************************************************************************** - -class CredentialsAdapter extends TypeAdapter { - @override - final int typeId = 4; - - @override - Credentials read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return Credentials( - account: fields[0] as String, - password: fields[1] as String, - ); - } - - @override - void write(BinaryWriter writer, Credentials obj) { - writer - ..writeByte(2) - ..writeByte(0) - ..write(obj.account) - ..writeByte(1) - ..write(obj.password); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is CredentialsAdapter && runtimeType == other.runtimeType && typeId == other.typeId; -} diff --git a/lib/credentials/entity/login_status.dart b/lib/credentials/entity/login_status.dart deleted file mode 100644 index daf7914e7..000000000 --- a/lib/credentials/entity/login_status.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:sit/storage/hive/type_id.dart'; - -part 'login_status.g.dart'; - -@HiveType(typeId: CoreHiveType.loginStatus) -enum LoginStatus { - @HiveField(0) - never, - @HiveField(2) - offline, - @HiveField(3) - validated, - @HiveField(4) - everLogin, -} diff --git a/lib/credentials/entity/login_status.g.dart b/lib/credentials/entity/login_status.g.dart deleted file mode 100644 index 68f853d49..000000000 --- a/lib/credentials/entity/login_status.g.dart +++ /dev/null @@ -1,54 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'login_status.dart'; - -// ************************************************************************** -// TypeAdapterGenerator -// ************************************************************************** - -class LoginStatusAdapter extends TypeAdapter { - @override - final int typeId = 5; - - @override - LoginStatus read(BinaryReader reader) { - switch (reader.readByte()) { - case 0: - return LoginStatus.never; - case 2: - return LoginStatus.offline; - case 3: - return LoginStatus.validated; - case 4: - return LoginStatus.everLogin; - default: - return LoginStatus.never; - } - } - - @override - void write(BinaryWriter writer, LoginStatus obj) { - switch (obj) { - case LoginStatus.never: - writer.writeByte(0); - break; - case LoginStatus.offline: - writer.writeByte(2); - break; - case LoginStatus.validated: - writer.writeByte(3); - break; - case LoginStatus.everLogin: - writer.writeByte(4); - break; - } - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is LoginStatusAdapter && runtimeType == other.runtimeType && typeId == other.typeId; -} diff --git a/lib/credentials/entity/user_type.dart b/lib/credentials/entity/user_type.dart deleted file mode 100644 index a2228774d..000000000 --- a/lib/credentials/entity/user_type.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:sit/storage/hive/type_id.dart'; - -part 'user_type.g.dart'; - -typedef UserCapability = ({ - bool enableClass2nd, - bool enableExamArrange, - bool enableExamResult, -}); - -@HiveType(typeId: CoreHiveType.oaUserType) -enum OaUserType { - @HiveField(0) - undergraduate(( - enableClass2nd: true, - enableExamArrange: true, - enableExamResult: true, - )), - @HiveField(1) - postgraduate(( - enableClass2nd: false, - // postgraduates use a different SIS, so disable them temporarily - enableExamArrange: false, - enableExamResult: false, - )), - @HiveField(2) - other(( - enableClass2nd: false, - enableExamArrange: false, - enableExamResult: false, - )); - - final UserCapability capability; - - const OaUserType(this.capability); - - String l10n() => "OaUserType.$name".tr(); -} diff --git a/lib/credentials/entity/user_type.g.dart b/lib/credentials/entity/user_type.g.dart deleted file mode 100644 index 5d6bfd131..000000000 --- a/lib/credentials/entity/user_type.g.dart +++ /dev/null @@ -1,49 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'user_type.dart'; - -// ************************************************************************** -// TypeAdapterGenerator -// ************************************************************************** - -class OaUserTypeAdapter extends TypeAdapter { - @override - final int typeId = 6; - - @override - OaUserType read(BinaryReader reader) { - switch (reader.readByte()) { - case 0: - return OaUserType.undergraduate; - case 1: - return OaUserType.postgraduate; - case 2: - return OaUserType.other; - default: - return OaUserType.undergraduate; - } - } - - @override - void write(BinaryWriter writer, OaUserType obj) { - switch (obj) { - case OaUserType.undergraduate: - writer.writeByte(0); - break; - case OaUserType.postgraduate: - writer.writeByte(1); - break; - case OaUserType.other: - writer.writeByte(2); - break; - } - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is OaUserTypeAdapter && runtimeType == other.runtimeType && typeId == other.typeId; -} diff --git a/lib/credentials/error.dart b/lib/credentials/error.dart deleted file mode 100644 index fbdac19e5..000000000 --- a/lib/credentials/error.dart +++ /dev/null @@ -1,15 +0,0 @@ -enum CredentialsErrorType { - accountPassword, - captcha, - frozen, -} - -class CredentialsException implements Exception { - final CredentialsErrorType type; - final String? message; - - const CredentialsException({ - required this.type, - this.message, - }); -} diff --git a/lib/credentials/i18n.dart b/lib/credentials/i18n.dart deleted file mode 100644 index 26a53cfea..000000000 --- a/lib/credentials/i18n.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:sit/l10n/common.dart'; - -class CredentialsI18n with CommonI18nMixin { - const CredentialsI18n(); - - static const ns = "credentials"; - - String get studentId => "$ns.studentId".tr(); - - String get account => "$ns.account".tr(); - - String get password => "$ns.password".tr(); - - String get oaAccount => "$ns.oaAccount".tr(); - - String get oaPwd => "$ns.oaPwd".tr(); - - String get savedOaPwd => "$ns.savedOaPwd".tr(); - - String get savedOaPwdDesc => "$ns.savedOaPwdDesc".tr(); - - String get login => "$ns.login".tr(); - - String get forgotPwd => "$ns.forgotPwd".tr(); -} diff --git a/lib/credentials/init.dart b/lib/credentials/init.dart deleted file mode 100644 index 2abc6adb1..000000000 --- a/lib/credentials/init.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:sit/credentials/entity/credential.dart'; -import 'package:sit/credentials/entity/user_type.dart'; -import 'package:sit/design/adaptive/editor.dart'; - -import 'entity/login_status.dart'; -import 'storage/credential.dart'; - -class CredentialsInit { - static late CredentialStorage storage; - - static void init() { - storage = const CredentialStorage(); - Editor.registerEditor((ctx, desc, initial) => StringsEditor( - fields: [ - (name: "account", initial: initial.account), - (name: "password", initial: initial.password), - ], - title: desc, - ctor: (values) => Credentials(account: values[0], password: values[1]), - )); - EditorEx.registerEnumEditor(LoginStatus.values); - EditorEx.registerEnumEditor(OaUserType.values); - } -} diff --git a/lib/credentials/storage/credential.dart b/lib/credentials/storage/credential.dart deleted file mode 100644 index 22e0d0025..000000000 --- a/lib/credentials/storage/credential.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:hive_flutter/hive_flutter.dart'; -import 'package:sit/credentials/entity/user_type.dart'; -import 'package:sit/storage/hive/init.dart'; - -import '../entity/credential.dart'; -import '../entity/login_status.dart'; - -class _OaK { - static const ns = "/oa"; - static const credentials = "$ns/credentials"; - static const lastAuthTime = "$ns/lastAuthTime"; - static const loginStatus = "$ns/loginStatus"; - static const userType = "$ns/userType"; -} - -class _EmailK { - static const ns = "/eduEmail"; - static const credentials = "$ns/credentials"; -} - -class _LibraryK { - static const ns = "/library"; - static const credentials = "$ns/credentials"; -} - -class CredentialStorage { - Box get box => HiveInit.credentials; - - const CredentialStorage(); - - // OA - Credentials? get oaCredentials => box.get(_OaK.credentials); - - set oaCredentials(Credentials? newV) => box.put(_OaK.credentials, newV); - - DateTime? get oaLastAuthTime => box.get(_OaK.lastAuthTime); - - set oaLastAuthTime(DateTime? newV) => box.put(_OaK.lastAuthTime, newV); - - LoginStatus? get oaLoginStatus => box.get(_OaK.loginStatus); - - set oaLoginStatus(LoginStatus? newV) => box.put(_OaK.loginStatus, newV); - - OaUserType? get oaUserType => box.get(_OaK.userType); - - set oaUserType(OaUserType? newV) => box.put(_OaK.userType, newV); - - ValueListenable listenOaChange() => box.listenable(keys: [ - _OaK.credentials, - _OaK.lastAuthTime, - _OaK.loginStatus, - _OaK.userType, - ]); - - // Edu Email - Credentials? get eduEmailCredentials => box.get(_EmailK.credentials); - - set eduEmailCredentials(Credentials? newV) => box.put(_EmailK.credentials, newV); - - ValueListenable listenEduEmailChange() => box.listenable(keys: [ - _EmailK.credentials, - ]); - - // Library - Credentials? get libraryCredentials => box.get(_LibraryK.credentials); - - set libraryCredentials(Credentials? newV) => box.put(_LibraryK.credentials, newV); - - ValueListenable listenLibraryChange() => box.listenable(keys: [ - _LibraryK.credentials, - ]); -} diff --git a/lib/credentials/utils.dart b/lib/credentials/utils.dart deleted file mode 100644 index 38e1b985d..000000000 --- a/lib/credentials/utils.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:sit/credentials/entity/user_type.dart'; - -/// 本、专科生(10位学号) -final RegExp _reUndergraduateId = RegExp(r'^(\d{6}[YGHE\d]\d{3})$'); - -/// 研究生(9位学号) -final RegExp _rePostgraduateId = RegExp(r'^(\d{2}6\d{6})$'); - -/// 教师(4位工号) -final RegExp _reTeacherId = RegExp(r'^(\d{4})$'); - -/// [oaAccount] can be a student ID or a work number. -OaUserType? estimateOaUserType(String oaAccount) { - if (oaAccount.length == 10 && _reUndergraduateId.hasMatch(oaAccount.toUpperCase())) { - return OaUserType.undergraduate; - } else if (oaAccount.length == 9 && _rePostgraduateId.hasMatch(oaAccount)) { - return OaUserType.postgraduate; - } else if (oaAccount.length == 4 && _reTeacherId.hasMatch(oaAccount)) { - return OaUserType.other; - } - return null; -} diff --git a/lib/credentials/widgets/oa_scope.dart b/lib/credentials/widgets/oa_scope.dart deleted file mode 100644 index 6673036c6..000000000 --- a/lib/credentials/widgets/oa_scope.dart +++ /dev/null @@ -1,81 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:sit/credentials/entity/credential.dart'; -import 'package:sit/credentials/entity/user_type.dart'; - -import '../entity/login_status.dart'; -import '../init.dart'; - -extension OaAuthX on BuildContext { - OaAuth get auth => OaAuth.of(this); -} - -class OaAuthManager extends StatefulWidget { - final Widget child; - - const OaAuthManager({super.key, required this.child}); - - @override - State createState() => _OaAuthManagerState(); -} - -class _OaAuthManagerState extends State { - final onOaChanged = CredentialsInit.storage.listenOaChange(); - - @override - void initState() { - super.initState(); - onOaChanged.addListener(anyChange); - } - - @override - void dispose() { - onOaChanged.removeListener(anyChange); - super.dispose(); - } - - void anyChange() { - setState(() {}); - } - - @override - Widget build(BuildContext context) { - final storage = CredentialsInit.storage; - return OaAuth( - credentials: storage.oaCredentials, - lastAuthTime: storage.oaLastAuthTime, - loginStatus: storage.oaLoginStatus ?? LoginStatus.never, - userType: storage.oaUserType ?? OaUserType.other, - child: widget.child, - ); - } -} - -class OaAuth extends InheritedWidget { - final Credentials? credentials; - final DateTime? lastAuthTime; - final LoginStatus loginStatus; - final OaUserType userType; - - const OaAuth({ - super.key, - this.credentials, - this.lastAuthTime, - required this.loginStatus, - required this.userType, - required super.child, - }); - - static OaAuth of(BuildContext context) { - final OaAuth? result = context.dependOnInheritedWidgetOfExactType(); - assert(result != null, 'No AuthScope found in context'); - return result!; - } - - @override - bool updateShouldNotify(OaAuth oldWidget) { - return credentials != oldWidget.credentials || - lastAuthTime != oldWidget.lastAuthTime || - loginStatus != oldWidget.loginStatus || - userType != oldWidget.userType; - } -} diff --git a/lib/design/adaptive/dialog.dart b/lib/design/adaptive/dialog.dart deleted file mode 100644 index a58c8afdd..000000000 --- a/lib/design/adaptive/dialog.dart +++ /dev/null @@ -1,359 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; -import 'package:rettulf/rettulf.dart'; - -import 'foundation.dart'; - -typedef PickerActionWidgetBuilder = Widget Function(BuildContext context, int? selectedIndex); -typedef DualPickerActionWidgetBuilder = Widget Function(BuildContext context, int? selectedIndexA, int? selectedIndexB); - -extension DialogEx on BuildContext { - /// return: whether the button was hit - Future showTip({ - required String title, - required String desc, - required String ok, - bool highlight = false, - bool serious = false, - }) async { - return showAnyTip( - title: title, - make: (_) => desc.text(style: const TextStyle()), - ok: ok, - highlight: false, - serious: serious, - ); - } - - Future showAnyTip({ - required String title, - required WidgetBuilder make, - required String ok, - bool highlight = false, - bool serious = false, - }) async { - final dynamic confirm = await showAdaptiveDialog( - context: this, - builder: (ctx) => $Dialog$( - title: title, - serious: serious, - make: make, - primary: $Action$( - warning: highlight, - text: ok, - onPressed: () { - ctx.navigator.pop(true); - }, - )), - ); - return confirm == true; - } - - Future showRequest({ - required String title, - required String desc, - required String yes, - required String no, - bool highlight = false, - bool serious = false, - }) async { - return await showAnyRequest( - title: title, - make: (_) => desc.text(style: const TextStyle()), - yes: yes, - no: no, - highlight: highlight, - serious: serious, - ); - } - - Future showAnyRequest({ - required String title, - required WidgetBuilder make, - required String yes, - required String no, - bool highlight = false, - bool serious = false, - }) async { - return await showAdaptiveDialog( - context: this, - builder: (ctx) => $Dialog$( - title: title, - serious: serious, - make: make, - primary: $Action$( - warning: highlight, - text: yes, - onPressed: () { - ctx.navigator.pop(true); - }, - ), - secondary: $Action$( - text: no, - onPressed: () { - ctx.navigator.pop(false); - }, - ), - ), - ); - } - - Future showPicker({ - required int count, - String? ok, - bool Function(int? selected)? okEnabled, - double targetHeight = 240, - bool highlight = false, - FixedExtentScrollController? controller, - List? actions, - required IndexedWidgetBuilder make, - }) async { - final res = await navigator.push( - CupertinoModalPopupRoute( - builder: (ctx) => SoloPicker( - make: make, - count: count, - controller: controller, - ok: ok, - okEnabled: okEnabled, - targetHeight: targetHeight, - highlight: highlight, - actions: actions, - ), - ), - ); - if (res is int) { - return res; - } else { - assert(res == null, "return value is ${res.runtimeType} actually"); - return null; - } - } - - Future<(int, int)?> showDualPicker({ - required int countA, - required int countB, - String? ok, - bool Function(int? selectedA, int? selectedB)? okEnabled, - double targetHeight = 240, - bool highlight = false, - FixedExtentScrollController? controllerA, - FixedExtentScrollController? controllerB, - List? actions, - required IndexedWidgetBuilder makeA, - required IndexedWidgetBuilder makeB, - }) async { - final res = await navigator.push( - CupertinoModalPopupRoute( - builder: (ctx) => DualPicker( - makeA: makeA, - countA: countA, - countB: countB, - makeB: makeB, - controllerA: controllerA, - controllerB: controllerB, - ok: ok, - okEnabled: okEnabled, - targetHeight: targetHeight, - highlight: highlight, - actions: actions, - ), - ), - ); - if (res is (int, int)) { - return res; - } else { - assert(res == null, "return value is ${res.runtimeType} actually"); - return null; - } - } -} - -class SoloPicker extends StatefulWidget { - final int count; - final String? ok; - final bool Function(int? selected)? okEnabled; - final double targetHeight; - final bool highlight; - final FixedExtentScrollController? controller; - final List? actions; - final IndexedWidgetBuilder make; - - const SoloPicker({ - super.key, - required this.count, - this.ok, - this.okEnabled, - this.controller, - this.targetHeight = 240, - this.highlight = false, - this.actions, - required this.make, - }); - - @override - State createState() => _SoloPickerState(); -} - -class _SoloPickerState extends State { - late final $selected = ValueNotifier(widget.controller?.initialItem); - - @override - void dispose() { - $selected.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final ok = widget.ok; - return CupertinoActionSheet( - message: CupertinoPicker( - scrollController: widget.controller, - magnification: 1.22, - useMagnifier: true, - // This is called when selected item is changed. - onSelectedItemChanged: (int selectedItem) { - $selected.value = selectedItem; - }, - squeeze: 1.5, - itemExtent: 32.0, - children: List.generate(widget.count, (index) => widget.make(context, index)), - ).sized(h: widget.targetHeight), - actions: widget.actions - ?.map( - (e) => ValueListenableBuilder(valueListenable: $selected, builder: (ctx, value, child) => e(ctx, value))) - .toList(), - cancelButton: ok == null - ? null - : $selected >> - (ctx, selected) => PlatformTextButton( - onPressed: widget.okEnabled?.call(selected) ?? true - ? () { - Navigator.of(ctx).pop($selected.value); - } - : null, - child: ok.text(style: TextStyle(color: widget.highlight ? ctx.$red$ : null))), - ); - } -} - -class DualPicker extends StatefulWidget { - final FixedExtentScrollController? controllerA; - final FixedExtentScrollController? controllerB; - final int countA; - final int countB; - final String? ok; - final bool Function(int? selectedA, int? selectedB)? okEnabled; - final double targetHeight; - final bool highlight; - final List? actions; - final IndexedWidgetBuilder makeA; - final IndexedWidgetBuilder makeB; - - const DualPicker({ - super.key, - this.ok, - this.okEnabled, - this.actions, - this.highlight = false, - this.targetHeight = 240, - this.controllerA, - this.controllerB, - required this.makeA, - required this.countA, - required this.countB, - required this.makeB, - }); - - @override - State createState() => _DualPickerState(); -} - -class _DualPickerState extends State { - late final $selectedA = ValueNotifier(widget.controllerA?.initialItem ?? 0); - late final $selectedB = ValueNotifier(widget.controllerB?.initialItem ?? 0); - - @override - void dispose() { - $selectedA.dispose(); - $selectedB.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final ok = widget.ok; - return CupertinoActionSheet( - message: [ - CupertinoPicker( - scrollController: widget.controllerA, - magnification: 1.22, - useMagnifier: true, - // This is called when selected item is changed. - onSelectedItemChanged: (int selectedItem) { - $selectedA.value = selectedItem; - }, - squeeze: 1.5, - itemExtent: 32.0, - children: List.generate(widget.countA, (index) => widget.makeA(context, index)), - ).expanded(), - CupertinoPicker( - scrollController: widget.controllerB, - magnification: 1.22, - useMagnifier: true, - // This is called when selected item is changed. - onSelectedItemChanged: (int selectedItem) { - $selectedB.value = selectedItem; - }, - squeeze: 1.5, - itemExtent: 32.0, - children: List.generate(widget.countB, (index) => widget.makeB(context, index)), - ).expanded(), - ].row().sized(h: widget.targetHeight), - actions: widget.actions?.map((e) => $selectedA >> (ctx, a) => $selectedB >> (ctx, b) => e(ctx, a, b)).toList(), - cancelButton: ok == null - ? null - : $selectedA >> - (ctx, a) => - $selectedB >> - (ctx, b) => CupertinoButton( - onPressed: widget.okEnabled?.call(a, b) ?? true - ? () { - Navigator.of(ctx).pop(($selectedA.value, $selectedB.value)); - } - : null, - child: ok.text( - style: TextStyle( - color: widget.highlight ? ctx.$red$ : null, - ), - ), - ), - ); - } -} - -extension SnackBarX on BuildContext { - ScaffoldFeatureController showSnackBar({ - required Widget content, - Duration duration = const Duration(milliseconds: 1000), - SnackBarAction? action, - VoidCallback? onVisible, - SnackBarBehavior? behavior, - bool? showCloseIcon, - DismissDirection dismissDirection = DismissDirection.down, - }) { - final snackBar = SnackBar( - content: content, - duration: duration, - action: action, - onVisible: onVisible, - behavior: behavior, - showCloseIcon: showCloseIcon, - dismissDirection: dismissDirection, - ); - - return ScaffoldMessenger.of(this).showSnackBar(snackBar); - } -} diff --git a/lib/design/adaptive/editor.dart b/lib/design/adaptive/editor.dart deleted file mode 100644 index 9eb9e69f9..000000000 --- a/lib/design/adaptive/editor.dart +++ /dev/null @@ -1,515 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; -import 'package:sit/l10n/common.dart'; -import 'package:rettulf/rettulf.dart'; - -import 'foundation.dart'; -import 'dialog.dart'; - -const _i18n = CommonI18n(); - -typedef EditorBuilder = Widget Function(BuildContext ctx, String? desc, T initial); - -class Editor { - static final Map _customEditor = {}; - - static void registerEditor(EditorBuilder builder) { - _customEditor[T] = (ctx, desc, initial) => builder(ctx, desc, initial); - } - - static bool isSupport(dynamic test) { - return test is int || - test is String || - test is bool || - test is DateTime || - _customEditor.containsKey(test.runtimeType); - } - - static Future showAnyEditor(BuildContext ctx, dynamic initial, - {String? desc, bool readonlyIfNotSupport = true}) async { - if (initial is int) { - return await showIntEditor(ctx, desc: desc, initial: initial); - } else if (initial is String) { - return await showStringEditor(ctx, desc: desc, initial: initial); - } else if (initial is bool) { - return await showBoolEditor(ctx, desc: desc, initial: initial); - } else if (initial is DateTime) { - return await showDateTimeEditor( - ctx, - desc: desc, - initial: initial, - firstDate: DateTime(0), - lastDate: DateTime(9999), - ); - } else { - final customEditorBuilder = _customEditor[initial.runtimeType]; - if (customEditorBuilder != null) { - return await showAdaptiveDialog( - context: ctx, - builder: (ctx) => customEditorBuilder(ctx, desc, initial), - ); - } else { - if (readonlyIfNotSupport) { - return await showReadonlyEditor(ctx, desc: desc, initial: initial); - } else { - throw UnsupportedError("Editing $initial is not supported."); - } - } - } - } - - static Future showDateTimeEditor( - BuildContext ctx, { - String? desc, - required DateTime initial, - required DateTime firstDate, - required DateTime lastDate, - }) async { - final newValue = await showAdaptiveDialog( - context: ctx, - builder: (ctx) => DateTimeEditor( - initial: initial, - title: desc, - firstDate: firstDate, - lastDate: lastDate, - ), - ); - if (newValue == null) return null; - return newValue; - } - - static Future showBoolEditor( - BuildContext ctx, { - String? desc, - required bool initial, - }) async { - final newValue = await showAdaptiveDialog( - context: ctx, - builder: (ctx) => BoolEditor( - initial: initial, - desc: desc, - ), - ); - if (newValue == null) return null; - return newValue; - } - - static Future showStringEditor( - BuildContext ctx, { - String? desc, - required String initial, - }) async { - final newValue = await showAdaptiveDialog( - context: ctx, - builder: (ctx) => StringEditor( - initial: initial, - title: desc, - )); - if (newValue == null) return null; - return newValue; - } - - static Future showReadonlyEditor( - BuildContext ctx, { - String? desc, - required dynamic initial, - }) async { - await showDialog( - context: ctx, - builder: (ctx) => _readonlyEditor( - ctx, - (ctx) => SelectableText(initial.toString()), - title: desc, - ), - ); - } - - static Future showIntEditor( - BuildContext ctx, { - String? desc, - required int initial, - }) async { - final newValue = await showAdaptiveDialog( - context: ctx, - builder: (ctx) => IntEditor( - initial: initial, - title: desc, - ), - ); - if (newValue == null) return null; - return newValue; - } -} - -extension EditorEx on Editor { - static void registerEnumEditor(List values) { - Editor.registerEditor((ctx, desc, initial) => EnumEditor( - initial: initial, - title: desc, - values: values, - )); - } -} - -Widget _readonlyEditor(BuildContext ctx, WidgetBuilder make, {String? title}) { - return $Dialog$( - title: title, - primary: $Action$( - text: _i18n.close, - onPressed: () { - ctx.navigator.pop(false); - }), - make: (ctx) => make(ctx)); -} - -class EnumEditor extends StatefulWidget { - final T initial; - final List values; - final String? title; - - const EnumEditor({ - super.key, - required this.initial, - this.title, - required this.values, - }); - - @override - State> createState() => _EnumEditorState(); -} - -class _EnumEditorState extends State> { - late T current = widget.initial; - late final int initialIndex = max(widget.values.indexOf(widget.initial), 0); - - @override - Widget build(BuildContext context) { - return $Dialog$( - title: widget.title, - primary: $Action$( - text: _i18n.submit, - isDefault: true, - onPressed: () { - context.navigator.pop(widget.initial); - }), - secondary: $Action$( - text: _i18n.cancel, - onPressed: () { - context.navigator.pop(widget.initial); - }), - make: (ctx) => PlatformTextButton( - child: current.toString().text(), - onPressed: () async { - FixedExtentScrollController controller = FixedExtentScrollController(initialItem: initialIndex); - controller.addListener(() { - final selected = widget.values[controller.selectedItem]; - if (selected != current) { - setState(() { - current = selected; - }); - } - }); - await ctx.showPicker( - count: widget.values.length, - controller: controller, - make: (ctx, index) => widget.values[index].toString().text()); - controller.dispose(); - }, - ), - ); - } -} - -class DateTimeEditor extends StatefulWidget { - final DateTime initial; - final String? title; - final DateTime firstDate; - final DateTime lastDate; - - const DateTimeEditor({ - super.key, - required this.initial, - this.title, - required this.firstDate, - required this.lastDate, - }); - - @override - State createState() => _DateTimeEditorState(); -} - -class _DateTimeEditorState extends State { - late DateTime current = widget.initial; - - @override - Widget build(BuildContext context) { - return $Dialog$( - title: widget.title, - primary: $Action$( - text: _i18n.submit, - isDefault: true, - onPressed: () { - context.navigator.pop(current); - }), - secondary: $Action$( - text: _i18n.cancel, - onPressed: () { - context.navigator.pop(widget.initial); - }), - make: (ctx) => PlatformTextButton( - child: current.toString().text(), - onPressed: () async { - final newDate = await showDatePicker( - context: context, - initialDate: widget.initial, - firstDate: widget.firstDate, - lastDate: widget.lastDate, - ); - if (newDate != null) { - setState(() { - current = newDate; - }); - } - }, - ), - ); - } -} - -class IntEditor extends StatefulWidget { - final int initial; - final String? title; - - const IntEditor({super.key, required this.initial, this.title}); - - @override - State createState() => _IntEditorState(); -} - -class _IntEditorState extends State { - late TextEditingController controller; - late int value = widget.initial; - - @override - void initState() { - super.initState(); - controller = TextEditingController(text: widget.initial.toString()); - } - - @override - void dispose() { - controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return $Dialog$( - title: widget.title, - primary: $Action$( - text: _i18n.submit, - isDefault: true, - onPressed: () { - context.navigator.pop(value); - }), - secondary: $Action$( - text: _i18n.cancel, - onPressed: () { - context.navigator.pop(widget.initial); - }), - make: (ctx) => buildBody(ctx)); - } - - Widget buildBody(BuildContext ctx) { - return Row( - children: [ - PlatformTextButton( - child: const Icon(Icons.remove), - onPressed: () { - setState(() { - value--; - controller.text = value.toString(); - }); - }, - ), - $TextField$( - controller: controller, - keyboardType: TextInputType.number, - inputFormatters: [ - FilteringTextInputFormatter.allow(RegExp(r'\d')), - ], - onChanged: (v) { - final newV = int.tryParse(v); - if (newV != null) { - setState(() { - value = newV; - }); - } - }, - ).sized(w: 100), - PlatformTextButton( - child: const Icon(Icons.add), - onPressed: () { - setState(() { - value++; - controller.text = value.toString(); - }); - }, - ), - ], - ); - } -} - -class BoolEditor extends StatefulWidget { - final bool initial; - final String? desc; - - const BoolEditor({super.key, required this.initial, this.desc}); - - @override - State createState() => _BoolEditorState(); -} - -class _BoolEditorState extends State { - late bool value = widget.initial; - - @override - Widget build(BuildContext context) { - return $Dialog$( - primary: $Action$( - text: _i18n.submit, - isDefault: true, - onPressed: () { - context.navigator.pop(value); - }), - secondary: $Action$( - text: _i18n.cancel, - onPressed: () { - context.navigator.pop(widget.initial); - }), - make: (ctx) => $ListTile$( - title: (widget.desc ?? "").text(), - trailing: Switch.adaptive( - value: value, - onChanged: (newValue) { - setState(() { - value = newValue; - }); - }, - ))); - } -} - -class StringEditor extends StatefulWidget { - final String initial; - final String? title; - - const StringEditor({required this.initial, this.title}); - - @override - State createState() => _StringEditorState(); -} - -class _StringEditorState extends State { - late TextEditingController controller; - - @override - void initState() { - super.initState(); - controller = TextEditingController(text: widget.initial); - } - - @override - void dispose() { - controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final lines = context.isPortrait ? widget.initial.length ~/ 30 + 1 : widget.initial.length ~/ 100 + 1; - return $Dialog$( - title: widget.title, - primary: $Action$( - text: _i18n.submit, - isDefault: true, - onPressed: () { - context.navigator.pop(controller.text); - }), - secondary: $Action$( - text: _i18n.cancel, - onPressed: () { - context.navigator.pop(widget.initial); - }), - make: (ctx) => $TextField$( - maxLines: lines, - controller: controller, - )); - } -} - -class StringsEditor extends StatefulWidget { - final List<({String name, String initial})> fields; - final String? title; - final T Function(List values) ctor; - - const StringsEditor({ - super.key, - required this.fields, - required this.title, - required this.ctor, - }); - - @override - State createState() => _StringsEditorState(); -} - -class _StringsEditorState extends State { - late List<({String name, TextEditingController $value})> $values; - - late TextEditingController $password; - - @override - void initState() { - super.initState(); - $values = widget.fields.map((e) => (name: e.name, $value: TextEditingController(text: e.initial))).toList(); - } - - @override - void dispose() { - for (final (name: _, :$value) in $values) { - $value.dispose(); - } - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return $Dialog$( - title: widget.title, - make: (ctx) => $values.map((e) => buildField(e.name, e.$value)).toList().column(mas: MainAxisSize.min), - primary: $Action$( - text: _i18n.submit, - onPressed: () { - context.navigator.pop(widget.ctor($values.map((e) => e.$value.text).toList())); - }), - secondary: $Action$( - text: _i18n.cancel, - onPressed: () { - context.navigator.pop(); - }), - ); - } - - Widget buildField(String fieldName, TextEditingController controller) { - return $TextField$( - controller: controller, - textInputAction: TextInputAction.next, - labelText: fieldName, - ).padV(1); - } -} diff --git a/lib/design/adaptive/foundation.dart b/lib/design/adaptive/foundation.dart deleted file mode 100644 index 432311619..000000000 --- a/lib/design/adaptive/foundation.dart +++ /dev/null @@ -1,274 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; -import 'package:rettulf/rettulf.dart'; - -import 'multiplatform.dart'; - -const _kDialogAlpha = 0.89; - -extension $BuildContextEx$ on BuildContext { - Future show$Sheet$( - WidgetBuilder builder, { - bool dismissible = true, - }) async { - if (isCupertino) { - return await showCupertinoModalBottomSheet( - context: this, - builder: builder, - animationCurve: Curves.fastEaseInToSlowEaseOut, - isDismissible: dismissible, - ); - } else { - // dismissible not working with CustomScrollView - // see https://github.com/flutter/flutter/issues/36283 - return await showModalBottomSheet( - context: this, - builder: builder, - isDismissible: dismissible, - isScrollControlled: true, - useSafeArea: true, - // It's a workaround - showDragHandle: true, - ); - } - } -} - -class $Action$ { - final String text; - final bool isDefault; - final bool warning; - final VoidCallback? onPressed; - - const $Action$({ - required this.text, - this.onPressed, - this.isDefault = false, - this.warning = false, - }); -} - -class $Dialog$ extends StatelessWidget { - final String? title; - final $Action$ primary; - final $Action$? secondary; - - /// Highlight the title - final bool serious; - final WidgetBuilder make; - - const $Dialog$({ - super.key, - this.title, - required this.primary, - required this.make, - this.secondary, - this.serious = false, - }); - - @override - Widget build(BuildContext context) { - Widget dialog; - final second = secondary; - if (isCupertino) { - dialog = CupertinoAlertDialog( - title: title?.text(style: TextStyle(fontWeight: FontWeight.w600, color: serious ? context.$red$ : null)), - content: make(context), - actions: [ - if (second != null) - CupertinoDialogAction( - isDestructiveAction: second.warning, - isDefaultAction: second.isDefault, - onPressed: () { - second.onPressed?.call(); - }, - child: second.text.text(), - ), - CupertinoDialogAction( - isDestructiveAction: primary.warning, - isDefaultAction: primary.isDefault, - onPressed: () { - primary.onPressed?.call(); - }, - child: primary.text.text(), - ) - ], - ); - } else { - // For other platform - dialog = AlertDialog( - backgroundColor: context.theme.dialogBackgroundColor.withOpacity(_kDialogAlpha), - title: title?.text(style: TextStyle(fontWeight: FontWeight.w600, color: serious ? context.$red$ : null)), - content: make(context), - actions: [ - CupertinoButton( - onPressed: () { - primary.onPressed?.call(); - }, - child: primary.text.text( - style: TextStyle( - color: primary.warning ? context.$red$ : null, - fontWeight: primary.isDefault ? FontWeight.w600 : null, - ), - )), - if (second != null) - CupertinoButton( - onPressed: () { - second.onPressed?.call(); - }, - child: second.text.text( - style: TextStyle( - color: second.warning ? context.$red$ : null, - fontWeight: second.isDefault ? FontWeight.w600 : null, - ), - ), - ) - ], - actionsAlignment: MainAxisAlignment.spaceEvenly, - ); - } - return dialog; - } -} - -class $ListTile$ extends StatelessWidget { - final Widget title; - final Widget? subtitle; - final Widget? leading; - final Widget? trailing; - final FutureOr Function()? onTap; - - const $ListTile$({ - super.key, - required this.title, - this.subtitle, - this.leading, - this.trailing, - this.onTap, - }); - - @override - Widget build(BuildContext context) { - if (isCupertino) { - return CupertinoListTile( - title: title, - subtitle: subtitle, - leading: leading, - trailing: trailing, - onTap: onTap, - ); - } else { - return ListTile( - title: title, - subtitle: subtitle, - leading: leading, - trailing: trailing, - onTap: onTap, - ); - } - } -} - -class $TextField$ extends StatelessWidget { - final TextEditingController? controller; - final String? placeholder; - final Widget? prefixIcon; - final Widget? suffixIcon; - final bool autofocus; - - /// On Cupertino, it's a candidate of placeholder. - /// On Material, it's the [InputDecoration.labelText] - final String? labelText; - final TextInputAction? textInputAction; - final ValueChanged? onSubmit; - final Iterable? autofillHints; - final TextInputType? keyboardType; - final int? maxLines; - final List? inputFormatters; - final ValueChanged? onChanged; - - const $TextField$({ - super.key, - this.controller, - this.autofocus = false, - this.placeholder, - this.labelText, - this.autofillHints, - this.textInputAction, - this.prefixIcon, - this.suffixIcon, - this.onSubmit, - this.maxLines, - this.keyboardType, - this.inputFormatters, - this.onChanged, - }); - - @override - Widget build(BuildContext context) { - if (isCupertino) { - return CupertinoTextField( - controller: controller, - autofocus: autofocus, - placeholder: placeholder ?? labelText, - textInputAction: textInputAction, - prefix: prefixIcon, - suffix: suffixIcon, - autofillHints: autofillHints, - onSubmitted: onSubmit, - maxLines: maxLines, - onChanged: onChanged, - keyboardType: keyboardType, - inputFormatters: inputFormatters, - decoration: const BoxDecoration( - color: CupertinoDynamicColor.withBrightness( - color: CupertinoColors.white, - darkColor: CupertinoColors.darkBackgroundGray, - ), - border: _kDefaultRoundedBorder, - borderRadius: BorderRadius.all(Radius.circular(8.0)), - ), - style: CupertinoTheme.of(context).textTheme.textStyle, - ); - } else { - return TextFormField( - controller: controller, - autofocus: autofocus, - textInputAction: textInputAction, - maxLines: maxLines, - keyboardType: keyboardType, - inputFormatters: inputFormatters, - onChanged: onChanged, - decoration: InputDecoration( - hintText: placeholder, - icon: prefixIcon, - labelText: labelText, - suffixIcon: suffixIcon, - ), - onFieldSubmitted: onSubmit, - ); - } - } -} - -const BorderSide _kDefaultRoundedBorderSide = BorderSide( - color: CupertinoDynamicColor.withBrightness( - color: Color(0x33000000), - darkColor: Color(0xAAA0A0A0), - ), - width: 1.0, -); -const Border _kDefaultRoundedBorder = Border( - top: _kDefaultRoundedBorderSide, - bottom: _kDefaultRoundedBorderSide, - left: _kDefaultRoundedBorderSide, - right: _kDefaultRoundedBorderSide, -); - -extension ColorEx on BuildContext { - Color get $red$ => isCupertino ? CupertinoDynamicColor.resolve(CupertinoColors.systemRed, this) : Colors.redAccent; -} diff --git a/lib/design/adaptive/multiplatform.dart b/lib/design/adaptive/multiplatform.dart deleted file mode 100644 index 55e4dc4d6..000000000 --- a/lib/design/adaptive/multiplatform.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:sit/r.dart'; -import 'package:universal_platform/universal_platform.dart'; - -bool get isCupertino => R.debugCupertino || UniversalPlatform.isIOS || UniversalPlatform.isMacOS; - -extension ShareX on BuildContext { - Rect? getSharePositionOrigin() { - final box = findRenderObject() as RenderBox?; - final sharePositionOrigin = box == null ? null : box.localToGlobal(Offset.zero) & box.size; - if (UniversalPlatform.isIOS || UniversalPlatform.isMacOS) { - assert(sharePositionOrigin != null, "sharePositionOrigin should be nonnull on iPad and macOS"); - } - return sharePositionOrigin; - } -} diff --git a/lib/design/adaptive/swipe.dart b/lib/design/adaptive/swipe.dart deleted file mode 100644 index c17c3ab07..000000000 --- a/lib/design/adaptive/swipe.dart +++ /dev/null @@ -1,86 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_swipe_action_cell/flutter_swipe_action_cell.dart'; -import 'package:rettulf/rettulf.dart'; - -import 'multiplatform.dart'; - -class SwipeToDismissAction { - final VoidCallback action; - final String label; - - SwipeToDismissAction({ - required this.action, - required this.label, - }); -} - -class SwipeToDismiss extends StatelessWidget { - final Widget child; - final SwipeToDismissAction? left; - final SwipeToDismissAction? right; - final Key childKey; - - const SwipeToDismiss({ - super.key, - required this.childKey, - required this.child, - this.left, - this.right, - }); - - @override - Widget build(BuildContext context) { - final left = this.left; - final right = this.right; - if (isCupertino) { - return SwipeActionCell( - key: childKey, - trailingActions: right == null - ? null - : [ - SwipeAction( - title: right.label, - style: context.textTheme.titleSmall ?? const TextStyle(), - performsFirstActionWithFullSwipe: true, - onTap: (CompletionHandler handler) async { - await handler(true); - right.action(); - }, - color: Colors.red, - ), - ], - leadingActions: left == null - ? null - : [ - SwipeAction( - title: left.label, - style: context.textTheme.titleSmall ?? const TextStyle(), - performsFirstActionWithFullSwipe: true, - onTap: (CompletionHandler handler) async { - await handler(true); - left.action(); - }, - color: Colors.red, - ), - ], - child: child, - ); - } else { - return Dismissible( - direction: DismissDirection.endToStart, - key: childKey, - onDismissed: (dir) async { - if (dir == DismissDirection.startToEnd) { - await HapticFeedback.heavyImpact(); - left?.action(); - } else if (dir == DismissDirection.endToStart) { - await HapticFeedback.heavyImpact(); - right?.action(); - } - }, - child: child, - ); - } - } -} diff --git a/lib/design/animation/animated.dart b/lib/design/animation/animated.dart deleted file mode 100644 index fc312fda1..000000000 --- a/lib/design/animation/animated.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:flutter/widgets.dart'; - -extension AnimatedEx on Widget { - Widget animatedSwitched({ - Duration duration = const Duration(milliseconds: 500), - Curve? switchInCurve, - Curve? switchOutCurve, - }) => - AnimatedSwitcher( - switchInCurve: switchInCurve ?? Curves.linear, - switchOutCurve: switchOutCurve ?? Curves.linear, - duration: duration, - child: this, - ); - - Widget animatedSized({ - Duration duration = const Duration(milliseconds: 500), - Alignment align = Alignment.center, - Curve? curve, - }) => - AnimatedSize( - curve: curve ?? Curves.linear, - duration: duration, - alignment: align, - child: this, - ); -} diff --git a/lib/design/animation/button.dart b/lib/design/animation/button.dart deleted file mode 100644 index 54de3d3d3..000000000 --- a/lib/design/animation/button.dart +++ /dev/null @@ -1,209 +0,0 @@ -// Steal from "https://github.com/ThomasEcalle/bouncing_widget" -import 'package:flutter/material.dart'; - -class Bouncing extends StatefulWidget { - /// Child that will receive the bouncing animation - final Widget child; - - /// Callback on click event - final VoidCallback? onPressed; - - /// Scale factor - /// < 0 => the bouncing will be reversed and widget will grow - /// 1 => default value - /// > 1 => increase the bouncing effect - final double scaleFactor; - - /// Animation duration - final Duration duration; - - /// Whether the animation can revers or not - final bool stayOnBottom; - - /// BouncingWidget constructor - const Bouncing({ - super.key, - required this.child, - this.onPressed, - this.scaleFactor = 1, - this.duration = const Duration(milliseconds: 200), - this.stayOnBottom = false, - }); - - @override - State createState() => _BouncingState(); -} - -class _BouncingState extends State with SingleTickerProviderStateMixin { - /// Animation controller - late AnimationController _controller; - - /// View scale used in order to make the bouncing animation - late double _scale; - - /// Key of the given child used to get its size and position whenever we need - final GlobalKey _childKey = GlobalKey(); - - /// If the touch position is outside or not of the given child - bool _isOutside = false; - - /// Simple getter on widget's child - Widget get child => widget.child; - - /// Simple getter on widget's onPressed callback - VoidCallback? get onPressed => widget.onPressed; - - /// Simple getter on widget's scaleFactor - double get scaleFactor => widget.scaleFactor; - - /// Simple getter on widget's animation duration - Duration get duration => widget.duration; - - /// Simple getter on widget's stayOnBottom boolean - bool get _stayOnBottom => widget.stayOnBottom; - - /// We instantiate the animation controller - /// The idea is to call setState() each time the controller's - /// value changes - @override - void initState() { - _controller = AnimationController( - vsync: this, - duration: duration, - lowerBound: 0.0, - upperBound: 0.1, - )..addListener(() { - setState(() {}); - }); - - super.initState(); - } - - @override - void didUpdateWidget(Bouncing oldWidget) { - if (oldWidget.stayOnBottom != _stayOnBottom) { - if (!_stayOnBottom) { - _reverseAnimation(); - } - } - super.didUpdateWidget(oldWidget); - } - - /// Dispose the animation controller - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - /// Each time the [_controller]'s value changes, build() will be called - /// We just have to calculate the appropriate scale from the controller value - /// and pass it to our Transform.scale widget - @override - Widget build(BuildContext context) { - _scale = 1 - (_controller.value * scaleFactor); - return GestureDetector( - onTapDown: _onTapDown, - onTapUp: _onTapUp, - onLongPressEnd: (details) => _onLongPressEnd(details, context), - onHorizontalDragEnd: _onDragEnd, - onVerticalDragEnd: _onDragEnd, - onHorizontalDragUpdate: (details) => _onDragUpdate(details, context), - onVerticalDragUpdate: (details) => _onDragUpdate(details, context), - child: Transform.scale( - key: _childKey, - scale: _scale, - child: child, - ), - ); - } - - /// Simple method called when we need to notify the user of a press event - void _triggerOnPressed() { - onPressed?.call(); - } - - /// We start the animation - void _onTapDown(TapDownDetails details) { - _controller.forward(); - } - - /// We reverse the animation and notify the user of a press event - void _onTapUp(TapUpDetails details) { - if (!_stayOnBottom) { - Future.delayed(duration, () { - _reverseAnimation(); - }); - } - - _triggerOnPressed(); - } - - /// Here we are listening on each change when drag event is triggered - /// We must keep the [_isOutside] value updated in order to use it later - void _onDragUpdate(DragUpdateDetails details, BuildContext context) { - final Offset touchPosition = details.globalPosition; - _isOutside = _isOutsideChildBox(touchPosition); - } - - /// When this callback is triggered, we reverse the animation - /// If the touch position is inside the children renderBox, we notify the user of a press event - void _onLongPressEnd(LongPressEndDetails details, BuildContext context) { - final Offset touchPosition = details.globalPosition; - - if (!_isOutsideChildBox(touchPosition)) { - _triggerOnPressed(); - } - - _reverseAnimation(); - } - - /// When this callback is triggered, we reverse the animation - /// As we do not have position details, we use the [_isOutside] field to know - /// if we need to notify the user of a press event - void _onDragEnd(DragEndDetails details) { - if (!_isOutside) { - _triggerOnPressed(); - } - _reverseAnimation(); - } - - void _reverseAnimation() { - if (mounted) { - _controller.reverse(); - } - } - - /// Method called when we need to now if a specific touch position is inside the given - /// child render box - bool _isOutsideChildBox(Offset touchPosition) { - final RenderBox? childRenderBox = _childKey.currentContext?.findRenderObject() as RenderBox?; - if (childRenderBox == null) { - return true; - } - final Size childSize = childRenderBox.size; - final Offset childPosition = childRenderBox.localToGlobal(Offset.zero); - - return (touchPosition.dx < childPosition.dx || - touchPosition.dx > childPosition.dx + childSize.width || - touchPosition.dy < childPosition.dy || - touchPosition.dy > childPosition.dy + childSize.height); - } -} - -extension BouncingEx on Widget { - Bouncing withBouncing({ - VoidCallback? onTap, - Key? key, - double scale = 1, - Duration duration = const Duration(milliseconds: 200), - bool stayOnBottom = false, - }) => - Bouncing( - onPressed: onTap, - scaleFactor: scale, - duration: duration, - stayOnBottom: stayOnBottom, - child: this, - ); -} diff --git a/lib/design/animation/number.dart b/lib/design/animation/number.dart deleted file mode 100644 index 4cc9f6d3c..000000000 --- a/lib/design/animation/number.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:flutter/widgets.dart'; - -class AnimatedNumber extends StatelessWidget { - final Duration duration; - final double value; - final Widget Function(BuildContext context, double value) builder; - - const AnimatedNumber({ - super.key, - required this.value, - required this.builder, - this.duration = const Duration(milliseconds: 300), - }); - - @override - Widget build(BuildContext context) { - return TweenAnimationBuilder( - tween: Tween(begin: value, end: value), - duration: duration, - builder: (ctx, value, _) => builder(ctx, value), - ); - } -} diff --git a/lib/design/animation/progress.dart b/lib/design/animation/progress.dart deleted file mode 100644 index 6acc34f77..000000000 --- a/lib/design/animation/progress.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:flutter/material.dart'; - -class ProgressWatcher { - double _progress; - final void Function(double progress)? callback; - - ProgressWatcher({ - double initial = 0.0, - this.callback, - }) : _progress = initial; - - double get value => _progress; - - set value(double newV) { - _progress = newV; - callback?.call(newV); - } -} - -class AnimatedProgressBar extends StatelessWidget { - final double value; - final Duration duration; - - const AnimatedProgressBar({ - super.key, - required this.value, - this.duration = const Duration(milliseconds: 250), - }); - - @override - Widget build(BuildContext context) { - return TweenAnimationBuilder( - duration: duration, - curve: Curves.easeInOut, - tween: Tween( - begin: 0, - end: value, - ), - builder: (context, value, _) => LinearProgressIndicator(value: value), - ); - } -} diff --git a/lib/design/dash_decoration.dart b/lib/design/dash_decoration.dart deleted file mode 100644 index 1807e4689..000000000 --- a/lib/design/dash_decoration.dart +++ /dev/null @@ -1,115 +0,0 @@ -import 'dart:ui'; -import 'package:flutter/widgets.dart'; - -class DashDecoration extends Decoration { - final Set borders; - final Shape shape; - final Color color; - final BorderRadius? borderRadius; - final List dash; - final double strokeWidth; - - const DashDecoration({ - this.borders = const {}, - this.shape = Shape.line, - this.color = const Color(0xFF9E9E9E), - this.borderRadius, - this.dash = const [5, 5], - this.strokeWidth = 1, - }); - - @override - BoxPainter createBoxPainter([VoidCallback? onChanged]) { - return _DashPainter( - shape: shape, - borders: borders, - color: color, - borderRadius: borderRadius, - dash: dash, - strokeWidth: strokeWidth, - ); - } -} - -class _DashPainter extends BoxPainter { - final Set borders; - final Shape shape; - final Color color; - final BorderRadius borderRadius; - final List dash; - final double strokeWidth; - - _DashPainter({ - required this.shape, - required this.borders, - required this.color, - BorderRadius? borderRadius, - required this.dash, - required this.strokeWidth, - }) : borderRadius = borderRadius ?? BorderRadius.circular(0); - - @override - void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) { - Path outPath = Path(); - if (shape == Shape.line) { - for (final border in borders) { - if (border == LinePosition.left) { - outPath.moveTo(offset.dx, offset.dy); - outPath.lineTo(offset.dx, offset.dy + configuration.size!.height); - } else if (border == LinePosition.top) { - outPath.moveTo(offset.dx, offset.dy); - outPath.lineTo(offset.dx + configuration.size!.width, offset.dy); - } else if (border == LinePosition.right) { - outPath.moveTo(offset.dx + configuration.size!.width, offset.dy); - outPath.lineTo(offset.dx + configuration.size!.width, offset.dy + configuration.size!.height); - } else { - outPath.moveTo(offset.dx, offset.dy + configuration.size!.height); - outPath.lineTo(offset.dx + configuration.size!.width, offset.dy + configuration.size!.height); - } - } - } else if (shape == Shape.box) { - RRect rect = RRect.fromLTRBAndCorners( - offset.dx, - offset.dy, - offset.dx + configuration.size!.width, - offset.dy + configuration.size!.height, - bottomLeft: borderRadius.bottomLeft, - bottomRight: borderRadius.bottomRight, - topLeft: borderRadius.topLeft, - topRight: borderRadius.topRight, - ); - outPath.addRRect(rect); - } else if (shape == Shape.circle) { - outPath.addOval(Rect.fromLTWH(offset.dx, offset.dy, configuration.size!.width, configuration.size!.height)); - } - - PathMetrics metrics = outPath.computeMetrics(forceClosed: false); - Path drawPath = Path(); - - for (PathMetric me in metrics) { - double totalLength = me.length; - int index = -1; - - for (double start = 0; start < totalLength;) { - double to = start + dash[(++index) % dash.length]; - to = to > totalLength ? totalLength : to; - bool isEven = index % 2 == 0; - if (isEven) { - drawPath.addPath(me.extractPath(start, to, startWithMoveTo: true), Offset.zero); - } - start = to; - } - } - - canvas.drawPath( - drawPath, - Paint() - ..color = color - ..style = PaintingStyle.stroke - ..strokeWidth = strokeWidth); - } -} - -enum LinePosition { left, top, right, bottom } - -enum Shape { line, box, circle } diff --git a/lib/design/widgets/app.dart b/lib/design/widgets/app.dart deleted file mode 100644 index bfc3b3bc8..000000000 --- a/lib/design/widgets/app.dart +++ /dev/null @@ -1,68 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:rettulf/rettulf.dart'; - -import 'card.dart'; - -class AppCard extends StatelessWidget { - /// [SizedBox] by default. - final Widget? view; - final Widget? title; - final Widget? subtitle; - final List? leftActions; - - /// 12 by default. - final double? leftActionsSpacing; - final List? rightActions; - - /// 0 by default - final double? rightActionsSpacing; - - const AppCard({ - super.key, - this.view, - this.title, - this.subtitle, - this.leftActions, - this.rightActions, - this.leftActionsSpacing, - this.rightActionsSpacing, - }); - - @override - Widget build(BuildContext context) { - final leftActions = this.leftActions ?? const []; - final rightActions = this.rightActions ?? const []; - final textTheme = context.textTheme; - return FilledCard( - child: [ - Theme( - data: context.theme.copyWith( - cardTheme: context.theme.cardTheme.copyWith( - // in light mode, cards look in a lower level. - elevation: context.isDarkMode ? 4 : 2, - ), - ), - child: AnimatedSize( - duration: Durations.long2, - alignment: Alignment.topCenter, - curve: Curves.fastEaseInToSlowEaseOut, - child: view ?? const SizedBox(), - ), - ), - ListTile( - titleTextStyle: textTheme.titleLarge, - title: title, - subtitleTextStyle: textTheme.bodyLarge?.copyWith(color: context.colorScheme.onSurfaceVariant), - subtitle: subtitle, - ), - OverflowBar( - alignment: MainAxisAlignment.spaceBetween, - children: [ - leftActions.wrap(spacing: leftActionsSpacing ?? 8), - rightActions.wrap(spacing: rightActionsSpacing ?? 0), - ], - ).padOnly(l: 16, b: rightActions.isEmpty ? 12 : 8, r: 16), - ].column(), - ); - } -} diff --git a/lib/design/widgets/button.dart b/lib/design/widgets/button.dart deleted file mode 100644 index 44004e6b4..000000000 --- a/lib/design/widgets/button.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:flutter/material.dart'; - -class PlainExtendedButton extends StatelessWidget { - final Widget label; - final Widget? icon; - final Object? hero; - final VoidCallback? tap; - - const PlainExtendedButton({super.key, this.hero, required this.label, this.icon, this.tap}); - - @override - Widget build(BuildContext context) { - return FloatingActionButton.extended( - heroTag: hero, - icon: icon, - backgroundColor: Colors.transparent, - hoverColor: Colors.transparent, - elevation: 0, - highlightElevation: 0, - label: label, - onPressed: tap, - ); - } -} - -class PlainButton extends StatelessWidget { - final Widget? label; - final Widget? child; - final Object? hero; - final VoidCallback? tap; - - const PlainButton({super.key, this.hero, this.label, this.child, this.tap}); - - @override - Widget build(BuildContext context) { - return FloatingActionButton( - heroTag: hero, - backgroundColor: Colors.transparent, - hoverColor: Colors.transparent, - elevation: 0, - highlightElevation: 0, - onPressed: tap, - child: child, - ); - } -} diff --git a/lib/design/widgets/capture.dart b/lib/design/widgets/capture.dart deleted file mode 100644 index 1a56d06ac..000000000 --- a/lib/design/widgets/capture.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'dart:typed_data'; -import 'package:flutter/rendering.dart'; -import 'dart:ui' as ui; - -class WidgetCaptureController { - final GlobalKey containerKey; - - const WidgetCaptureController({ - required this.containerKey, - }); - - /// to capture widget to image by GlobalKey in RenderRepaintBoundary - Future capture() async { - try { - /// boundary widget by GlobalKey - final boundary = containerKey.currentContext?.findRenderObject() as RenderRepaintBoundary?; - - /// convert boundary to image - final image = await boundary?.toImage(pixelRatio: 6); - - /// set ImageByteFormat - final byteData = await image?.toByteData(format: ui.ImageByteFormat.png); - final pngBytes = byteData?.buffer.asUint8List(); - return pngBytes; - } catch (e) { - rethrow; - } - } -} - -class WidgetCapture extends StatelessWidget { - final Widget? child; - final WidgetCaptureController controller; - - const WidgetCapture({ - super.key, - required this.controller, - required this.child, - }); - - @override - Widget build(BuildContext context) { - /// to capture widget to image by GlobalKey in RepaintBoundary - return RepaintBoundary( - key: controller.containerKey, - child: child, - ); - } -} diff --git a/lib/design/widgets/card.dart b/lib/design/widgets/card.dart deleted file mode 100644 index 18b52c57f..000000000 --- a/lib/design/widgets/card.dart +++ /dev/null @@ -1,117 +0,0 @@ -import 'package:flutter/material.dart'; - -class OutlinedCard extends StatelessWidget { - final Widget? child; - final EdgeInsetsGeometry? margin; - final Clip? clip; - final Color? color; - - const OutlinedCard({ - super.key, - this.child, - this.margin, - this.color, - this.clip, - }); - - @override - Widget build(BuildContext context) { - return Card( - margin: margin, - clipBehavior: clip, - elevation: 0, - shape: RoundedRectangleBorder( - side: BorderSide( - color: color ?? Theme.of(context).colorScheme.outline, - ), - borderRadius: const BorderRadius.all(Radius.circular(12)), - ), - child: child, - ); - } -} - -class FilledCard extends StatelessWidget { - final Widget? child; - final EdgeInsetsGeometry? margin; - final Color? color; - final Clip? clip; - - const FilledCard({ - super.key, - this.child, - this.margin, - this.color, - this.clip, - }); - - @override - Widget build(BuildContext context) { - return Card( - elevation: 0, - clipBehavior: clip, - color: color ?? Theme.of(context).colorScheme.surfaceVariant, - margin: margin, - child: child, - ); - } -} - -extension WidgetCardX on Widget { - Widget inOutlinedCard({ - Clip? clip, - }) { - return Builder( - builder: (context) => Card( - elevation: 0, - clipBehavior: clip, - shape: RoundedRectangleBorder( - side: BorderSide( - color: Theme.of(context).colorScheme.outline, - ), - borderRadius: const BorderRadius.all(Radius.circular(12)), - ), - child: this, - ), - ); - } - - Widget inFilledCard({ - Clip? clip, - }) { - return Builder( - builder: (context) => Card( - elevation: 0, - clipBehavior: clip, - color: Theme.of(context).colorScheme.surfaceVariant, - child: this, - ), - ); - } - - Widget inAnyCard({ - Clip? clip, - CardType type = CardType.plain, - }) { - return switch (type) { - CardType.plain => Card( - clipBehavior: clip, - child: this, - ), - CardType.filled => FilledCard( - clip: clip, - child: this, - ), - CardType.outlined => OutlinedCard( - clip: clip, - child: this, - ), - }; - } -} - -enum CardType { - plain, - filled, - outlined; -} diff --git a/lib/design/widgets/common.dart b/lib/design/widgets/common.dart deleted file mode 100644 index 40a2b5854..000000000 --- a/lib/design/widgets/common.dart +++ /dev/null @@ -1,75 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_svg/svg.dart'; -import 'package:rettulf/rettulf.dart'; - -class LeavingBlank extends StatelessWidget { - final WidgetBuilder iconBuilder; - final String? desc; - final VoidCallback? onIconTap; - final Widget? subtitle; - - const LeavingBlank.builder({super.key, required this.iconBuilder, required this.desc, this.onIconTap, this.subtitle}); - - factory LeavingBlank({ - Key? key, - required IconData icon, - String? desc, - VoidCallback? onIconTap, - double size = 120, - Widget? subtitle, - }) { - return LeavingBlank.builder( - iconBuilder: (ctx) => icon.make(size: size, color: ctx.colorScheme.primary), - desc: desc, - onIconTap: onIconTap, - subtitle: subtitle, - ); - } - - factory LeavingBlank.svgAssets({ - Key? key, - required String assetName, - String? desc, - VoidCallback? onIconTap, - double width = 120, - double height = 120, - Widget? subtitle, - }) { - return LeavingBlank.builder( - iconBuilder: (ctx) => SvgPicture.asset(assetName, width: width, height: height), - desc: desc, - onIconTap: onIconTap, - subtitle: subtitle, - ); - } - - @override - Widget build(BuildContext context) { - Widget icon = iconBuilder(context).padAll(20); - if (onIconTap != null) { - icon = icon.on(tap: onIconTap); - } - if (subtitle != null) { - return [ - icon, - if (desc != null) buildDesc(context, desc!), - subtitle!, - ].column(maa: MainAxisAlignment.spaceAround, mas: MainAxisSize.min).center(); - } else { - return [ - icon, - if (desc != null) buildDesc(context, desc!), - ].column(maa: MainAxisAlignment.spaceAround, mas: MainAxisSize.min).center(); - } - } - - Widget buildDesc(BuildContext ctx, String desc) { - return desc - .text( - style: ctx.textTheme.titleLarge, - textAlign: TextAlign.center, - ) - .center() - .padAll(10); - } -} diff --git a/lib/design/widgets/duration_picker.dart b/lib/design/widgets/duration_picker.dart deleted file mode 100644 index 43b705b13..000000000 --- a/lib/design/widgets/duration_picker.dart +++ /dev/null @@ -1,886 +0,0 @@ -/* -Original author: https://github.com/juliansteenbakker/duration_picker - -MIT License - -Copyright (c) 2018 Chris Harris - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - */ - -import 'dart:async'; -import 'dart:math' as math; - -import 'package:flutter/material.dart'; - -const Duration _kDialAnimateDuration = Duration(milliseconds: 200); - -const double _kDurationPickerWidthPortrait = 328.0; -const double _kDurationPickerWidthLandscape = 512.0; - -const double _kDurationPickerHeightPortrait = 380.0; -const double _kDurationPickerHeightLandscape = 304.0; - -const double _kTwoPi = 2 * math.pi; -const double _kPiByTwo = math.pi / 2; - -const double _kCircleTop = _kPiByTwo; - -/// Use [DialPainter] to style the durationPicker to your style. -class DialPainter extends CustomPainter { - const DialPainter({ - required this.context, - required this.labels, - required this.backgroundColor, - required this.accentColor, - required this.theta, - required this.textDirection, - required this.selectedValue, - required this.pct, - required this.baseUnitMultiplier, - required this.baseUnitHand, - required this.baseUnit, - }); - - final List labels; - final Color? backgroundColor; - final Color accentColor; - final double theta; - final TextDirection textDirection; - final int? selectedValue; - final BuildContext context; - - final double pct; - final int baseUnitMultiplier; - final int baseUnitHand; - final BaseUnit baseUnit; - - @override - void paint(Canvas canvas, Size size) { - const epsilon = .001; - const sweep = _kTwoPi - epsilon; - const startAngle = -math.pi / 2.0; - - final radius = size.shortestSide / 2.0; - final center = Offset(size.width / 2.0, size.height / 2.0); - final centerPoint = center; - - final pctTheta = (0.25 - (theta % _kTwoPi) / _kTwoPi) % 1.0; - - // Draw the background outer ring - canvas.drawCircle(centerPoint, radius, Paint()..color = backgroundColor!); - - // Draw a translucent circle for every secondary unit - for (var i = 0; i < baseUnitMultiplier; i = i + 1) { - canvas.drawCircle( - centerPoint, - radius, - Paint()..color = accentColor.withOpacity((i == 0) ? 0.3 : 0.1), - ); - } - - // Draw the inner background circle - canvas.drawCircle( - centerPoint, - radius * 0.88, - Paint()..color = Theme.of(context).canvasColor, - ); - - // Get the offset point for an angle value of theta, and a distance of _radius - Offset getOffsetForTheta(double theta, double radius) { - return center + Offset(radius * math.cos(theta), -radius * math.sin(theta)); - } - - // Draw the handle that is used to drag and to indicate the position around the circle - final handlePaint = Paint()..color = accentColor; - final handlePoint = getOffsetForTheta(theta, radius - 10.0); - canvas.drawCircle(handlePoint, 20.0, handlePaint); - - // Get the appropriate base unit string - String getBaseUnitString() { - switch (baseUnit) { - case BaseUnit.millisecond: - return 'ms.'; - case BaseUnit.second: - return 'sec.'; - case BaseUnit.minute: - return 'min.'; - case BaseUnit.hour: - return 'hr.'; - } - } - - // Get the appropriate secondary unit string - String getSecondaryUnitString() { - switch (baseUnit) { - case BaseUnit.millisecond: - return 's '; - case BaseUnit.second: - return 'm '; - case BaseUnit.minute: - return 'h '; - case BaseUnit.hour: - return 'd '; - } - } - - // Draw the Text in the center of the circle which displays the duration string - final secondaryUnits = (baseUnitMultiplier == 0) ? '' : '$baseUnitMultiplier${getSecondaryUnitString()} '; - final baseUnits = '$baseUnitHand'; - - final textDurationValuePainter = TextPainter( - textAlign: TextAlign.center, - text: TextSpan( - text: '$secondaryUnits$baseUnits', - style: Theme.of(context).textTheme.displayMedium!.copyWith(fontSize: size.shortestSide * 0.15), - ), - textDirection: TextDirection.ltr, - )..layout(); - final middleForValueText = Offset( - centerPoint.dx - (textDurationValuePainter.width / 2), - centerPoint.dy - textDurationValuePainter.height / 2, - ); - textDurationValuePainter.paint(canvas, middleForValueText); - - final textMinPainter = TextPainter( - textAlign: TextAlign.center, - text: TextSpan( - text: getBaseUnitString(), //th: ${theta}', - style: Theme.of(context).textTheme.bodyMedium, - ), - textDirection: TextDirection.ltr, - )..layout(); - textMinPainter.paint( - canvas, - Offset( - centerPoint.dx - (textMinPainter.width / 2), - centerPoint.dy + (textDurationValuePainter.height / 2) - textMinPainter.height / 2, - ), - ); - - // Draw an arc around the circle for the amount of the circle that has elapsed. - final elapsedPainter = Paint() - ..style = PaintingStyle.stroke - ..strokeCap = StrokeCap.round - ..color = accentColor.withOpacity(0.3) - ..isAntiAlias = true - ..strokeWidth = radius * 0.12; - - canvas.drawArc( - Rect.fromCircle( - center: centerPoint, - radius: radius - radius * 0.12 / 2, - ), - startAngle, - sweep * pctTheta, - false, - elapsedPainter, - ); - - // Paint the labels (the minute strings) - void paintLabels(List labels) { - final labelThetaIncrement = -_kTwoPi / labels.length; - var labelTheta = _kPiByTwo; - - for (final label in labels) { - final labelOffset = Offset(-label.width / 2.0, -label.height / 2.0); - - label.paint( - canvas, - getOffsetForTheta(labelTheta, radius - 40.0) + labelOffset, - ); - - labelTheta += labelThetaIncrement; - } - } - - paintLabels(labels); - } - - @override - bool shouldRepaint(DialPainter oldDelegate) { - return oldDelegate.labels != labels || - oldDelegate.backgroundColor != backgroundColor || - oldDelegate.accentColor != accentColor || - oldDelegate.theta != theta; - } -} - -class _Dial extends StatefulWidget { - const _Dial({ - required this.duration, - required this.onChanged, - this.baseUnit = BaseUnit.minute, - this.snapToMins = 1.0, - }); - - final Duration duration; - final ValueChanged onChanged; - final BaseUnit baseUnit; - - /// The resolution of mins of the dial, i.e. if snapToMins = 5.0, only durations of 5min intervals will be selectable. - final double? snapToMins; - - @override - _DialState createState() => _DialState(); -} - -class _DialState extends State<_Dial> with SingleTickerProviderStateMixin { - @override - void initState() { - super.initState(); - _thetaController = AnimationController( - duration: _kDialAnimateDuration, - vsync: this, - ); - _thetaTween = Tween( - begin: _getThetaForDuration(widget.duration, widget.baseUnit), - ); - _theta = _thetaTween.animate( - CurvedAnimation(parent: _thetaController, curve: Curves.fastOutSlowIn), - )..addListener(() => setState(() {})); - _thetaController.addStatusListener((status) { - if (status == AnimationStatus.completed) { - _secondaryUnitValue = _secondaryUnitHand(); - _baseUnitValue = _baseUnitHand(); - setState(() {}); - } - }); - - _turningAngle = _kPiByTwo - _turningAngleFactor() * _kTwoPi; - _secondaryUnitValue = _secondaryUnitHand(); - _baseUnitValue = _baseUnitHand(); - } - - late ThemeData themeData; - MaterialLocalizations? localizations; - MediaQueryData? media; - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - assert(debugCheckHasMediaQuery(context)); - themeData = Theme.of(context); - localizations = MaterialLocalizations.of(context); - media = MediaQuery.of(context); - } - - @override - void dispose() { - _thetaController.dispose(); - super.dispose(); - } - - late Tween _thetaTween; - late Animation _theta; - late AnimationController _thetaController; - - final double _pct = 0.0; - int _secondaryUnitValue = 0; - bool _dragging = false; - int _baseUnitValue = 0; - double _turningAngle = 0.0; - - static double _nearest(double target, double a, double b) { - return ((target - a).abs() < (target - b).abs()) ? a : b; - } - - void _animateTo(double targetTheta) { - final currentTheta = _theta.value; - var beginTheta = _nearest(targetTheta, currentTheta, currentTheta + _kTwoPi); - beginTheta = _nearest(targetTheta, beginTheta, currentTheta - _kTwoPi); - _thetaTween - ..begin = beginTheta - ..end = targetTheta; - _thetaController - ..value = 0.0 - ..forward(); - } - - // Converts the duration to the chosen base unit. For example, for base unit minutes, this gets the number of minutes - // in the duration - int _getDurationInBaseUnits(Duration duration, BaseUnit baseUnit) { - switch (baseUnit) { - case BaseUnit.millisecond: - return duration.inMilliseconds; - case BaseUnit.second: - return duration.inSeconds; - case BaseUnit.minute: - return duration.inMinutes; - case BaseUnit.hour: - return duration.inHours; - } - } - - // Converts the duration to the chosen secondary unit. For example, for base unit minutes, this gets the number - // of hours in the duration - int _getDurationInSecondaryUnits(Duration duration, BaseUnit baseUnit) { - switch (baseUnit) { - case BaseUnit.millisecond: - return duration.inSeconds; - case BaseUnit.second: - return duration.inMinutes; - case BaseUnit.minute: - return duration.inHours; - case BaseUnit.hour: - return duration.inDays; - } - } - - // Gets the relation between the base unit and the secondary unit, which is the unit just greater than the base unit. - // For example if the base unit is second, it will get the number of seconds in a minute - int _getBaseUnitToSecondaryUnitFactor(BaseUnit baseUnit) { - switch (baseUnit) { - case BaseUnit.millisecond: - return Duration.millisecondsPerSecond; - case BaseUnit.second: - return Duration.secondsPerMinute; - case BaseUnit.minute: - return Duration.minutesPerHour; - case BaseUnit.hour: - return Duration.hoursPerDay; - } - } - - double _getThetaForDuration(Duration duration, BaseUnit baseUnit) { - final int baseUnits = _getDurationInBaseUnits(duration, baseUnit); - final int baseToSecondaryFactor = _getBaseUnitToSecondaryUnitFactor(baseUnit); - - return (_kPiByTwo - (baseUnits % baseToSecondaryFactor) / baseToSecondaryFactor.toDouble() * _kTwoPi) % _kTwoPi; - } - - double _turningAngleFactor() { - return _getDurationInBaseUnits(widget.duration, widget.baseUnit) / - _getBaseUnitToSecondaryUnitFactor(widget.baseUnit); - } - - // TODO: Fix snap to mins - Duration _getTimeForTheta(double theta) { - return _angleToDuration(_turningAngle); - // var fractionalRotation = (0.25 - (theta / _kTwoPi)); - // fractionalRotation = fractionalRotation < 0 - // ? 1 - fractionalRotation.abs() - // : fractionalRotation; - // var mins = (fractionalRotation * 60).round(); - // debugPrint('Mins0: ${widget.snapToMins }'); - // if (widget.snapToMins != null) { - // debugPrint('Mins1: $mins'); - // mins = ((mins / widget.snapToMins!).round() * widget.snapToMins!).round(); - // debugPrint('Mins2: $mins'); - // } - // if (mins == 60) { - // // _snappedHours = _hours + 1; - // // mins = 0; - // return new Duration(hours: 1, minutes: mins); - // } else { - // // _snappedHours = _hours; - // return new Duration(hours: _hours, minutes: mins); - // } - } - - Duration _notifyOnChangedIfNeeded() { - _secondaryUnitValue = _secondaryUnitHand(); - _baseUnitValue = _baseUnitHand(); - final d = _angleToDuration(_turningAngle); - widget.onChanged(d); - - return d; - } - - void _updateThetaForPan() { - setState(() { - final offset = _position! - _center!; - final angle = (math.atan2(offset.dx, offset.dy) - _kPiByTwo) % _kTwoPi; - - // Stop accidental abrupt pans from making the dial seem like it starts from 1h. - // (happens when wanting to pan from 0 clockwise, but when doing so quickly, one actually pans from before 0 (e.g. setting the duration to 59mins, and then crossing 0, which would then mean 1h 1min). - if (angle >= _kCircleTop && - _theta.value <= _kCircleTop && - _theta.value >= 0.1 && // to allow the radians sign change at 15mins. - _secondaryUnitValue == 0) return; - - _thetaTween - ..begin = angle - ..end = angle; - }); - } - - Offset? _position; - Offset? _center; - - void _handlePanStart(DragStartDetails details) { - assert(!_dragging); - _dragging = true; - final box = context.findRenderObject() as RenderBox?; - _position = box?.globalToLocal(details.globalPosition); - _center = box?.size.center(Offset.zero); - - _notifyOnChangedIfNeeded(); - } - - void _handlePanUpdate(DragUpdateDetails details) { - final oldTheta = _theta.value; - _position = _position! + details.delta; - // _position! += details.delta; - _updateThetaForPan(); - final newTheta = _theta.value; - - _updateTurningAngle(oldTheta, newTheta); - _notifyOnChangedIfNeeded(); - } - - int _secondaryUnitHand() { - return _getDurationInSecondaryUnits(widget.duration, widget.baseUnit); - } - - int _baseUnitHand() { - // Result is in [0; num base units in secondary unit - 1], even if overall time is >= 1 secondary unit - return _getDurationInBaseUnits(widget.duration, widget.baseUnit) % - _getBaseUnitToSecondaryUnitFactor(widget.baseUnit); - } - - Duration _angleToDuration(double angle) { - return _baseUnitToDuration(_angleToBaseUnit(angle)); - } - - Duration _baseUnitToDuration(double baseUnitValue) { - final int unitFactor = _getBaseUnitToSecondaryUnitFactor(widget.baseUnit); - - switch (widget.baseUnit) { - case BaseUnit.millisecond: - return Duration( - seconds: baseUnitValue ~/ unitFactor, - milliseconds: (baseUnitValue % unitFactor.toDouble()).toInt(), - ); - case BaseUnit.second: - return Duration( - minutes: baseUnitValue ~/ unitFactor, - seconds: (baseUnitValue % unitFactor.toDouble()).toInt(), - ); - case BaseUnit.minute: - return Duration( - hours: baseUnitValue ~/ unitFactor, - minutes: (baseUnitValue % unitFactor.toDouble()).toInt(), - ); - case BaseUnit.hour: - return Duration( - days: baseUnitValue ~/ unitFactor, - hours: (baseUnitValue % unitFactor.toDouble()).toInt(), - ); - } - } - - String _durationToBaseUnitString(Duration duration) { - switch (widget.baseUnit) { - case BaseUnit.millisecond: - return duration.inMilliseconds.toString(); - case BaseUnit.second: - return duration.inSeconds.toString(); - case BaseUnit.minute: - return duration.inMinutes.toString(); - case BaseUnit.hour: - return duration.inHours.toString(); - } - } - - double _angleToBaseUnit(double angle) { - // Coordinate transformation from mathematical COS to dial COS - final dialAngle = _kPiByTwo - angle; - - // Turn dial angle into minutes, may go beyond 60 minutes (multiple turns) - return dialAngle / _kTwoPi * _getBaseUnitToSecondaryUnitFactor(widget.baseUnit); - } - - void _updateTurningAngle(double oldTheta, double newTheta) { - // Register any angle by which the user has turned the dial. - // - // The resulting turning angle fully captures the state of the dial, - // including multiple turns (= full hours). The [_turningAngle] is in - // mathematical coordinate system, i.e. 3-o-clock position being zero, and - // increasing counter clock wise. - - // From positive to negative (in mathematical COS) - if (newTheta > 1.5 * math.pi && oldTheta < 0.5 * math.pi) { - _turningAngle = _turningAngle - ((_kTwoPi - newTheta) + oldTheta); - } - // From negative to positive (in mathematical COS) - else if (newTheta < 0.5 * math.pi && oldTheta > 1.5 * math.pi) { - _turningAngle = _turningAngle + ((_kTwoPi - oldTheta) + newTheta); - } else { - _turningAngle = _turningAngle + (newTheta - oldTheta); - } - } - - void _handlePanEnd(DragEndDetails details) { - assert(_dragging); - _dragging = false; - _position = null; - _center = null; - _animateTo(_getThetaForDuration(widget.duration, widget.baseUnit)); - } - - void _handleTapUp(TapUpDetails details) { - final box = context.findRenderObject() as RenderBox?; - _position = box?.globalToLocal(details.globalPosition); - _center = box?.size.center(Offset.zero); - _updateThetaForPan(); - _notifyOnChangedIfNeeded(); - - _animateTo( - _getThetaForDuration(_getTimeForTheta(_theta.value), widget.baseUnit), - ); - _dragging = false; - _position = null; - _center = null; - } - - List _buildBaseUnitLabels(TextTheme textTheme) { - final style = textTheme.titleMedium; - - var baseUnitMarkerValues = []; - - switch (widget.baseUnit) { - case BaseUnit.millisecond: - const int interval = 100; - const int factor = Duration.millisecondsPerSecond; - const int length = factor ~/ interval; - baseUnitMarkerValues = List.generate( - length, - (index) => Duration(milliseconds: index * interval), - ); - break; - case BaseUnit.second: - const int interval = 5; - const int factor = Duration.secondsPerMinute; - const int length = factor ~/ interval; - baseUnitMarkerValues = List.generate( - length, - (index) => Duration(seconds: index * interval), - ); - break; - case BaseUnit.minute: - const int interval = 5; - const int factor = Duration.minutesPerHour; - const int length = factor ~/ interval; - baseUnitMarkerValues = List.generate( - length, - (index) => Duration(minutes: index * interval), - ); - break; - case BaseUnit.hour: - const int interval = 3; - const int factor = Duration.hoursPerDay; - const int length = factor ~/ interval; - baseUnitMarkerValues = List.generate(length, (index) => Duration(hours: index * interval)); - break; - } - - final labels = []; - for (final duration in baseUnitMarkerValues) { - final painter = TextPainter( - text: TextSpan(style: style, text: _durationToBaseUnitString(duration)), - textDirection: TextDirection.ltr, - )..layout(); - labels.add(painter); - } - return labels; - } - - @override - Widget build(BuildContext context) { - Color? backgroundColor; - switch (themeData.brightness) { - case Brightness.light: - backgroundColor = Colors.grey[200]; - break; - case Brightness.dark: - backgroundColor = themeData.colorScheme.background; - break; - } - - final theme = Theme.of(context); - - int? selectedDialValue; - _secondaryUnitValue = _secondaryUnitHand(); - _baseUnitValue = _baseUnitHand(); - - return GestureDetector( - excludeFromSemantics: true, - onPanStart: _handlePanStart, - onPanUpdate: _handlePanUpdate, - onPanEnd: _handlePanEnd, - onTapUp: _handleTapUp, - child: CustomPaint( - painter: DialPainter( - pct: _pct, - baseUnitMultiplier: _secondaryUnitValue, - baseUnitHand: _baseUnitValue, - baseUnit: widget.baseUnit, - context: context, - selectedValue: selectedDialValue, - labels: _buildBaseUnitLabels(theme.textTheme), - backgroundColor: backgroundColor, - accentColor: themeData.colorScheme.secondary, - theta: _theta.value, - textDirection: Directionality.of(context), - ), - ), - ); - } -} - -/// A duration picker designed to appear inside a popup dialog. -/// -/// Pass this widget to [showDialog]. The value returned by [showDialog] is the -/// selected [Duration] if the user taps the "OK" button, or null if the user -/// taps the "CANCEL" button. The selected time is reported by calling -/// [Navigator.pop]. -class DurationPickerDialog extends StatefulWidget { - /// Creates a duration picker. - /// - /// [initialTime] must not be null. - const DurationPickerDialog({ - Key? key, - required this.initialTime, - this.baseUnit = BaseUnit.minute, - this.snapToMins = 1.0, - this.decoration, - }) : super(key: key); - - /// The duration initially selected when the dialog is shown. - final Duration initialTime; - final BaseUnit baseUnit; - final double snapToMins; - final BoxDecoration? decoration; - - @override - DurationPickerDialogState createState() => DurationPickerDialogState(); -} - -class DurationPickerDialogState extends State { - @override - void initState() { - super.initState(); - _selectedDuration = widget.initialTime; - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - localizations = MaterialLocalizations.of(context); - } - - Duration? get selectedDuration => _selectedDuration; - Duration? _selectedDuration; - - late MaterialLocalizations localizations; - - void _handleTimeChanged(Duration value) { - setState(() { - _selectedDuration = value; - }); - } - - void _handleCancel() { - Navigator.pop(context); - } - - void _handleOk() { - Navigator.pop(context, _selectedDuration); - } - - @override - Widget build(BuildContext context) { - assert(debugCheckHasMediaQuery(context)); - final theme = Theme.of(context); - final boxDecoration = widget.decoration ?? BoxDecoration(color: theme.dialogBackgroundColor); - final Widget picker = Padding( - padding: const EdgeInsets.all(16.0), - child: AspectRatio( - aspectRatio: 1.0, - child: _Dial( - duration: _selectedDuration!, - onChanged: _handleTimeChanged, - baseUnit: widget.baseUnit, - snapToMins: widget.snapToMins, - ), - ), - ); - - final Widget actions = ButtonBarTheme( - data: ButtonBarTheme.of(context), - child: ButtonBar( - children: [ - TextButton( - onPressed: _handleCancel, - child: Text(localizations.cancelButtonLabel), - ), - TextButton( - onPressed: _handleOk, - child: Text(localizations.okButtonLabel), - ), - ], - ), - ); - - final dialog = Dialog( - child: OrientationBuilder( - builder: (BuildContext context, Orientation orientation) { - final Widget pickerAndActions = DecoratedBox( - decoration: boxDecoration, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded( - child: picker, - ), // picker grows and shrinks with the available space - actions, - ], - ), - ); - - switch (orientation) { - case Orientation.portrait: - return SizedBox( - width: _kDurationPickerWidthPortrait, - height: _kDurationPickerHeightPortrait, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded( - child: pickerAndActions, - ), - ], - ), - ); - case Orientation.landscape: - return SizedBox( - width: _kDurationPickerWidthLandscape, - height: _kDurationPickerHeightLandscape, - child: Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Flexible( - child: pickerAndActions, - ), - ], - ), - ); - } - }, - ), - ); - - return Theme( - data: theme.copyWith( - dialogBackgroundColor: Colors.transparent, - ), - child: dialog, - ); - } - - @override - void dispose() { - super.dispose(); - } -} - -/// Shows a dialog containing the duration picker. -/// -/// The returned Future resolves to the duration selected by the user when the user -/// closes the dialog. If the user cancels the dialog, null is returned. -/// -/// To show a dialog with [initialTime] equal to the current time: -/// -/// ```dart -/// showDurationPicker( -/// initialTime: new Duration.now(), -/// context: context, -/// ); -/// ``` -Future showDurationPicker({ - required BuildContext context, - required Duration initialTime, - BaseUnit baseUnit = BaseUnit.minute, - double snapToMins = 1.0, - BoxDecoration? decoration, -}) async { - return showAdaptiveDialog( - context: context, - builder: (BuildContext context) => DurationPickerDialog( - initialTime: initialTime, - baseUnit: baseUnit, - snapToMins: snapToMins, - decoration: decoration, - ), - ); -} - -/// The [DurationPicker] widget. -class DurationPicker extends StatelessWidget { - final Duration duration; - final ValueChanged onChange; - final BaseUnit baseUnit; - final double? snapToMins; - - final double? width; - final double? height; - - const DurationPicker({ - Key? key, - this.duration = Duration.zero, - required this.onChange, - this.baseUnit = BaseUnit.minute, - this.snapToMins, - this.width, - this.height, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return SizedBox( - width: width ?? _kDurationPickerWidthPortrait / 1.5, - height: height ?? _kDurationPickerHeightPortrait / 1.5, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded( - child: _Dial( - duration: duration, - onChanged: onChange, - baseUnit: baseUnit, - snapToMins: snapToMins, - ), - ), - ], - ), - ); - } -} - -/// This enum contains the possible units for the [DurationPicker] -enum BaseUnit { - millisecond, - second, - minute, - hour, -} diff --git a/lib/design/widgets/entry_card.dart b/lib/design/widgets/entry_card.dart deleted file mode 100644 index 81fb26981..000000000 --- a/lib/design/widgets/entry_card.dart +++ /dev/null @@ -1,386 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:pull_down_button/pull_down_button.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:sit/design/adaptive/foundation.dart'; -import 'package:sit/design/adaptive/multiplatform.dart'; -import 'package:sit/design/widgets/card.dart'; - -enum EntryActionType { - edit, - share, - other; -} - -class EntryAction { - final IconData? icon; - final IconData? cupertinoIcon; - final bool main; - final String label; - final EntryActionType type; - final bool oneShot; - final bool delayContextMenu; - final Future Function()? action; - - const EntryAction({ - required this.label, - this.main = false, - this.icon, - this.delayContextMenu = true, - this.oneShot = false, - this.cupertinoIcon, - this.action, - this.type = EntryActionType.other, - }); -} - -class EntrySelectAction { - final String selectLabel; - final String selectedLabel; - final Future Function()? action; - - EntrySelectAction({ - required this.selectLabel, - required this.selectedLabel, - required this.action, - }); -} - -class EntryDeleteAction { - final String label; - final Future Function()? action; - final bool delayContextMenu; - - EntryDeleteAction({ - required this.label, - required this.action, - this.delayContextMenu = true, - }); -} - -class EntryDetailsAction { - final String label; - final IconData? icon; - - EntryDetailsAction({ - required this.label, - this.icon, - }); -} - -class EntryCard extends StatelessWidget { - final bool selected; - final String title; - final List Function(BuildContext context, Animation? animation) itemBuilder; - final Widget Function(BuildContext context, List Function(BuildContext context)? actionsBuilder) - detailsBuilder; - final List Function(BuildContext context) actions; - final EntrySelectAction Function(BuildContext context) selectAction; - final EntryDeleteAction Function(BuildContext context)? deleteAction; - - const EntryCard({ - super.key, - required this.title, - required this.selected, - required this.itemBuilder, - required this.actions, - required this.selectAction, - required this.detailsBuilder, - this.deleteAction, - }); - - @override - Widget build(BuildContext context) { - return isCupertino ? buildCupertinoCard(context) : buildMaterialCard(context); - } - - Widget buildMaterialCard(BuildContext context) { - final actions = this.actions(context); - final body = InkWell( - child: [ - ...itemBuilder(context, null), - OverflowBar( - alignment: MainAxisAlignment.spaceBetween, - children: [ - buildMaterialMainActions(context, actions.where((action) => action.main).toList()), - buildMaterialActionPopup(context, actions.where((action) => !action.main).toList()), - ], - ), - ].column(caa: CrossAxisAlignment.start).padSymmetric(v: 10, h: 15), - onTap: () async { - await context.show$Sheet$((ctx) => detailsBuilder(context, null)); - }, - ); - return selected - ? body.inFilledCard( - clip: Clip.hardEdge, - ) - : body.inOutlinedCard( - clip: Clip.hardEdge, - ); - } - - Widget buildCupertinoCard(BuildContext context) { - return Builder( - builder: (context) { - final actions = this.actions(context); - final deleteAction = this.deleteAction?.call(context); - return CupertinoContextMenu.builder( - enableHapticFeedback: true, - actions: buildContextMenuActions( - context, - actions: actions, - selectAction: selectAction(context), - deleteAction: deleteAction, - ), - builder: (context, animation) { - return buildCupertinoCardBody( - context, - animation: animation, - ); - }, - ); - }, - ); - } - - Widget buildCupertinoCardBody( - BuildContext context, { - required Animation animation, - }) { - Widget body = [ - ...itemBuilder(context, animation), - OverflowBar( - alignment: MainAxisAlignment.end, - children: [ - if (animation.value <= 0) - CupertinoButton( - padding: EdgeInsets.zero, - onPressed: selected ? null : selectAction(context).action, - child: selected - ? Icon(CupertinoIcons.check_mark, color: context.colorScheme.primary) - : const Icon(CupertinoIcons.square), - ), - ], - ), - ].column(caa: CrossAxisAlignment.start).padOnly(t: 12, l: 12, r: 8, b: 4); - if (animation.value <= 0) { - body = body.inkWell(onTap: () async { - if (animation.value <= 0) { - await context.show$Sheet$((ctx) => detailsBuilder(context, buildDetailsActions)); - } - }); - } - final widget = selected - ? body.inFilledCard( - clip: Clip.hardEdge, - ) - : body.inOutlinedCard( - clip: Clip.hardEdge, - ); - return widget; - } - - List buildContextMenuActions( - BuildContext context, { - required List actions, - required EntrySelectAction selectAction, - required EntryDeleteAction? deleteAction, - }) { - final all = []; - if (!selected) { - final selectCallback = selectAction.action; - all.add(CupertinoContextMenuAction( - trailingIcon: CupertinoIcons.check_mark, - onPressed: selectCallback == null - ? null - : () async { - Navigator.of(context, rootNavigator: true).pop(); - await Future.delayed(const Duration(milliseconds: 336)); - await selectCallback(); - }, - child: selectAction.selectLabel.text(), - )); - } - for (final action in actions) { - final callback = action.action; - all.add(CupertinoContextMenuAction( - trailingIcon: action.cupertinoIcon ?? action.icon, - onPressed: callback == null - ? null - : () async { - Navigator.of(context, rootNavigator: true).pop(); - if (action.delayContextMenu) { - await Future.delayed(const Duration(milliseconds: 336)); - } - await callback(); - }, - child: action.label.text(), - )); - } - if (deleteAction != null) { - all.add(CupertinoContextMenuAction( - trailingIcon: CupertinoIcons.delete, - onPressed: () async { - Navigator.of(context, rootNavigator: true).pop(); - if (deleteAction.delayContextMenu) { - await Future.delayed(const Duration(milliseconds: 336)); - } - await deleteAction.action?.call(); - }, - isDestructiveAction: true, - child: deleteAction.label.text(), - )); - } - assert(all.isNotEmpty, "CupertinoContextMenuActions can't be empty"); - return all; - } - - Widget buildMaterialMainActions(BuildContext context, List mainActions) { - final all = []; - all.add(buildMaterialSelectAction(context)); - for (final action in mainActions) { - all.add(OutlinedButton( - onPressed: action.action, - child: action.label.text(), - )); - } - return all.wrap(spacing: 4); - } - - Widget buildMaterialSelectAction(BuildContext context) { - final selectAction = this.selectAction(context); - if (selected) { - return FilledButton.icon( - icon: const Icon(Icons.check), - onPressed: null, - label: selectAction.selectedLabel.text(), - ); - } else { - return FilledButton( - onPressed: selectAction.action, - child: selectAction.selectLabel.text(), - ); - } - } - - Widget buildMaterialActionPopup(BuildContext context, List secondaryActions) { - return PopupMenuButton( - position: PopupMenuPosition.under, - padding: EdgeInsets.zero, - itemBuilder: (ctx) { - final all = []; - for (final action in secondaryActions) { - final callback = action.action; - all.add(PopupMenuItem( - onTap: callback == null - ? null - : () async { - await callback(); - }, - child: ListTile( - leading: Icon(action.icon), - title: action.label.text(), - enabled: callback != null, - ), - )); - } - final deleteAction = this.deleteAction; - if (deleteAction != null) { - final deleteActionWidget = deleteAction(context); - all.add(PopupMenuItem( - onTap: () async { - await deleteActionWidget.action?.call(); - }, - child: ListTile( - leading: const Icon(Icons.delete, color: Colors.redAccent), - title: deleteActionWidget.label.text(style: const TextStyle(color: Colors.redAccent)), - ), - )); - } - return all; - }, - ); - } - - List buildDetailsActions(BuildContext context) { - final all = []; - final actions = this.actions(context); - final selectAction = this.selectAction.call(context); - final deleteAction = this.deleteAction?.call(context); - final editAction = actions.firstWhereOrNull((action) => action.type == EntryActionType.edit); - if (editAction != null) { - all.add(CupertinoButton( - onPressed: editAction.action == null - ? null - : () async { - await editAction.action?.call(); - }, - child: editAction.label.text(), - )); - // remove edit action - actions.retainWhere((action) => action.type != EntryActionType.edit); - if (!selected) { - actions.insert( - 0, - EntryAction( - label: selectAction.selectLabel, - oneShot: true, - cupertinoIcon: CupertinoIcons.check_mark, - action: selectAction.action, - ), - ); - } - } else if (!selected) { - all.add(CupertinoButton( - onPressed: selectAction.action == null - ? null - : () async { - await selectAction.action?.call(); - if (!context.mounted) return; - context.navigator.pop(); - }, - child: selectAction.selectLabel.text(), - )); - } - all.add(PullDownButton( - itemBuilder: (context) => [ - ...actions.map( - (action) => PullDownMenuItem( - icon: action.cupertinoIcon ?? action.icon, - title: action.label, - onTap: action.action == null - ? null - : () async { - if (action.oneShot) { - if (!context.mounted) return; - context.navigator.pop(); - } - await action.action?.call(); - }, - ), - ), - if (deleteAction != null) ...[ - const PullDownMenuDivider.large(), - PullDownMenuItem( - icon: CupertinoIcons.delete, - title: deleteAction.label, - onTap: () async { - await deleteAction.action?.call(); - if (!context.mounted) return; - context.navigator.pop(); - }, - isDestructive: true, - ), - ], - ], - buttonBuilder: (context, showMenu) => CupertinoButton( - onPressed: showMenu, - padding: EdgeInsets.zero, - child: const Icon(CupertinoIcons.ellipsis_circle), - ), - )); - return all; - } -} diff --git a/lib/design/widgets/expansion_tile.dart b/lib/design/widgets/expansion_tile.dart deleted file mode 100644 index 70e5dc6ba..000000000 --- a/lib/design/widgets/expansion_tile.dart +++ /dev/null @@ -1,189 +0,0 @@ -import 'package:flutter/material.dart'; - -// thanks to "https://github.com/simplewidgets/rounded_expansion_tile" -const _kDefaultDuration = Duration(milliseconds: 300); - -class AnimatedExpansionTile extends StatefulWidget { - final List children; - final bool? autofocus; - final EdgeInsetsGeometry? contentPadding; - final bool? dense; - final bool? enabled; - final bool? enableFeedback; - final Color? focusColor; - final FocusNode? focusNode; - final double? horizontalTitleGap; - final Color? hoverColor; - final bool? isThreeLine; - final Widget? leading; - final double? minLeadingWidth; - final double? minVerticalPadding; - final MouseCursor? mouseCursor; - final void Function()? onLongPress; - final bool? selected; - final Color? selectedTileColor; - final ShapeBorder? shape; - final Widget? subtitle; - final Widget? title; - final Color? tileColor; - final Widget? trailing; - final VisualDensity? visualDensity; - final Duration? duration; - final Curve? fadeCurve; - final Curve? sizeCurve; - final EdgeInsets? childrenPadding; - final bool? rotateTrailing; - final bool? noTrailing; - final bool initiallyExpanded; - - const AnimatedExpansionTile({ - super.key, - required this.children, - this.title, - this.subtitle, - this.leading, - this.trailing, - this.duration, - this.autofocus, - this.contentPadding, - this.dense, - this.enabled, - this.enableFeedback, - this.focusColor, - this.focusNode, - this.horizontalTitleGap, - this.hoverColor, - this.isThreeLine, - this.minLeadingWidth, - this.minVerticalPadding, - this.mouseCursor, - this.onLongPress, - this.selected, - this.selectedTileColor, - this.shape, - this.tileColor, - this.visualDensity, - this.childrenPadding, - this.rotateTrailing, - this.noTrailing, - this.initiallyExpanded = false, - this.fadeCurve, - this.sizeCurve, - }); - - @override - AnimatedExpansionTileState createState() => AnimatedExpansionTileState(); -} - -class AnimatedExpansionTileState extends State with TickerProviderStateMixin { - late bool _expanded; - bool? _rotateTrailing; - bool? _noTrailing; - late AnimationController _iconController; - - // When the duration of the ListTile animation is NOT provided. This value will be used instead. - - @override - void initState() { - super.initState(); - _expanded = widget.initiallyExpanded; - // If not provided, this will be true - _rotateTrailing = widget.rotateTrailing ?? true; - // If not provided this will be false - _noTrailing = widget.noTrailing ?? false; - - _iconController = AnimationController( - duration: widget.duration ?? _kDefaultDuration, - vsync: this, - ); - } - - @override - void dispose() { - if (mounted) { - _iconController.dispose(); - super.dispose(); - } - } - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - // If bool is not provided the default will be false. - autofocus: widget.autofocus ?? false, - contentPadding: widget.contentPadding, - // If bool is not provided the default will be false. - dense: widget.dense, - // If bool is not provided the default will be true. - enabled: widget.enabled ?? true, - enableFeedback: - // If bool is not provided the default will be false. - widget.enableFeedback ?? false, - focusColor: widget.focusColor, - focusNode: widget.focusNode, - horizontalTitleGap: widget.horizontalTitleGap, - hoverColor: widget.hoverColor, - // If bool is not provided the default will be false. - isThreeLine: widget.isThreeLine ?? false, - key: widget.key, - leading: widget.leading, - minLeadingWidth: widget.minLeadingWidth, - minVerticalPadding: widget.minVerticalPadding, - mouseCursor: widget.mouseCursor, - onLongPress: widget.onLongPress, - // If bool is not provided the default will be false. - selected: widget.selected ?? false, - selectedTileColor: widget.selectedTileColor, - shape: widget.shape, - subtitle: widget.subtitle, - title: widget.title, - tileColor: widget.tileColor, - trailing: _noTrailing! ? null : _trailingIcon(), - visualDensity: widget.visualDensity, - onTap: () { - setState(() { - // Checks if the ListTile is expanded and sets state accordingly. - if (_expanded) { - _expanded = !_expanded; - _iconController.reverse(); - } else { - _expanded = !_expanded; - _iconController.forward(); - } - }); - }, - ), - ListView.builder( - physics: const NeverScrollableScrollPhysics(), - padding: widget.childrenPadding ?? EdgeInsets.zero, - shrinkWrap: true, - itemCount: widget.children.length, - itemBuilder: (ctx, i) => AnimatedCrossFade( - duration: widget.duration ?? _kDefaultDuration, - firstCurve: widget.fadeCurve ?? Curves.linear, - sizeCurve: widget.sizeCurve ?? Curves.fastEaseInToSlowEaseOut, - firstChild: widget.children[i], - secondChild: const SizedBox(), - crossFadeState: _expanded ? CrossFadeState.showFirst : CrossFadeState.showSecond, - ), - ), - ], - ); - } - - // Build trailing widget based on the user input. - Widget? _trailingIcon() { - final trailing = widget.trailing ?? const Icon(Icons.keyboard_arrow_down); - if (_rotateTrailing!) { - return RotationTransition(turns: Tween(begin: 0.0, end: 0.5).animate(_iconController), child: trailing); - } else { - // If developer sets rotateTrailing to false the widget will just be returned. - return trailing; - } - } -} diff --git a/lib/design/widgets/fab.dart b/lib/design/widgets/fab.dart deleted file mode 100644 index 1e3359a3a..000000000 --- a/lib/design/widgets/fab.dart +++ /dev/null @@ -1,117 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; - -enum _FABType { - regular, - small, - large, - extended, -} - -class AutoHideFAB extends StatefulWidget { - final ScrollController controller; - final Widget? label; - final Widget? child; - final VoidCallback? onPressed; - final _FABType _type; - - /// false by default. - final bool? alwaysShow; - - const AutoHideFAB({ - super.key, - required this.controller, - required this.onPressed, - this.label, - this.child, - this.alwaysShow, - }) : _type = _FABType.regular; - - const AutoHideFAB.extended({ - super.key, - required this.controller, - required this.onPressed, - required Widget this.label, - required Widget? icon, - this.alwaysShow, - }) : _type = _FABType.extended, - child = icon; - - @override - State createState() => _AutoHideFABState(); -} - -class _AutoHideFABState extends State { - bool showBtn = true; - - bool get alwaysShow => widget.alwaysShow ?? false; - - @override - void initState() { - super.initState(); - widget.controller.addListener(onScrollChanged); - } - - @override - void dispose() { - widget.controller.addListener(onScrollChanged); - super.dispose(); - } - - void onScrollChanged() { - final direction = widget.controller.positions.last.userScrollDirection; - if (direction == ScrollDirection.forward) { - if (!showBtn) { - setState(() { - showBtn = true; - }); - } - } else if (direction == ScrollDirection.reverse) { - if (showBtn) { - setState(() { - showBtn = false; - }); - } - } - } - - @override - Widget build(BuildContext context) { - return AnimatedSlideDown( - upWhen: alwaysShow || showBtn, - child: switch (widget._type) { - _FABType.extended => FloatingActionButton.extended( - icon: widget.child, - onPressed: widget.onPressed, - label: widget.label!, - ), - _FABType.regular => FloatingActionButton( - onPressed: widget.onPressed, - child: widget.child, - ), - _FABType.small => FloatingActionButton.small( - onPressed: widget.onPressed, - child: widget.child, - ), - _FABType.large => FloatingActionButton.large( - onPressed: widget.onPressed, - child: widget.child, - ), - }, - ); - } -} - -// ignore: non_constant_identifier_names -Widget AnimatedSlideDown({ - required bool upWhen, - required Widget child, -}) { - const duration = Duration(milliseconds: 300); - return AnimatedSlide( - duration: duration, - curve: Curves.fastEaseInToSlowEaseOut.flipped, - offset: upWhen ? Offset.zero : const Offset(0, 2), - child: child, - ); -} diff --git a/lib/design/widgets/grouped.dart b/lib/design/widgets/grouped.dart deleted file mode 100644 index 2bfcf62e0..000000000 --- a/lib/design/widgets/grouped.dart +++ /dev/null @@ -1,158 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:sit/design/widgets/card.dart'; -import 'package:sliver_tools/sliver_tools.dart'; - -typedef HeaderBuilder = Widget Function( - bool expanded, - VoidCallback toggleExpand, - Widget defaultTrailing, -); - -class GroupedSection extends StatefulWidget { - final bool initialExpanded; - final int itemCount; - final HeaderBuilder headerBuilder; - final Widget Function(BuildContext context, int index) itemBuilder; - - const GroupedSection({ - super.key, - this.initialExpanded = true, - required this.itemCount, - required this.itemBuilder, - required this.headerBuilder, - }); - - @override - State createState() => _GroupedSectionState(); -} - -class _GroupedSectionState extends State { - late var expanded = widget.initialExpanded; - - @override - Widget build(BuildContext context) { - return MultiSliver( - pushPinnedChildren: true, - children: [ - SliverPinnedHeader( - child: Card( - clipBehavior: Clip.hardEdge, - child: widget.headerBuilder( - expanded, - () { - setState(() { - expanded = !expanded; - }); - }, - expanded ? const Icon(Icons.expand_less) : const Icon(Icons.expand_more), - ), - ), - ), - SliverAnimatedPaintExtent( - duration: Durations.medium3, - curve: Curves.fastEaseInToSlowEaseOut, - child: SliverList( - delegate: !expanded - ? const SliverChildListDelegate.fixed([]) - : SliverChildBuilderDelegate( - widget.itemBuilder, - childCount: widget.itemCount, - ), - ), - ) - ], - ); - } -} - -class AsyncGroupSection extends StatefulWidget { - final bool initialExpanded; - final Widget? title; - final Widget? subtitle; - final Future> Function() fetch; - final Widget Function(BuildContext context, int index, T item) itemBuilder; - - const AsyncGroupSection({ - super.key, - this.title, - this.subtitle, - this.initialExpanded = true, - required this.fetch, - required this.itemBuilder, - }); - - @override - State> createState() => _AsyncGroupSectionState(); -} - -class _AsyncGroupSectionState extends State> { - late var expanded = widget.initialExpanded; - List? items; - bool isFetching = false; - - @override - void initState() { - super.initState(); - if (widget.initialExpanded) { - fetchData(); - } - } - - Future fetchData() async { - if (items != null) return; - setState(() { - isFetching = true; - }); - final data = await widget.fetch(); - setState(() { - items = data; - isFetching = false; - }); - } - - @override - Widget build(BuildContext context) { - final items = this.items; - return MultiSliver( - pushPinnedChildren: true, - children: [ - SliverPinnedHeader( - child: FilledCard( - clip: Clip.hardEdge, - child: ListTile( - title: widget.title, - subtitle: widget.subtitle, - dense: true, - onTap: () async { - setState(() { - expanded = !expanded; - }); - await fetchData(); - }, - trailing: isFetching - ? const CircularProgressIndicator.adaptive() - : expanded - ? const Icon(Icons.expand_less) - : const Icon(Icons.expand_more), - ), - ), - ), - SliverAnimatedPaintExtent( - duration: const Duration(milliseconds: 300), - curve: Curves.fastEaseInToSlowEaseOut, - child: SliverList( - delegate: !expanded || items == null - ? const SliverChildListDelegate.fixed([]) - : SliverChildBuilderDelegate( - childCount: items.length, - (ctx, i) { - final item = items[i]; - return widget.itemBuilder(ctx, i, item); - }, - ), - ), - ) - ], - ); - } -} diff --git a/lib/design/widgets/list_tile.dart b/lib/design/widgets/list_tile.dart deleted file mode 100644 index 1a31ce72a..000000000 --- a/lib/design/widgets/list_tile.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:sit/design/adaptive/dialog.dart'; -import 'package:sit/l10n/common.dart'; - -class DetailListTile extends StatelessWidget { - final String? title; - final String? subtitle; - final Widget? leading; - final Widget? trailing; - final bool copyable; - - const DetailListTile({ - super.key, - this.title, - this.subtitle, - this.copyable = true, - this.leading, - this.trailing, - }); - - @override - Widget build(BuildContext context) { - final subtitle = this.subtitle; - return Tooltip( - triggerMode: TooltipTriggerMode.tap, - verticalOffset: 0, - richMessage: TextSpan( - text: const CommonI18n().copy, - recognizer: copyable && subtitle != null - ? (TapGestureRecognizer() - ..onTap = () async { - final title = this.title; - if (title != null) { - context.showSnackBar(content: const CommonI18n().copyTipOf(title).text()); - } - await Clipboard.setData(ClipboardData(text: subtitle)); - }) - : null), - child: ListTile( - leading: leading, - trailing: trailing, - title: title?.text(), - subtitle: subtitle?.text(), - visualDensity: VisualDensity.compact, - ), - ); - } -} diff --git a/lib/design/widgets/multi_select.dart b/lib/design/widgets/multi_select.dart deleted file mode 100644 index 8a1ca318f..000000000 --- a/lib/design/widgets/multi_select.dart +++ /dev/null @@ -1,307 +0,0 @@ -library multiselect_scope; - -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; - -// Steal from: "https://github.com/flankb/multiselect_scope/blob/master/lib/multiselect_scope.dart" - -/// An object that stores the selected indexes and also allows you to change them -class MultiselectController extends ChangeNotifier { - List _selectedIndexes = []; - List _dataSource = []; - - List get selectedIndexes => _selectedIndexes; - - bool get selectedAny => _selectedIndexes.any((element) => true); - - late int _itemsCount; - - void select(int index) { - assert(index >= 0 && index < _dataSource.length); - if (index < 0 || index >= _dataSource.length) return; - final indexContains = _selectedIndexes.contains(index); - if (!indexContains) { - _selectedIndexes.add(index); - notifyListeners(); - } - } - - void unselect(int index) { - assert(index >= 0 && index < _dataSource.length); - if (index < 0 || index >= _dataSource.length) return; - final indexContains = _selectedIndexes.contains(index); - if (indexContains) { - _selectedIndexes.remove(index); - notifyListeners(); - } - } - - void toggle(int index) { - assert(index >= 0 && index < _dataSource.length); - if (index < 0 || index >= _dataSource.length) return; - final indexContains = _selectedIndexes.contains(index); - final doSelect = indexContains ? false : true; - - if (doSelect) { - if (!indexContains) { - _selectedIndexes.add(index); - notifyListeners(); - } - } else { - if (indexContains) { - _selectedIndexes.remove(index); - notifyListeners(); - } - } - } - - void selectItem(T item) { - final index = _dataSource.indexOf(item); - if (index < 0) return; - select(index); - } - - void unselectItem(T item) { - final index = _dataSource.indexOf(item); - if (index < 0) return; - unselect(index); - } - - void toggleItem(T item) { - final index = _dataSource.indexOf(item); - if (index < 0) return; - toggle(index); - } - - /// Get current selected items in [dataSource] - List getSelectedItems() { - final selectedItems = selectedIndexes.map((e) => _dataSource[e]).toList(); - return selectedItems; - } - - /// Set all selection to empty - void clearSelection() { - if (selectedIndexes.any((element) => true)) { - selectedIndexes.clear(); - notifyListeners(); - } - } - - /// Replace selection by all not selected items - void invertSelection() { - _selectedIndexes = List.generate(_itemsCount, (i) => i).toSet().difference(_selectedIndexes.toSet()).toList(); - - notifyListeners(); - } - - /// Select all items in [dataSource] - void selectAll() { - _selectedIndexes = List.generate(_itemsCount, (i) => i); - notifyListeners(); - } - - bool isSelectedAll() { - return _selectedIndexes.length == _dataSource.length; - } - - /// Check selection of item by it index - bool isSelectedIndex(int index) { - return _selectedIndexes.contains(index); - } - - /// Check selection of item by it index - bool isSelectedItem(T item) { - final index = _dataSource.indexOf(item); - return index >= 0 && _selectedIndexes.contains(index); - } - - /// Set selection by specified indexes - /// Replace existing selected indexes by [newIndexes] - void setSelectedIndexes(List newIndexes) { - _setSelectedIndexes(newIndexes, true); - } - - void setSelectedItems(List newItems) { - _setSelectedItems(newItems, true); - } - - void _setDataSource(List dataSource) { - _dataSource = dataSource; - _itemsCount = dataSource.length; - } - - void _setSelectedIndexes(List newIndexes, bool notifyListeners) { - _selectedIndexes = newIndexes; - if (notifyListeners) { - this.notifyListeners(); - } - } - - void _setSelectedItems(List items, bool notifyListeners) { - _selectedIndexes = items.map((item) => _dataSource.indexOf(item)).where((index) => index >= 0).toList(); - if (notifyListeners) { - this.notifyListeners(); - } - } - - T operator [](int index) { - return _dataSource[index]; - } -} - -typedef SelectionChangedCallback = void Function(List selectedIndexes, List selectedItems); - -/// Widget to manage item selection -class MultiselectScope extends StatefulWidget { - /// A child widget that usually contains in its subtree a list - /// of items whose selection you want to control - final Widget child; - - /// Function that invoked when selected indexes changes. - /// Builds appropriate listeners on stage of init [MultiselectScope] widget - /// and then does not change. - /// This function will not invoke on first load of this widget. - final SelectionChangedCallback? onSelectionChanged; - - /// An object that stores the selected indexes and also allows you to change them - /// This object may be set once and can not be replaced - /// when updating the widget configuration - final MultiselectController? controller; - - /// Data for selection tracking - /// For example list of `Cars` or `Employees` - final List dataSource; - - /// Clear selection if user push back button - final bool clearSelectionOnPop; - - /// If [true]: when you update [dataSource] then selected indexes will update - /// so that the same elements in new [dataSource] are selected - /// If [false]: selected indexes will have not automatically updates during [dataSource] update - final bool keepSelectedItemsBetweenUpdates; - - /// Selected indexes, which will be initialized - /// when the widget is inserted into the widget tree - final List? initialSelectedIndexes; - - const MultiselectScope({ - super.key, - required this.dataSource, - this.controller, - this.onSelectionChanged, - this.clearSelectionOnPop = false, - this.keepSelectedItemsBetweenUpdates = true, - this.initialSelectedIndexes, - required this.child, - }); - - @override - State> createState() => _MultiselectScopeState(); - - static MultiselectController controllerOf(BuildContext context) { - return context.dependOnInheritedWidgetOfExactType<_InheritedMultiselectNotifier>()!.controller - as MultiselectController; - } -} - -class _MultiselectScopeState extends State> { - late List _hashesCopy; - late MultiselectController _multiselectController; - - void _onSelectionChangedFunc() { - if (widget.onSelectionChanged != null) { - widget.onSelectionChanged!( - _multiselectController.selectedIndexes, _multiselectController.getSelectedItems().cast()); - } - } - - List _createHashesCopy(MultiselectScope widget) { - return widget.dataSource.map((e) => e.hashCode).toList(); - } - - @override - void initState() { - super.initState(); - - _multiselectController = widget.controller ?? MultiselectController(); - - _hashesCopy = _createHashesCopy(widget); - _multiselectController._setDataSource(widget.dataSource); - - if (widget.initialSelectedIndexes != null) { - _multiselectController._setSelectedIndexes(widget.initialSelectedIndexes!, false); - } - - if (widget.onSelectionChanged != null) { - _multiselectController.addListener(_onSelectionChangedFunc); - } - } - - @override - void dispose() { - _multiselectController.removeListener(_onSelectionChangedFunc); - super.dispose(); - } - - @override - void didUpdateWidget(MultiselectScope oldWidget) { - super.didUpdateWidget(oldWidget); - debugPrint('didUpdateWidget GreatMultiselect'); - - if (widget.keepSelectedItemsBetweenUpdates) { - _updateController(oldWidget); - } - - _multiselectController._setDataSource(widget.dataSource); - } - - @override - Widget build(BuildContext context) { - debugPrint('build GreatMultiselect'); - return widget.clearSelectionOnPop - ? PopScope( - canPop: !_multiselectController.selectedAny, - onPopInvoked: (didPop) { - if (!didPop) { - _multiselectController.clearSelection(); - } - }, - child: _buildMultiselectScope(), - ) - : _buildMultiselectScope(); - } - - _InheritedMultiselectNotifier _buildMultiselectScope() => - _InheritedMultiselectNotifier(controller: _multiselectController, child: widget.child); - - void _updateController(MultiselectScope oldWidget) { - if (!oldWidget.keepSelectedItemsBetweenUpdates && widget.keepSelectedItemsBetweenUpdates) { - // Recalculate hashes of previous state - _hashesCopy = _createHashesCopy(oldWidget); - } - - final newHashesCopy = _createHashesCopy(widget); - - //debugPrint( - // "Old dataSource: ${_hashesCopy} new dataSource: ${newHashesCopy}"); - final oldSelectedHashes = _multiselectController.selectedIndexes.map((e) => _hashesCopy[e]).toList(); - - final newIndexes = []; - newHashesCopy.asMap().forEach((index, value) { - //debugPrint("$index $value"); - if (oldSelectedHashes.contains(value)) { - newIndexes.add(index); - } - }); - - _multiselectController._setSelectedIndexes(newIndexes, false); - _hashesCopy = newHashesCopy; - } -} - -class _InheritedMultiselectNotifier extends InheritedNotifier { - final MultiselectController controller; - - const _InheritedMultiselectNotifier({Key? key, required Widget child, required this.controller}) - : super(key: key, child: child, notifier: controller); -} diff --git a/lib/design/widgets/navigation.dart b/lib/design/widgets/navigation.dart deleted file mode 100644 index d12143bcc..000000000 --- a/lib/design/widgets/navigation.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; - -class PageNavigationTile extends StatelessWidget { - final Widget? title; - final Widget? subtitle; - final Widget leading; - final String path; - - const PageNavigationTile({ - super.key, - this.title, - this.subtitle, - required this.leading, - required this.path, - }); - - @override - Widget build(BuildContext context) { - return ListTile( - title: title, - subtitle: subtitle, - leading: leading, - trailing: const Icon(Icons.navigate_next_rounded), - onTap: () { - context.push(path); - }, - ); - } -} diff --git a/lib/design/widgets/tags.dart b/lib/design/widgets/tags.dart deleted file mode 100644 index 73e8c4417..000000000 --- a/lib/design/widgets/tags.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:rettulf/rettulf.dart'; - -class TagsGroup extends StatelessWidget { - final List tags; - - const TagsGroup( - this.tags, { - super.key, - }); - - @override - Widget build(BuildContext context) { - final textTheme = context.textTheme; - return tags - .map( - (tag) => Chip( - label: tag.text(), - padding: EdgeInsets.zero, - labelStyle: textTheme.bodySmall, - elevation: 4, - ), - ) - .toList() - .wrap(spacing: 4); - } -} diff --git a/lib/design/widgets/view.dart b/lib/design/widgets/view.dart deleted file mode 100644 index 4e094133d..000000000 --- a/lib/design/widgets/view.dart +++ /dev/null @@ -1,94 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:universal_platform/universal_platform.dart'; - -extension ScrollWidgetListEx on List { - Widget scrolledWithBar({Key? key, ScrollController? controller}) { - if (UniversalPlatform.isWindows || UniversalPlatform.isLinux) { - return listview(); - } else { - return _ScrolledWithBar( - key: key, - controller: controller, - children: this, - ); - } - } -} - -extension ScrollSingleWidgetEx on Widget { - Widget scrolledWithBar({Key? key, ScrollController? controller}) { - if (UniversalPlatform.isWindows || UniversalPlatform.isLinux) { - return scrolled(); - } else { - return _ScrolledWithBar( - key: key, - controller: controller, - child: this, - ); - } - } -} - -class _ScrolledWithBar extends StatefulWidget { - final Widget? child; - final List? children; - final ScrollController? controller; - - const _ScrolledWithBar({super.key, this.child, this.children, this.controller}) - : assert(child != null || children != null); - - @override - State<_ScrolledWithBar> createState() => _ScrolledWithBarState(); -} - -class _ScrolledWithBarState extends State<_ScrolledWithBar> { - ScrollController? controller; - - @override - void initState() { - super.initState(); - final child = widget.child; - if (child is ScrollView) { - final childController = child.controller; - assert(childController != null, "The ScrollView external provided should have a ScrollController."); - controller = child.controller; - } else { - controller = widget.controller ?? ScrollController(); - } - } - - @override - Widget build(BuildContext context) { - return LayoutBuilder( - builder: (ctx, constraints) { - final width = constraints.maxWidth; - var child = widget.child; - final children = widget.children; - if (child != null) { - // child mode - if (child is! ScrollView) { - child = child.scrolled(controller: controller); - } - } else if (children != null) { - // list mode - child = children.listview(controller: controller); - } else { - throw Exception("Never reached."); - } - return Scrollbar( - thickness: width / 25, - radius: const Radius.circular(12), - controller: controller, - interactive: true, - child: child); - }, - ); - } - - @override - void dispose() { - super.dispose(); - controller?.dispose(); - } -} diff --git a/lib/entity/campus.dart b/lib/entity/campus.dart deleted file mode 100644 index 2a653db46..000000000 --- a/lib/entity/campus.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:sit/storage/hive/type_id.dart'; - -part 'campus.g.dart'; - -typedef CampusCapability = ({bool enableElectricity}); - -@HiveType(typeId: CoreHiveType.campus) -enum Campus { - @HiveField(0) - fengxian((enableElectricity: true)), - @HiveField(1) - xuhui((enableElectricity: false)); - - final CampusCapability capability; - - const Campus(this.capability); - - String l10nName() => "campus.$name".tr(); -} diff --git a/lib/entity/campus.g.dart b/lib/entity/campus.g.dart deleted file mode 100644 index 5e9de46f5..000000000 --- a/lib/entity/campus.g.dart +++ /dev/null @@ -1,43 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'campus.dart'; - -// ************************************************************************** -// TypeAdapterGenerator -// ************************************************************************** - -class CampusAdapter extends TypeAdapter { - @override - final int typeId = 3; - - @override - Campus read(BinaryReader reader) { - switch (reader.readByte()) { - case 0: - return Campus.fengxian; - case 1: - return Campus.xuhui; - default: - return Campus.fengxian; - } - } - - @override - void write(BinaryWriter writer, Campus obj) { - switch (obj) { - case Campus.fengxian: - writer.writeByte(0); - break; - case Campus.xuhui: - writer.writeByte(1); - break; - } - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || other is CampusAdapter && runtimeType == other.runtimeType && typeId == other.typeId; -} diff --git a/lib/entity/version.dart b/lib/entity/version.dart deleted file mode 100644 index 14d36556c..000000000 --- a/lib/entity/version.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:package_info_plus/package_info_plus.dart'; -import 'package:universal_platform/universal_platform.dart'; -import 'package:version/version.dart'; - -enum AppPlatform { - android("Android"), - windows("Windows"), - iOS("iOS"), - macOS("macOS"), - linux("Linux"), - web("Web"), - unknown("?"); - - final String name; - - const AppPlatform(this.name); -} - -class AppVersion { - final AppPlatform platform; - final Version full; - - const AppVersion(this.platform, this.full); -} - -Future getCurrentVersion() async { - final info = await PackageInfo.fromPlatform(); - final AppPlatform platform; - if (UniversalPlatform.isAndroid) { - platform = AppPlatform.android; - } else if (UniversalPlatform.isIOS) { - platform = AppPlatform.iOS; - } else if (UniversalPlatform.isMacOS) { - platform = AppPlatform.macOS; - } else if (UniversalPlatform.isLinux) { - platform = AppPlatform.linux; - } else if (UniversalPlatform.isWindows) { - platform = AppPlatform.windows; - } else if (UniversalPlatform.isWeb) { - platform = AppPlatform.web; - } else { - platform = AppPlatform.unknown; - } - return AppVersion(platform, Version.parse(info.version)); -} diff --git a/lib/files.dart b/lib/files.dart deleted file mode 100644 index a5ba0b9a2..000000000 --- a/lib/files.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'dart:io'; - -import 'package:flutter/foundation.dart'; -import 'package:path/path.dart'; -import 'package:universal_platform/universal_platform.dart'; - -class Files { - const Files._(); - - static late final Directory temp; - static late final Directory cache; - static late final Directory internal; - static late final Directory user; - - static Directory get screenshot => temp.subDir("screenshot"); - - static const timetable = TimetableFiles(); - static const oaAnnounce = OaAnnounceFiles(); - - static Future init() async { - if (kIsWeb) return; - await screenshot.create(recursive: true); - await timetable.init(); - } -} - -extension DirectoryX on Directory { - File subFile(String p1, [String? p2, String? p3, String? p4]) => File(join(path, p1, p2, p3, p4)); - - Directory subDir(String p1, [String? p2, String? p3, String? p4]) => Directory(join(path, p1, p2, p3, p4)); -} - -class TimetableFiles { - const TimetableFiles(); - - File get screenshotFile => Files.screenshot.subFile("timetable.png"); - - File get backgroundFile => Files.user.subFile("timetable", "background.png"); - - // on MIUI, OpenFile can't open file under `Files.user` - Directory get calendarDir => (UniversalPlatform.isAndroid ? Files.cache : Files.user).subDir("timetable", "calendar"); - - Future init() async { - await calendarDir.create(recursive: true); - await Files.user.subDir("timetable").create(recursive: true); - } -} - -class OaAnnounceFiles { - const OaAnnounceFiles(); - - Directory attachmentDir(String uuid) => Files.internal.subDir("attachment", uuid); - - Future init() async {} -} diff --git a/lib/game/2048/components/animated_tile.dart b/lib/game/2048/components/animated_tile.dart deleted file mode 100644 index 3f9e63fa8..000000000 --- a/lib/game/2048/components/animated_tile.dart +++ /dev/null @@ -1,77 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../models/tile.dart'; - -class AnimatedTile extends AnimatedWidget { - //We use Listenable.merge in order to update the animated widget when both of the controllers have change - AnimatedTile( - {super.key, - required this.moveAnimation, - required this.scaleAnimation, - required this.tile, - required this.child, - required this.size}) - : super(listenable: Listenable.merge([moveAnimation, scaleAnimation])); - - final Tile tile; - final Widget child; - final CurvedAnimation moveAnimation; - final CurvedAnimation scaleAnimation; - final double size; - - //Get the current top position based on current index of the tile - late final double _top = tile.getTop(size); - - //Get the current left position based on current index of the tile - late final double _left = tile.getLeft(size); - - //Get the next top position based on current next index of the tile - late final double _nextTop = tile.getNextTop(size) ?? _top; - - //Get the next top position based on next index of the tile - late final double _nextLeft = tile.getNextLeft(size) ?? _left; - - //top tween used to move the tile from top to bottom - late final Animation top = Tween( - begin: _top, - end: _nextTop, - ).animate( - moveAnimation, - ), - //left tween used to move the tile from left to right - left = Tween( - begin: _left, - end: _nextLeft, - ).animate( - moveAnimation, - ), - //scale tween used to use give "pop" effect when a merge happens - scale = TweenSequence( - >[ - TweenSequenceItem( - tween: Tween(begin: 1.0, end: 1.5).chain(CurveTween(curve: Curves.easeOut)), - weight: 50.0, - ), - TweenSequenceItem( - tween: Tween(begin: 1.5, end: 1.0).chain(CurveTween(curve: Curves.easeIn)), - weight: 50.0, - ), - ], - ).animate( - scaleAnimation, - ); - - @override - Widget build(BuildContext context) { - return Positioned( - top: top.value, - left: left.value, - //Only use scale animation if the tile was merged - child: tile.merged - ? ScaleTransition( - scale: scale, - child: child, - ) - : child); - } -} diff --git a/lib/game/2048/components/button.dart b/lib/game/2048/components/button.dart deleted file mode 100644 index 7442e0224..000000000 --- a/lib/game/2048/components/button.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -import '../const/colors.dart'; - -class ButtonWidget extends ConsumerWidget { - const ButtonWidget({super.key, this.text, this.icon, required this.onPressed}); - - final String? text; - final IconData? icon; - final VoidCallback onPressed; - - @override - Widget build(BuildContext context, WidgetRef ref) { - if (icon != null) { - //Button Widget with icon for Undo and Restart Game button. - return Container( - decoration: BoxDecoration(color: scoreColor, borderRadius: BorderRadius.circular(8.0)), - child: IconButton( - color: textColorWhite, - onPressed: onPressed, - icon: Icon( - icon, - size: 24.0, - )), - ); - } - //Button Widget with text for New Game and Try Again button. - return ElevatedButton( - style: ButtonStyle( - padding: MaterialStateProperty.all(const EdgeInsets.all(16.0)), - backgroundColor: MaterialStateProperty.all(buttonColor)), - onPressed: onPressed, - child: Text( - text!, - style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18.0), - )); - } -} diff --git a/lib/game/2048/components/empty_board.dart b/lib/game/2048/components/empty_board.dart deleted file mode 100644 index 1c6acca97..000000000 --- a/lib/game/2048/components/empty_board.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'dart:math'; -import 'package:flutter/material.dart'; - -import '../const/colors.dart'; - -class EmptyBoardWidget extends StatelessWidget { - const EmptyBoardWidget({super.key}); - - @override - Widget build(BuildContext context) { - //Decides the maximum size the Board can be based on the shortest size of the screen. - final size = max(290.0, min((MediaQuery.of(context).size.shortestSide * 0.90).floorToDouble(), 460.0)); - - //Decide the size of the tile based on the size of the board minus the space between each tile. - final sizePerTile = (size / 4).floorToDouble(); - final tileSize = sizePerTile - 12.0 - (12.0 / 4); - final boardSize = sizePerTile * 4; - return Container( - width: boardSize, - height: boardSize, - decoration: BoxDecoration(color: boardColor, borderRadius: BorderRadius.circular(6.0)), - child: Stack( - children: List.generate(16, (i) { - //Render the empty board in 4x4 GridView - var x = ((i + 1) / 4).ceil(); - var y = x - 1; - - var top = y * (tileSize) + (x * 12.0); - var z = (i - (4 * y)); - var left = z * (tileSize) + ((z + 1) * 12.0); - - return Positioned( - top: top, - left: left, - child: Container( - width: tileSize, - height: tileSize, - decoration: BoxDecoration(color: emptyTileColor, borderRadius: BorderRadius.circular(6.0)), - ), - ); - }), - ), - ); - } -} diff --git a/lib/game/2048/components/score_board.dart b/lib/game/2048/components/score_board.dart deleted file mode 100644 index 9dcafa2e1..000000000 --- a/lib/game/2048/components/score_board.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -import '../const/colors.dart'; -import '../managers/board.dart'; - -class ScoreBoard extends ConsumerWidget { - const ScoreBoard({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final score = ref.watch(boardManager.select((board) => board.score)); - final best = ref.watch(boardManager.select((board) => board.best)); - - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Score(label: 'Score', score: '$score'), - const SizedBox( - width: 8.0, - ), - Score(label: 'Best', score: '$best', padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0)), - ], - ); - } -} - -class Score extends StatelessWidget { - const Score({super.key, required this.label, required this.score, this.padding}); - - final String label; - final String score; - final EdgeInsets? padding; - - @override - Widget build(BuildContext context) { - return Container( - padding: padding ?? const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), - decoration: BoxDecoration(color: scoreColor, borderRadius: BorderRadius.circular(8.0)), - child: Column(children: [ - Text( - label.toUpperCase(), - style: const TextStyle(fontSize: 18.0, color: color2), - ), - Text( - score, - style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 18.0), - ) - ]), - ); - } -} diff --git a/lib/game/2048/components/tile_board.dart b/lib/game/2048/components/tile_board.dart deleted file mode 100644 index 1b225e1b7..000000000 --- a/lib/game/2048/components/tile_board.dart +++ /dev/null @@ -1,81 +0,0 @@ -import 'dart:math'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -import '../const/colors.dart'; -import '../managers/board.dart'; - -import 'animated_tile.dart'; -import 'button.dart'; - -class TileBoardWidget extends ConsumerWidget { - const TileBoardWidget({super.key, required this.moveAnimation, required this.scaleAnimation}); - - final CurvedAnimation moveAnimation; - final CurvedAnimation scaleAnimation; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final board = ref.watch(boardManager); - - //Decides the maximum size the Board can be based on the shortest size of the screen. - final size = max(290.0, min((MediaQuery.of(context).size.shortestSide * 0.90).floorToDouble(), 460.0)); - - //Decide the size of the tile based on the size of the board minus the space between each tile. - final sizePerTile = (size / 4).floorToDouble(); - final tileSize = sizePerTile - 12.0 - (12.0 / 4); - final boardSize = sizePerTile * 4; - return SizedBox( - width: boardSize, - height: boardSize, - child: Stack( - children: [ - ...List.generate(board.tiles.length, (i) { - var tile = board.tiles[i]; - - return AnimatedTile( - key: ValueKey(tile.id), - tile: tile, - moveAnimation: moveAnimation, - scaleAnimation: scaleAnimation, - size: tileSize, - //In order to optimize performances and prevent unneeded re-rendering the actual tile is passed as child to the AnimatedTile - //as the tile won't change for the duration of the movement (apart from it's position) - child: Container( - width: tileSize, - height: tileSize, - decoration: BoxDecoration(color: tileColors[tile.value], borderRadius: BorderRadius.circular(6.0)), - child: Center( - child: Text( - '${tile.value}', - style: TextStyle( - fontWeight: FontWeight.bold, fontSize: 24.0, color: tile.value < 8 ? textColor : textColorWhite), - )), - ), - ); - }), - if (board.over) - Positioned.fill( - child: Container( - color: overlayColor, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - board.won ? 'You win!' : 'Game over!', - style: const TextStyle(color: textColor, fontWeight: FontWeight.bold, fontSize: 64.0), - ), - ButtonWidget( - text: board.won ? 'New Game' : 'Try again', - onPressed: () { - ref.read(boardManager.notifier).newGame(); - }, - ) - ], - ), - )) - ], - ), - ); - } -} diff --git a/lib/game/2048/const/colors.dart b/lib/game/2048/const/colors.dart deleted file mode 100644 index 92283248b..000000000 --- a/lib/game/2048/const/colors.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'dart:ui'; - -const backgroundColor = Color(0xfffaf8ef); -const textColor = Color(0xff776e65); -const textColorWhite = Color(0xfff9f6f2); -const boardColor = Color(0xffbbada0); -const emptyTileColor = Color(0xffcdc1b4); -const buttonColor = Color(0xff8f7a66); -const scoreColor = Color(0xffbbada0); -const overlayColor = Color.fromRGBO(238, 228, 218, 0.73); - -const color2 = Color(0xffeee4da); -const color4 = Color(0xffeee1c9); -const color8 = Color(0xfff3b27a); -const color16 = Color(0xfff69664); -const color32 = Color(0xfff77c5f); -const color64 = Color(0xfff75f3b); -const color128 = Color(0xffedd073); -const color256 = Color(0xffedcc62); -const color512 = Color(0xffedc950); -const color1024 = Color(0xffedc53f); -const color2048 = Color(0xffedc22e); - -const tileColors = { - 2: color2, - 4: color4, - 8: color8, - 16: color16, - 32: color32, - 64: color64, - 128: color128, - 256: color256, - 512: color512, - 1024: color1024, - 2048: color2048, -}; diff --git a/lib/game/2048/game.dart b/lib/game/2048/game.dart deleted file mode 100644 index ad5289c22..000000000 --- a/lib/game/2048/game.dart +++ /dev/null @@ -1,167 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_swipe_detector/flutter_swipe_detector.dart'; - -import 'components/button.dart'; -import 'components/empty_board.dart'; -import 'components/score_board.dart'; -import 'components/tile_board.dart'; -import 'const/colors.dart'; -import 'managers/board.dart'; - -class Game2048 extends ConsumerStatefulWidget { - const Game2048({super.key}); - - @override - ConsumerState createState() => _GameState(); -} - -class _GameState extends ConsumerState with TickerProviderStateMixin, WidgetsBindingObserver { - //The controller used to move the the tiles - late final AnimationController _moveController = AnimationController( - duration: const Duration(milliseconds: 100), - vsync: this, - )..addStatusListener((status) { - //When the movement finishes merge the tiles and start the scale animation which gives the pop effect. - if (status == AnimationStatus.completed) { - ref.read(boardManager.notifier).merge(); - _scaleController.forward(from: 0.0); - } - }); - - //The curve animation for the move animation controller. - late final CurvedAnimation _moveAnimation = CurvedAnimation( - parent: _moveController, - curve: Curves.easeInOut, - ); - - //The contoller used to show a popup effect when the tiles get merged - late final AnimationController _scaleController = AnimationController( - duration: const Duration(milliseconds: 200), - vsync: this, - )..addStatusListener((status) { - //When the scale animation finishes end the round and if there is a queued movement start the move controller again for the next direction. - if (status == AnimationStatus.completed) { - if (ref.read(boardManager.notifier).endRound()) { - _moveController.forward(from: 0.0); - } - } - }); - - //The curve animation for the scale animation controller. - late final CurvedAnimation _scaleAnimation = CurvedAnimation( - parent: _scaleController, - curve: Curves.easeInOut, - ); - - @override - void initState() { - //Add an Observer for the Lifecycles of the App - WidgetsBinding.instance.addObserver(this); - super.initState(); - } - - @override - Widget build(BuildContext context) { - return RawKeyboardListener( - autofocus: true, - focusNode: FocusNode(), - onKey: (RawKeyEvent event) { - //Move the tile with the arrows on the keyboard on Desktop - if (ref.read(boardManager.notifier).onKey(event)) { - _moveController.forward(from: 0.0); - } - }, - child: SwipeDetector( - onSwipe: (direction, offset) { - if (ref.read(boardManager.notifier).move(direction)) { - _moveController.forward(from: 0.0); - } - }, - child: Scaffold( - backgroundColor: backgroundColor, - body: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - '2048', - style: TextStyle(color: textColor, fontWeight: FontWeight.bold, fontSize: 52.0), - ), - Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - const ScoreBoard(), - const SizedBox( - height: 32.0, - ), - Row( - children: [ - ButtonWidget( - icon: Icons.undo, - onPressed: () { - //Undo the round. - ref.read(boardManager.notifier).undo(); - }, - ), - const SizedBox( - width: 16.0, - ), - ButtonWidget( - icon: Icons.refresh, - onPressed: () { - //Restart the game - ref.read(boardManager.notifier).newGame(); - }, - ) - ], - ) - ], - ) - ], - ), - ), - const SizedBox( - height: 32.0, - ), - Stack( - children: [ - const EmptyBoardWidget(), - TileBoardWidget(moveAnimation: _moveAnimation, scaleAnimation: _scaleAnimation) - ], - ) - ], - ), - ), - ), - ); - } - - @override - void didChangeAppLifecycleState(AppLifecycleState state) { - //Save current state when the app becomes inactive - if (state == AppLifecycleState.inactive) { - ref.read(boardManager.notifier).save(); - } - super.didChangeAppLifecycleState(state); - } - - @override - void dispose() { - //Remove the Observer for the Lifecycles of the App - WidgetsBinding.instance.removeObserver(this); - - //Dispose the animations. - _moveAnimation.dispose(); - _scaleAnimation.dispose(); - _moveController.dispose(); - _scaleController.dispose(); - super.dispose(); - } -} diff --git a/lib/game/2048/index.dart b/lib/game/2048/index.dart deleted file mode 100644 index 216e499fc..000000000 --- a/lib/game/2048/index.dart +++ /dev/null @@ -1,28 +0,0 @@ -// thanks to https://github.com/angjelkom/flutter_2048 - -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:rettulf/rettulf.dart'; - -import 'game.dart'; - -class Game2048Page extends StatefulWidget { - const Game2048Page({super.key}); - - @override - State createState() => _Game2048PageState(); -} - -class _Game2048PageState extends State { - @override - Widget build(BuildContext context) { - return ProviderScope( - child: Scaffold( - appBar: AppBar( - title: "2048".text(), - ), - body: const Game2048(), - ), - ); - } -} diff --git a/lib/game/2048/managers/board.dart b/lib/game/2048/managers/board.dart deleted file mode 100644 index 6d7ad3b08..000000000 --- a/lib/game/2048/managers/board.dart +++ /dev/null @@ -1,301 +0,0 @@ -import 'dart:math'; -import 'package:flutter/services.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_swipe_detector/flutter_swipe_detector.dart'; -import 'package:uuid/uuid.dart'; - -import '../models/tile.dart'; -import '../models/board.dart'; - -import 'next_direction.dart'; -import 'round.dart'; - -class BoardManager extends StateNotifier { - // We will use this list to retrieve the right index when user swipes up/down - // which will allow us to reuse most of the logic. - final verticalOrder = [12, 8, 4, 0, 13, 9, 5, 1, 14, 10, 6, 2, 15, 11, 7, 3]; - - final StateNotifierProviderRef ref; - - BoardManager(this.ref) : super(Board.newGame(0, [])) { - //Load the last saved state or start a new game. - load(); - } - - void load() async { - //Access the box and get the first item at index 0 - //which will always be just one item of the Board model - //and here we don't need to call fromJson function of the board model - //in order to construct the Board model - //instead the adapter we added earlier will do that automatically. - //If there is no save locally it will start a new game. - state = _newGame(); - } - - // Create New Game state. - Board _newGame() { - return Board.newGame(state.best + state.score, [random([])]); - } - - // Start New Game - void newGame() { - state = _newGame(); - } - - // Check whether the indexes are in the same row or column in the board. - bool _inRange(index, nextIndex) { - return index < 4 && nextIndex < 4 || - index >= 4 && index < 8 && nextIndex >= 4 && nextIndex < 8 || - index >= 8 && index < 12 && nextIndex >= 8 && nextIndex < 12 || - index >= 12 && nextIndex >= 12; - } - - Tile _calculate(Tile tile, List tiles, direction) { - bool asc = direction == SwipeDirection.left || direction == SwipeDirection.up; - bool vert = direction == SwipeDirection.up || direction == SwipeDirection.down; - // Get the first index from the left in the row - // Example: for left swipe that can be: 0, 4, 8, 12 - // for right swipe that can be: 3, 7, 11, 15 - // depending which row in the column in the board we need - // let's say the title.index = 6 (which is the 3rd tile from the left and 2nd from right side, in the second row) - // ceil means it will ALWAYS round up to the next largest integer - // NOTE: don't confuse ceil it with floor or round as even if the value is 2.1 output would be 3. - // ((6 + 1) / 4) = 1.75 - // Ceil(1.75) = 2 - // If it's ascending: 2 * 4 – 4 = 4, which is the first index from the left side in the second row - // If it's descending: 2 * 4 – 1 = 7, which is the last index from the left side and first index from the right side in the second row - // If user swipes vertically use the verticalOrder list to retrieve the up/down index else use the existing index - int index = vert ? verticalOrder[tile.index] : tile.index; - int nextIndex = ((index + 1) / 4).ceil() * 4 - (asc ? 4 : 1); - - // If the list of the new tiles to be rendered is not empty get the last tile - // and if that tile is in the same row as the curren tile set the next index for the current tile to be after the last tile - if (tiles.isNotEmpty) { - var last = tiles.last; - // If user swipes vertically use the verticalOrder list to retrieve the up/down index else use the existing index - var lastIndex = last.nextIndex ?? last.index; - lastIndex = vert ? verticalOrder[lastIndex] : lastIndex; - if (_inRange(index, lastIndex)) { - // If the order is ascending set the tile after the last processed tile - // If the order is descending set the tile before the last processed tile - nextIndex = lastIndex + (asc ? 1 : -1); - } - } - - // Return immutable copy of the current tile with the new next index - // which can either be the top left index in the row or the last tile nextIndex/index + 1 - return tile.copyWith(nextIndex: vert ? verticalOrder.indexOf(nextIndex) : nextIndex); - } - - //Move the tile in the direction - bool move(SwipeDirection direction) { - bool asc = direction == SwipeDirection.left || direction == SwipeDirection.up; - bool vert = direction == SwipeDirection.up || direction == SwipeDirection.down; - // Sort the list of tiles by index. - // If user swipes vertically use the verticalOrder list to retrieve the up/down index - state.tiles.sort(((a, b) => - (asc ? 1 : -1) * - (vert ? verticalOrder[a.index].compareTo(verticalOrder[b.index]) : a.index.compareTo(b.index)))); - - List tiles = []; - - for (int i = 0, l = state.tiles.length; i < l; i++) { - var tile = state.tiles[i]; - - // Calculate nextIndex for current tile. - tile = _calculate(tile, tiles, direction); - tiles.add(tile); - - if (i + 1 < l) { - var next = state.tiles[i + 1]; - // Assign current tile nextIndex or index to the next tile if its allowed to be moved. - if (tile.value == next.value) { - // If user swipes vertically use the verticalOrder list to retrieve the up/down index else use the existing index - var index = vert ? verticalOrder[tile.index] : tile.index, - nextIndex = vert ? verticalOrder[next.index] : next.index; - if (_inRange(index, nextIndex)) { - tiles.add(next.copyWith(nextIndex: tile.nextIndex)); - // Skip next iteration if next tile was already assigned nextIndex. - i += 1; - continue; - } - } - } - } - - // Assign immutable copy of the new board state and trigger rebuild. - state = state.copyWith(tiles: tiles, undo: state); - return true; - } - - // Generates tiles at random place on the board - Tile random(List indexes) { - var i = 0; - var rng = Random(); - do { - i = rng.nextInt(16); - } while (indexes.contains(i)); - - return Tile(const Uuid().v4(), 2, i); - } - - //Merge tiles - void merge() { - List tiles = []; - var tilesMoved = false; - List indexes = []; - var score = state.score; - - for (int i = 0, l = state.tiles.length; i < l; i++) { - var tile = state.tiles[i]; - - var value = tile.value, merged = false; - - if (i + 1 < l) { - //sum the number of the two tiles with same index and mark the tile as merged and skip the next iteration. - var next = state.tiles[i + 1]; - if (tile.nextIndex == next.nextIndex || tile.index == next.nextIndex && tile.nextIndex == null) { - value = tile.value + next.value; - merged = true; - score += tile.value; - i += 1; - } - } - - if (merged || tile.nextIndex != null && tile.index != tile.nextIndex) { - tilesMoved = true; - } - - tiles.add(tile.copyWith(index: tile.nextIndex ?? tile.index, nextIndex: null, value: value, merged: merged)); - indexes.add(tiles.last.index); - } - - //If tiles got moved then generate a new tile at random position of the available positions on the board. - if (tilesMoved) { - tiles.add(random(indexes)); - } - state = state.copyWith(score: score, tiles: tiles); - } - - //Finish round, win or loose the game. - void _endRound() { - var gameOver = true, gameWon = false; - List tiles = []; - - //If there is no more empty place on the board - if (state.tiles.length == 16) { - state.tiles.sort(((a, b) => a.index.compareTo(b.index))); - - for (int i = 0, l = state.tiles.length; i < l; i++) { - var tile = state.tiles[i]; - - //If there is a tile with 2048 then the game is won. - if (tile.value == 2048) { - gameWon = true; - } - - var x = (i - (((i + 1) / 4).ceil() * 4 - 4)); - - if (x > 0 && i - 1 >= 0) { - //If tile can be merged with left tile then game is not lost. - var left = state.tiles[i - 1]; - if (tile.value == left.value) { - gameOver = false; - } - } - - if (x < 3 && i + 1 < l) { - //If tile can be merged with right tile then game is not lost. - var right = state.tiles[i + 1]; - if (tile.value == right.value) { - gameOver = false; - } - } - - if (i - 4 >= 0) { - //If tile can be merged with above tile then game is not lost. - var top = state.tiles[i - 4]; - if (tile.value == top.value) { - gameOver = false; - } - } - - if (i + 4 < l) { - //If tile can be merged with the bellow tile then game is not lost. - var bottom = state.tiles[i + 4]; - if (tile.value == bottom.value) { - gameOver = false; - } - } - //Set the tile merged: false - tiles.add(tile.copyWith(merged: false)); - } - } else { - //There is still a place on the board to add a tile so the game is not lost. - gameOver = false; - for (var tile in state.tiles) { - //If there is a tile with 2048 then the game is won. - if (tile.value == 2048) { - gameWon = true; - } - //Set the tile merged: false - tiles.add(tile.copyWith(merged: false)); - } - } - - state = state.copyWith(tiles: tiles, won: gameWon, over: gameOver); - } - - //Mark the merged as false after the merge animation is complete. - bool endRound() { - //End round. - _endRound(); - ref.read(roundManager.notifier).end(); - - //If player moved too fast before the current animation/transition finished, start the move for the next direction - var nextDirection = ref.read(nextDirectionManager); - if (nextDirection != null) { - move(nextDirection); - ref.read(nextDirectionManager.notifier).clear(); - return true; - } - return false; - } - - //undo one round only - void undo() { - if (state.undo != null) { - state = state.copyWith(score: state.undo!.score, best: state.undo!.best, tiles: state.undo!.tiles); - } - } - - //Move the tiles using the arrow keys on the keyboard. - bool onKey(RawKeyEvent event) { - SwipeDirection? direction; - if (event.isKeyPressed(LogicalKeyboardKey.arrowRight)) { - direction = SwipeDirection.right; - } else if (event.isKeyPressed(LogicalKeyboardKey.arrowLeft)) { - direction = SwipeDirection.left; - } else if (event.isKeyPressed(LogicalKeyboardKey.arrowUp)) { - direction = SwipeDirection.up; - } else if (event.isKeyPressed(LogicalKeyboardKey.arrowDown)) { - direction = SwipeDirection.down; - } - - if (direction != null) { - move(direction); - return true; - } - return false; - } - - void save() async { - //Here we don't need to call toJson function of the board model - //in order to convert the data to json - //instead the adapter we added earlier will do that automatically. - } -} - -final boardManager = StateNotifierProvider((ref) { - return BoardManager(ref); -}); diff --git a/lib/game/2048/managers/next_direction.dart b/lib/game/2048/managers/next_direction.dart deleted file mode 100644 index 859208469..000000000 --- a/lib/game/2048/managers/next_direction.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_swipe_detector/flutter_swipe_detector.dart'; - -/* -In case user swipes too fast we prevent for the next round to start until the current round finishes, we do that using the RoundManager, -but instead of canceling that round we will queue it so that the round automatically starts as soon the current finishes, -that way we will prevent the user feeling like the game is lag-ish or slow. -*/ -class NextDirectionManager extends StateNotifier { - NextDirectionManager() : super(null); - - void queue(direction) { - state = direction; - } - - void clear() { - state = null; - } -} - -final nextDirectionManager = StateNotifierProvider((ref) { - return NextDirectionManager(); -}); diff --git a/lib/game/2048/managers/round.dart b/lib/game/2048/managers/round.dart deleted file mode 100644 index 9eaa140d0..000000000 --- a/lib/game/2048/managers/round.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -/* -A Notifier when a round starts, in order to prevent the next round starts before the current ends -prevents animation issues when user tries to move tiles too soon. -*/ -class RoundManager extends StateNotifier { - RoundManager() : super(true); - - void end() { - state = true; - } - - void begin() { - state = false; - } -} - -final roundManager = StateNotifierProvider((ref) { - return RoundManager(); -}); diff --git a/lib/game/2048/models/board.dart b/lib/game/2048/models/board.dart deleted file mode 100644 index a65adb6e3..000000000 --- a/lib/game/2048/models/board.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; - -import '../models/tile.dart'; - -part 'board.g.dart'; - -@JsonSerializable(explicitToJson: true, anyMap: true) -class Board { - //Current score on the board - final int score; - - //Best score so far - final int best; - - //Current list of tiles shown on the board - final List tiles; - - //Whether the game is over or not - final bool over; - - //Whether the game is won or not - final bool won; - - //Keeps the previous round board state used for the undo functionality - final Board? undo; - - Board(this.score, this.best, this.tiles, {this.over = false, this.won = false, this.undo}); - - //Create a model for a new game. - Board.newGame(this.best, this.tiles) - : score = 0, - over = false, - won = false, - undo = null; - - //Create an immutable copy of the board - Board copyWith({int? score, int? best, List? tiles, bool? over, bool? won, Board? undo}) => - Board(score ?? this.score, best ?? this.best, tiles ?? this.tiles, - over: over ?? this.over, won: won ?? this.won, undo: undo ?? this.undo); - - //Create a Board from json data - factory Board.fromJson(Map json) => _$BoardFromJson(json); - - //Generate json data from the Board - Map toJson() => _$BoardToJson(this); -} diff --git a/lib/game/2048/models/board.g.dart b/lib/game/2048/models/board.g.dart deleted file mode 100644 index 71decac2f..000000000 --- a/lib/game/2048/models/board.g.dart +++ /dev/null @@ -1,25 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'board.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -Board _$BoardFromJson(Map json) => Board( - json['score'] as int, - json['best'] as int, - (json['tiles'] as List).map((e) => Tile.fromJson(Map.from(e as Map))).toList(), - over: json['over'] as bool? ?? false, - won: json['won'] as bool? ?? false, - undo: json['undo'] == null ? null : Board.fromJson(Map.from(json['undo'] as Map)), - ); - -Map _$BoardToJson(Board instance) => { - 'score': instance.score, - 'best': instance.best, - 'tiles': instance.tiles.map((e) => e.toJson()).toList(), - 'over': instance.over, - 'won': instance.won, - 'undo': instance.undo?.toJson(), - }; diff --git a/lib/game/2048/models/board_adapter.dart b/lib/game/2048/models/board_adapter.dart deleted file mode 100644 index a90ca23bc..000000000 --- a/lib/game/2048/models/board_adapter.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:hive_flutter/hive_flutter.dart'; - -import 'board.dart'; - -class BoardAdapter extends TypeAdapter { - @override - final typeId = 0; - - @override - Board read(BinaryReader reader) { - //Create a Board model from the json when reading the data that's being stored. - return Board.fromJson(Map.from(reader.read())); - } - - @override - void write(BinaryWriter writer, Board obj) { - //Store the board model as json when writing the data to the database. - writer.write(obj.toJson()); - } -} diff --git a/lib/game/2048/models/tile.dart b/lib/game/2048/models/tile.dart deleted file mode 100644 index 24e470a97..000000000 --- a/lib/game/2048/models/tile.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; - -part 'tile.g.dart'; - -@JsonSerializable(anyMap: true) -class Tile { - //Unique id used as ValueKey for the TileWidget - final String id; - - //The number on the tile - final int value; - - //The index of the tile on the board from which the position of the tile will be calculated - final int index; - - //The next index of the tile on the board - final int? nextIndex; - - //Whether the tile was merged with another tile - final bool merged; - - Tile(this.id, this.value, this.index, {this.nextIndex, this.merged = false}); - - //Calculate the current top position based on the current index - double getTop(double size) { - var i = ((index + 1) / 4).ceil(); - return ((i - 1) * size) + (12.0 * i); - } - - //Calculate the current left position based on the current index - double getLeft(double size) { - var i = (index - (((index + 1) / 4).ceil() * 4 - 4)); - return (i * size) + (12.0 * (i + 1)); - } - - //Calculate the next top position based on the next index - double? getNextTop(double size) { - if (nextIndex == null) return null; - var i = ((nextIndex! + 1) / 4).ceil(); - return ((i - 1) * size) + (12.0 * i); - } - - //Calculate the next top position based on the next index - double? getNextLeft(double size) { - if (nextIndex == null) return null; - var i = (nextIndex! - (((nextIndex! + 1) / 4).ceil() * 4 - 4)); - return (i * size) + (12.0 * (i + 1)); - } - - //Create an immutable copy of the tile - Tile copyWith({String? id, int? value, int? index, int? nextIndex, bool? merged}) => - Tile(id ?? this.id, value ?? this.value, index ?? this.index, - nextIndex: nextIndex ?? this.nextIndex, merged: merged ?? this.merged); - - //Create a Tile from json data - factory Tile.fromJson(Map json) => _$TileFromJson(json); - - //Generate json data from the Tile - Map toJson() => _$TileToJson(this); -} diff --git a/lib/game/2048/models/tile.g.dart b/lib/game/2048/models/tile.g.dart deleted file mode 100644 index 4283639fe..000000000 --- a/lib/game/2048/models/tile.g.dart +++ /dev/null @@ -1,23 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'tile.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -Tile _$TileFromJson(Map json) => Tile( - json['id'] as String, - json['value'] as int, - json['index'] as int, - nextIndex: json['nextIndex'] as int?, - merged: json['merged'] as bool? ?? false, - ); - -Map _$TileToJson(Tile instance) => { - 'id': instance.id, - 'value': instance.value, - 'index': instance.index, - 'nextIndex': instance.nextIndex, - 'merged': instance.merged, - }; diff --git a/lib/index.dart b/lib/index.dart deleted file mode 100644 index 7c5040302..000000000 --- a/lib/index.dart +++ /dev/null @@ -1,170 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:sit/timetable/i18n.dart' as $timetable; -import 'package:sit/school/i18n.dart' as $school; -import 'package:sit/life/i18n.dart' as $life; -import 'package:sit/me/i18n.dart' as $me; -import 'package:rettulf/rettulf.dart'; - -class MainStagePage extends StatefulWidget { - final StatefulNavigationShell navigationShell; - - const MainStagePage({super.key, required this.navigationShell}); - - @override - State createState() => _MainStagePageState(); -} - -typedef _NavigationDest = ({Widget icon, Widget activeIcon, String label}); - -extension _NavigationDestX on _NavigationDest { - BottomNavigationBarItem toBarItem() { - return BottomNavigationBarItem(icon: icon, activeIcon: activeIcon, label: label); - } - - NavigationRailDestination toRailDest() { - return NavigationRailDestination(icon: icon, selectedIcon: activeIcon, label: label.text()); - } -} - -class _MainStagePageState extends State { - var currentStage = 0; - late var items = buildItems(); - - List<({String route, ({Widget icon, Widget activeIcon, String label}) item})> buildItems() { - return [ - ( - route: "/timetable", - item: ( - icon: const Icon(Icons.calendar_month_outlined), - activeIcon: const Icon(Icons.calendar_month), - label: $timetable.i18n.navigation, - ) - ), - if (!kIsWeb) - ( - route: "/school", - item: ( - icon: const Icon(Icons.school_outlined), - activeIcon: const Icon(Icons.school), - label: $school.i18n.navigation, - ) - ), - if (!kIsWeb) - ( - route: "/life", - item: ( - icon: const Icon(Icons.spa_outlined), - activeIcon: const Icon(Icons.spa), - label: $life.i18n.navigation, - ) - ), - ( - route: "/me", - item: ( - icon: const Icon(Icons.person_outline), - activeIcon: const Icon(Icons.person), - label: $me.i18n.navigation, - ) - ), - ]; - } - - @override - void didChangeDependencies() { - items = buildItems(); - super.didChangeDependencies(); - } - - @override - Widget build(BuildContext context) { - if (context.isPortrait) { - final isKeyboardOpen = MediaQuery.of(context).viewInsets.bottom != 0.0; - return Scaffold( - body: widget.navigationShell, - bottomNavigationBar: isKeyboardOpen ? null : buildButtonNavigationBar(), - ); - } else { - return Scaffold( - body: [ - buildNavigationRail(), - const VerticalDivider(), - widget.navigationShell.expanded(), - ].row(), - ); - } - } - - Widget buildButtonNavigationBar() { - return BottomNavigationBar( - useLegacyColorScheme: false, - showUnselectedLabels: false, - enableFeedback: true, - type: BottomNavigationBarType.fixed, - landscapeLayout: BottomNavigationBarLandscapeLayout.centered, - currentIndex: getSelectedIndex(), - onTap: onItemTapped, - items: items.map((e) => e.item.toBarItem()).toList(), - ); - } - - Widget buildNavigationRail() { - return NavigationRail( - labelType: NavigationRailLabelType.all, - selectedIndex: getSelectedIndex(), - onDestinationSelected: onItemTapped, - destinations: items.map((e) => e.item.toRailDest()).toList(), - ); - } - - int getSelectedIndex() { - final location = GoRouterState.of(context).uri.toString(); - return items.indexWhere((e) => location.startsWith(e.route)); - } - - void onItemTapped(int index) { - widget.navigationShell.goBranch( - index, - initialLocation: index == widget.navigationShell.currentIndex, - ); - } -} - -abstract class DrawerDelegateProtocol { - const DrawerDelegateProtocol(); - - void openDrawer(); - - void closeDrawer(); - - void openEndDrawer(); - - void closeEndDrawer(); -} - -class DrawerDelegate extends DrawerDelegateProtocol { - final GlobalKey key; - - const DrawerDelegate(this.key); - - @override - void openDrawer() { - key.currentState?.openDrawer(); - } - - @override - void closeDrawer() { - key.currentState?.closeDrawer(); - } - - @override - void openEndDrawer() { - key.currentState?.openEndDrawer(); - } - - @override - void closeEndDrawer() { - key.currentState?.closeEndDrawer(); - } -} diff --git a/lib/init.dart b/lib/init.dart deleted file mode 100644 index c8b27c1d3..000000000 --- a/lib/init.dart +++ /dev/null @@ -1,116 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:sit/design/adaptive/editor.dart'; -import 'package:sit/entity/campus.dart'; - -import 'package:flutter/foundation.dart'; -import 'package:sit/credentials/init.dart'; -import 'package:sit/storage/hive/init.dart'; -import 'package:sit/session/class2nd.dart'; -import 'package:sit/session/gms.dart'; -import 'package:sit/session/library.dart'; -import 'package:sit/session/ywb.dart'; -import 'package:sit/life/electricity/init.dart'; -import 'package:sit/life/expense_records/init.dart'; -import 'package:sit/login/init.dart'; -import 'package:sit/me/edu_email/init.dart'; -import 'package:sit/school/ywb/init.dart'; -import 'package:sit/school/exam_arrange/init.dart'; -import 'package:sit/school/library/init.dart'; -import 'package:sit/school/oa_announce/init.dart'; -import 'package:sit/school/class2nd/init.dart'; -import 'package:sit/school/exam_result/init.dart'; -import 'package:sit/school/yellow_pages/init.dart'; -import 'package:sit/session/jwxt.dart'; -import 'package:sit/timetable/init.dart'; -import 'dart:async'; - -import 'package:cookie_jar/cookie_jar.dart'; -import 'package:dio/dio.dart'; -import 'package:sit/storage/hive/cookie.dart'; -import 'package:sit/network/dio.dart'; -import 'package:sit/route.dart'; -import 'package:sit/session/sso.dart'; - -import '../widgets/captcha_box.dart'; - -class Init { - const Init._(); - - static late CookieJar cookieJar; - static late Dio dio; - static late SsoSession ssoSession; - static late JwxtSession jwxtSession; - static late GmsSession gmsSession; - static late YwbSession ywbSession; - static late LibrarySession librarySession; - static late Class2ndSession class2ndSession; - - static Future initNetwork() async { - debugPrint("Initializing network"); - if (kIsWeb) { - cookieJar = WebCookieJar(); - } else { - cookieJar = PersistCookieJar( - storage: HiveCookieJar(HiveInit.cookies), - ); - } - dio = await DioInit.init( - cookieJar: cookieJar, - ); - ssoSession = SsoSession( - dio: dio, - cookieJar: cookieJar, - onError: (error, stackTrace) { - debugPrint(error.toString()); - debugPrintStack(stackTrace: stackTrace); - }, - inputCaptcha: (Uint8List imageBytes) async { - final context = $Key.currentContext!; - return await showAdaptiveDialog( - context: context, - barrierDismissible: false, - builder: (context) => CaptchaDialog(captchaData: imageBytes), - ); - }, - ); - jwxtSession = JwxtSession( - ssoSession: ssoSession, - ); - ywbSession = YwbSession( - dio: dio, - ); - librarySession = LibrarySession( - dio: dio, - ); - class2ndSession = Class2ndSession( - ssoSession: ssoSession, - ); - gmsSession = GmsSession( - ssoSession: ssoSession, - ); - } - - static Future initModules() async { - debugPrint("Initializing module storage"); - CredentialsInit.init(); - TimetableInit.init(); - if (!kIsWeb) { - OaAnnounceInit.init(); - ExamResultInit.init(); - ExamArrangeInit.init(); - ExpenseRecordsInit.init(); - LibraryInit.init(); - YwbInit.init(); - Class2ndInit.init(); - ElectricityBalanceInit.init(); - } - YellowPagesInit.init(); - EduEmailInit.init(); - LoginInit.init(); - } - - static void registerCustomEditor() { - EditorEx.registerEnumEditor(Campus.values); - EditorEx.registerEnumEditor(ThemeMode.values); - } -} diff --git a/lib/l10n/common.dart b/lib/l10n/common.dart deleted file mode 100644 index 3c3f6e874..000000000 --- a/lib/l10n/common.dart +++ /dev/null @@ -1,144 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; - -mixin class CommonI18nMixin { - String get open => "open".tr(); - - String get delete => "delete".tr(); - - String get confirm => "confirm".tr(); - - String get notNow => "notNow".tr(); - - String get error => "error".tr(); - - String get ok => "ok".tr(); - - String get yes => "yes".tr(); - - String get refresh => "refresh".tr(); - - String get close => "close".tr(); - - String get submit => "submit".tr(); - - String get cancel => "cancel".tr(); - - String get back => "back".tr(); - - String get clear => "clear".tr(); - - String get save => "save".tr(); - - String get continue$ => "continue".tr(); - - String get unknown => "unknown".tr(); - - String get failed => "failed".tr(); - - String get download => "download".tr(); - - String get fetching => "fetching".tr(); - - String get warning => "warning".tr(); - - String get exceptionInfo => "exceptionInfo".tr(); - - String get untitled => "untitled".tr(); - - String get congratulations => "congratulations".tr(); - - String get search => "search".tr(); - - String get seeAll => "seeAll".tr(); - - String get select => "select".tr(); - - String get unselect => "unselect".tr(); - - String get share => "share".tr(); - - String get edit => "edit".tr(); - - String get use => "use".tr(); - - String get used => "used".tr(); - - String get preview => "preview".tr(); - - String get copy => "copy".tr(); - - String get upload => "upload".tr(); - - String get pick => "pick".tr(); - - String get retry => "retry".tr(); - - String get duplicate => "duplicate".tr(); - - String copyTipOf(String item) => "copyTip".tr(args: [item]); - - String get done => "done".tr(); -} - -class CommonI18n with CommonI18nMixin { - const CommonI18n(); -} - -class NetworkI18n { - const NetworkI18n(); - - static const ns = "network"; - - String get error => "$ns.error".tr(); - - String get ipAddress => "$ns.ipAddress".tr(); - - String get connectionTimeoutError => "$ns.connectionTimeoutError".tr(); - - String get connectionTimeoutErrorDesc => "$ns.connectionTimeoutErrorDesc".tr(); - - String get openToolBtn => "$ns.openToolBtn".tr(); - - String get noAccessTip => "$ns.noAccessTip".tr(); - - String get untitled => "$ns.untitled".tr(); -} - -class UnitI18n { - const UnitI18n(); - - static const ns = "unit"; - - String rmb(String amount) => "$ns.rmb".tr(args: [amount]); - - String powerKwh(String amount) => "$ns.powerKwh".tr(args: [amount]); -} - -class TimeI18n { - const TimeI18n(); - - static const ns = "time"; - - String get minute => "$ns.minute".tr(); - - String get hour => "$ns.hour".tr(); - - String hourMinuteFormat(String hour, String minute) => "$ns.hourMinuteFormat".tr(namedArgs: { - "hour": hour, - "minute": minute, - }); - - String hourFormat(String hour) => "$ns.hourFormat".tr(args: [hour]); - - String minuteFormat(String minute) => "$ns.minuteFormat".tr(args: [minute]); -} - -class CampusI10n { - const CampusI10n(); - - static const ns = "campus"; - - String get xuhui => "$ns.xuhui".tr(); - - String get fengxian => "$ns.fengxian".tr(); -} diff --git a/lib/l10n/extension.dart b/lib/l10n/extension.dart deleted file mode 100644 index 988386ba1..000000000 --- a/lib/l10n/extension.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:sit/l10n/time.dart'; - -import 'lang.dart'; - -export 'package:sit/r.dart'; - -export 'lang.dart'; - -extension I18nBuildContext on BuildContext { - ///e.g.: Wednesday, September 21, 2022 - String formatYmdWeekText(DateTime date) => Lang.formatOf(locale).ymdWeekText.format(date); - - ///e.g.: Wednesday, September 21 - String formatMdWeekText(DateTime date) => Lang.formatOf(locale).mdWeekText.format(date); - - ///e.g.: September 21, 2022 - String formatYmdText(DateTime date) => Lang.formatOf(locale).ymdText.format(date); - - ///e.g.: 9/21/2022 - String formatYmdNum(DateTime date) => Lang.formatOf(locale).ymdNum.format(date); - - ///e.g.: 9/21/2022 23:57:23 - String formatYmdhmsNum(DateTime date) => Lang.formatOf(locale).ymdhmsNum.format(date); - - ///e.g.: 9/21/2022 23:57 - String formatYmdhmNum(DateTime date) => Lang.formatOf(locale).ymdhmNum.format(date); - - String formatYmText(DateTime date) => Lang.formatOf(locale).ymText.format(date); - - /// e.g.: 8:32:59 - String formatHmsNum(DateTime date) => Lang.formatOf(locale).hms.format(date); - - /// e.g.: 8:32 - String formatHmNum(DateTime date) => Lang.formatOf(locale).hm.format(date); - - /// e.g.: 9/21 - String formatMdNum(DateTime date) => Lang.formatOf(locale).mdNum.format(date); - - /// e.g.: 9/21 7:32 - String formatMdhmNum(DateTime date) => Lang.formatOf(locale).mdHmNum.format(date); - - Weekday firstDayInWeek() => Lang.formatOf(locale).firstDayInWeek; -} - -extension BrightnessL10nX on Brightness { - String l10n() => "brightness.$name".tr(); -} - -extension ThemeModeL10nX on ThemeMode { - String l10n() => "themeMode.$name".tr(); -} diff --git a/lib/l10n/lang.dart b/lib/l10n/lang.dart deleted file mode 100644 index d8e527b64..000000000 --- a/lib/l10n/lang.dart +++ /dev/null @@ -1,127 +0,0 @@ -import 'dart:ui'; - -import 'package:intl/intl.dart'; -import 'package:sit/l10n/time.dart'; -import 'package:sit/r.dart'; - -abstract class RegionalFormatter { - DateFormat get hms; - - DateFormat get hm; - - DateFormat get ymdText; - - DateFormat get ymdWeekText; - - DateFormat get mdWeekText; - - DateFormat get ymText; - - DateFormat get ymdNum; - - DateFormat get ymdhmsNum; - - DateFormat get ymdhmNum; - - DateFormat get mdHmNum; - - DateFormat get mdNum; - - Weekday get firstDayInWeek; -} - -class Lang { - Lang._(); - - static final zhHansFormatter = _ZhHansFormatter(); - static final zhHantFormatter = _ZhHantFormatter(); - static final enFormatter = _EnUsFormatter(); - static final locale2Format = { - R.enLocale: _EnUsFormatter(), - R.zhHansLocale: _ZhHansFormatter(), - R.zhHantLocale: _ZhHantFormatter(), - }; - - static RegionalFormatter formatOf(Locale locale) => locale2Format[locale] ?? zhHansFormatter; -} - -class _ZhHansFormatter implements RegionalFormatter { - @override - final hms = DateFormat("H:mm:ss"); - @override - final hm = DateFormat("H:mm"); - @override - final ymdText = DateFormat("yyyy年M月d日", "zh_Hans"); - @override - final ymdWeekText = DateFormat("yyyy年M月d日 EEEE", "zh_Hans"); - @override - final mdWeekText = DateFormat("M月d日 EEEE", "zh_Hans"); - @override - final ymText = DateFormat("yyyy年M月", "zh_Hans"); - @override - final ymdNum = DateFormat("yyyy/M/d", "zh_Hans"); - @override - final ymdhmsNum = DateFormat("yyyy/M/d H:mm:ss", "zh_Hans"); - @override - final ymdhmNum = DateFormat("yyyy/M/d H:mm:ss", "zh_Hans"); - @override - final mdHmNum = DateFormat("M/d H:mm", "zh_Hans"); - @override - final mdNum = DateFormat("M/d", "zh_Hans"); - @override - final firstDayInWeek = Weekday.monday; -} - -class _ZhHantFormatter implements RegionalFormatter { - @override - final hms = DateFormat("H:mm:ss"); - @override - final hm = DateFormat("H:mm"); - @override - final ymdText = DateFormat("yyyy年M月d日", "zh_Hant"); - @override - final ymdWeekText = DateFormat("yyyy年M月d日 EEEE", "zh_Hant"); - @override - final mdWeekText = DateFormat("M月d日 EEEE", "zh_Hant"); - @override - final ymText = DateFormat("yyyy年M月", "zh_Hant"); - @override - final ymdNum = DateFormat("yyyy/M/d", "zh_Hant"); - @override - final ymdhmsNum = DateFormat("yyyy/M/d H:mm:ss", "zh_Hant"); - @override - final ymdhmNum = DateFormat("yyyy/M/d H:mm:ss", "zh_Hant"); - @override - final mdHmNum = DateFormat("M/d H:mm", "zh_Hant"); - @override - final mdNum = DateFormat("M/d", "zh_Hant"); - @override - final firstDayInWeek = Weekday.monday; -} - -class _EnUsFormatter implements RegionalFormatter { - @override - final hms = DateFormat.jms(); - @override - final hm = DateFormat.jm(); - @override - final ymdText = DateFormat("MMMM d, yyyy", "en_US"); - @override - final ymdWeekText = DateFormat("EEEE, MMMM d, yyyy", "en_US"); - @override - final mdWeekText = DateFormat("EEEE, MMMM d", "en_US"); - @override - final ymText = DateFormat("MMMM, yyyy", "en_US"); - @override - final ymdNum = DateFormat("M/d/yyyy", "en_US"); - @override - final ymdhmsNum = DateFormat("M/d/yyyy", "en_US").add_jms(); - @override - final ymdhmNum = DateFormat("M/d/yyyy", "en_US").add_jm(); - @override - final mdHmNum = DateFormat("M/d", "en_US").add_jm(); - @override - final mdNum = DateFormat("M/d", "en_US"); - @override - final firstDayInWeek = Weekday.sunday; -} diff --git a/lib/l10n/time.dart b/lib/l10n/time.dart deleted file mode 100644 index ac33ead91..000000000 --- a/lib/l10n/time.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; - -enum Weekday { - monday, - tuesday, - wednesday, - thursday, - friday, - saturday, - sunday; - - int toJson() => index; - - int getIndex({required Weekday firstDay}) { - return (this - firstDay.index).index; - } - - factory Weekday.fromJson(int json) => Weekday.values.elementAtOrNull(json) ?? Weekday.monday; - - factory Weekday.fromIndex(int index) { - assert(0 <= index && index < Weekday.values.length); - return Weekday.values[index % Weekday.values.length]; - } - - String l10n() => "weekday.$index".tr(); - - String l10nShort() => "weekdayShort.$index".tr(); - - static List genSequence(Weekday firstDay) { - return List.generate(7, (index) => firstDay + index); - } - - Weekday operator +(int delta) { - return Weekday.values[(index + delta) % Weekday.values.length]; - } - - Weekday operator -(int delta) { - return Weekday.values[(index - delta) % Weekday.values.length]; - } - - List genSequenceStartWithThis() => genSequence(this); -} diff --git a/lib/l10n/tr.dart b/lib/l10n/tr.dart deleted file mode 100644 index 7967cf013..000000000 --- a/lib/l10n/tr.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/widgets.dart'; - -extension TrX on String { - List trSpan({ - BuildContext? context, - required Map args, - }) { - final translated = this.tr( - namedArgs: args.map((k, v) => MapEntry(k, "{$k}")), - ); - return replaceWidget(raw: translated, args: args); - } -} - -List replaceWidget({ - required String raw, - required Map args, -}) { - List spans = []; - RegExp regExp = RegExp(r'{(.*?)}'); - Iterable matches = regExp.allMatches(raw); - int currentIndex = 0; - - for (Match match in matches) { - spans.add(TextSpan(text: raw.substring(currentIndex, match.start))); - final key = match.group(1); - if (key == null) { - spans.add(const TextSpan(text: "?")); - } else { - final replaced = args[key]; - if (replaced == null) { - spans.add(TextSpan(text: key)); - } else { - spans.add(replaced); - } - } - currentIndex = match.end; - } - - if (currentIndex < raw.length) { - spans.add(TextSpan(text: raw.substring(currentIndex))); - } - return spans; -} diff --git a/lib/l10n/yaml_assets_loader.dart b/lib/l10n/yaml_assets_loader.dart deleted file mode 100644 index 31a8f3529..000000000 --- a/lib/l10n/yaml_assets_loader.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'dart:developer'; -import 'dart:ui'; - -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/services.dart'; -import 'package:yaml/yaml.dart'; - -//Loader for multiple yaml files -class YamlAssetLoader extends AssetLoader { - String getLocalePath(String basePath, Locale locale) { - return '$basePath/${locale.toStringWithSeparator(separator: "-")}.yaml'; - } - - @override - Future> load(String path, Locale locale) async { - var localePath = getLocalePath(path, locale); - log('easy localization loader: load yaml file $localePath'); - YamlMap yaml = loadYaml(await rootBundle.loadString(localePath)); - return convertYamlMapToMap(yaml); - } -} - -//Loader for single yaml file -class YamlSingleAssetLoader extends AssetLoader { - Map? yamlData; - - @override - Future> load(String path, Locale locale) async { - if (yamlData == null) { - log('easy localization loader: load yaml file $path'); - yamlData = convertYamlMapToMap(loadYaml(await rootBundle.loadString(path))); - } else { - log('easy localization loader: Yaml already loaded, read cache'); - } - return yamlData![locale.toString()]; - } -} - -/// Convert YamlMap to Map -Map convertYamlMapToMap(YamlMap yamlMap) { - final map = {}; - - for (final entry in yamlMap.entries) { - if (entry.value is YamlMap || entry.value is Map) { - map[entry.key.toString()] = convertYamlMapToMap(entry.value); - } else { - map[entry.key.toString()] = entry.value.toString(); - } - } - return map; -} diff --git a/lib/life/electricity/entity/balance.dart b/lib/life/electricity/entity/balance.dart deleted file mode 100644 index 22e6b68e1..000000000 --- a/lib/life/electricity/entity/balance.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; -import 'package:sit/storage/hive/type_id.dart'; - -part 'balance.g.dart'; - -double _parseBalance(String raw) { - return double.tryParse(raw) ?? 0; -} - -/// 0.61 RMB/kWh -const rmbPerKwh = 0.61; - -/// ```json -/// [{ -/// "RoomName":"105604", -/// "BaseBalance":"53.6640", -/// "ElecBalance":"0.0000", -/// "Balance":"53.6640" -/// }] -/// ``` -@JsonSerializable(createToJson: false) -@HiveType(typeId: CacheHiveType.electricityBalance) -class ElectricityBalance { - @JsonKey(name: "Balance", fromJson: _parseBalance) - @HiveField(0) - final double balance; - - @JsonKey(name: "BaseBalance", fromJson: _parseBalance) - @HiveField(1) - final double baseBalance; - - @JsonKey(name: "ElecBalance", fromJson: _parseBalance) - @HiveField(2) - final double electricityBalance; - - @JsonKey(name: "RoomName") - @HiveField(3) - final String roomNumber; - - const ElectricityBalance({ - required this.roomNumber, - required this.balance, - required this.baseBalance, - required this.electricityBalance, - }); - - factory ElectricityBalance.fromJson(Map json) => _$ElectricityBalanceFromJson(json); - - double get remainingPower => balance / rmbPerKwh; - - @override - String toString() { - return { - "balance": balance, - "baseBalance": baseBalance, - "electricityBalance": electricityBalance, - "roomNumber": roomNumber, - }.toString(); - } -} diff --git a/lib/life/electricity/entity/balance.g.dart b/lib/life/electricity/entity/balance.g.dart deleted file mode 100644 index e025c8f0e..000000000 --- a/lib/life/electricity/entity/balance.g.dart +++ /dev/null @@ -1,59 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'balance.dart'; - -// ************************************************************************** -// TypeAdapterGenerator -// ************************************************************************** - -class ElectricityBalanceAdapter extends TypeAdapter { - @override - final int typeId = 60; - - @override - ElectricityBalance read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return ElectricityBalance( - roomNumber: fields[3] as String, - balance: fields[0] as double, - baseBalance: fields[1] as double, - electricityBalance: fields[2] as double, - ); - } - - @override - void write(BinaryWriter writer, ElectricityBalance obj) { - writer - ..writeByte(4) - ..writeByte(0) - ..write(obj.balance) - ..writeByte(1) - ..write(obj.baseBalance) - ..writeByte(2) - ..write(obj.electricityBalance) - ..writeByte(3) - ..write(obj.roomNumber); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is ElectricityBalanceAdapter && runtimeType == other.runtimeType && typeId == other.typeId; -} - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -ElectricityBalance _$ElectricityBalanceFromJson(Map json) => ElectricityBalance( - roomNumber: json['RoomName'] as String, - balance: _parseBalance(json['Balance'] as String), - baseBalance: _parseBalance(json['BaseBalance'] as String), - electricityBalance: _parseBalance(json['ElecBalance'] as String), - ); diff --git a/lib/life/electricity/entity/room.dart b/lib/life/electricity/entity/room.dart deleted file mode 100644 index b536c7673..000000000 --- a/lib/life/electricity/entity/room.dart +++ /dev/null @@ -1,148 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:sit/utils/strings.dart'; - -class DormitoryRoom { - final int building; - final int floorWithRoom; - final int floor; - final int room; - - const DormitoryRoom({ - required this.building, - required this.floorWithRoom, - required this.floor, - required this.room, - }); - - /// For buildings #1 through #10, their floors are all below 10. - /// ## examples: - /// - 101301: building #1, room #301 - /// - 10251524: building #25, room #1524 - factory DormitoryRoom.fromFullString(String full) { - full = full.removePrefix("10"); - assert(full.length >= 4 && full.length <= 6, '"$full" is too long.'); - if (full.length == 4) { - // building is in 1 digit, like 1 301 - final buildingRaw = full[0]; - final floorWithRoomNumberRaw = full.substring(1); - final floorNumberRaw = full.substring(1, 2); - final roomNumberRaw = full.substring(2); - return DormitoryRoom( - building: int.tryParse(buildingRaw) ?? 0, - floorWithRoom: int.tryParse(floorWithRoomNumberRaw) ?? 0, - floor: int.tryParse(floorNumberRaw) ?? 0, - room: int.tryParse(roomNumberRaw) ?? 0, - ); - } else if (full.length == 5) { - // building is in 2 digit,like 12 301 - final buildingRaw = full.substring(0, 2); - final floorWithRoomNumber = full.substring(2); - final floorNumberRaw = full.substring(2, 3); - final roomNumberRaw = full.substring(3); - return DormitoryRoom( - building: int.tryParse(buildingRaw) ?? 0, - floorWithRoom: int.tryParse(floorWithRoomNumber) ?? 0, - floor: int.tryParse(floorNumberRaw) ?? 0, - room: int.tryParse(roomNumberRaw) ?? 0, - ); - } else if (full.length == 6) { - // building is in 2 digit,like 12 1301 - final buildingRaw = full.substring(0, 2); - final floorWithRoomNumber = full.substring(2); - final floorNumberRaw = full.substring(2, 4); - final roomNumberRaw = full.substring(4); - return DormitoryRoom( - building: int.tryParse(buildingRaw) ?? 0, - floorWithRoom: int.tryParse(floorWithRoomNumber) ?? 0, - floor: int.tryParse(floorNumberRaw) ?? 0, - room: int.tryParse(roomNumberRaw) ?? 0, - ); - } - // fallback - return const DormitoryRoom(building: 0, floorWithRoom: 0, floor: 0, room: 0); - } - - @override - String toString() { - return "Building $building #$floorWithRoom"; - } - - String l10n() { - return "dormitoryRoom".tr(namedArgs: { - "building": building.toString(), - "room": floorWithRoom.toString(), - }); - } - - static void quickSort(List rooms, int Function(int, int) compare) { - if (rooms.length < 2) { - return; - } - - List stack = []; - stack.add(0); - stack.add(rooms.length - 1); - - while (stack.isNotEmpty) { - int end = stack.removeLast(); - int start = stack.removeLast(); - - int pivotIndex = partition(rooms, start, end, compare); - - if (pivotIndex - 1 > start) { - stack.add(start); - stack.add(pivotIndex - 1); - } - - if (pivotIndex + 1 < end) { - stack.add(pivotIndex + 1); - stack.add(end); - } - } - } - - static int partition(List rooms, int start, int end, int Function(int, int) compare) { - int pivot = rooms[end]; - int i = start - 1; - - for (int j = start; j < end; j++) { - if (compare(rooms[j], pivot) <= 0) { - i++; - _swap(rooms, i, j); - } - } - - _swap(rooms, i + 1, end); - return i + 1; - } - - static void _swap(List rooms, int i, int j) { - int temp = rooms[i]; - rooms[i] = rooms[j]; - rooms[j] = temp; - } - - static int compare(int room1, int room2) { - var dormitoryRoom1 = DormitoryRoom.fromFullString(room1.toString()); - var dormitoryRoom2 = DormitoryRoom.fromFullString(room2.toString()); - if (dormitoryRoom1.building < dormitoryRoom2.building) { - return -1; - } else if (dormitoryRoom1.building > dormitoryRoom2.building) { - return 1; - } else { - if (dormitoryRoom1.floor < dormitoryRoom2.floor) { - return -1; - } else if (dormitoryRoom1.floor > dormitoryRoom2.floor) { - return 1; - } else { - if (dormitoryRoom1.room < dormitoryRoom2.room) { - return -1; - } else if (dormitoryRoom1.room > dormitoryRoom2.room) { - return 1; - } else { - return 0; - } - } - } - } -} diff --git a/lib/life/electricity/i18n.dart b/lib/life/electricity/i18n.dart deleted file mode 100644 index fcf5d7a4f..000000000 --- a/lib/life/electricity/i18n.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:sit/l10n/common.dart'; - -const i18n = _I18n(); - -class _I18n with CommonI18nMixin { - const _I18n(); - - static const ns = "electricity"; - final unit = const UnitI18n(); - - String get title => "$ns.title".tr(); - - String get searchRoom => "$ns.searchRoom".tr(); - - String get balance => "$ns.balance".tr(); - - String get remainingPower => "$ns.remainingPower".tr(); - - String get searchInvalidTip => "$ns.searchInvalidTip".tr(); - - String get refreshSuccessTip => "$ns.refreshSuccessTip".tr(); - - String get refreshFailedTip => "$ns.refreshFailedTip".tr(); -} diff --git a/lib/life/electricity/index.dart b/lib/life/electricity/index.dart deleted file mode 100644 index 26ce117e8..000000000 --- a/lib/life/electricity/index.dart +++ /dev/null @@ -1,188 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:sit/design/adaptive/multiplatform.dart'; -import 'package:sit/design/widgets/app.dart'; -import 'package:sit/design/adaptive/dialog.dart'; -import 'package:sit/settings/settings.dart'; -import 'package:sit/life/electricity/storage/electricity.dart'; -import 'package:sit/r.dart'; -import 'package:sit/utils/async_event.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:share_plus/share_plus.dart'; - -import '../event.dart'; -import 'entity/balance.dart'; -import 'i18n.dart'; -import 'init.dart'; -import 'widget/card.dart'; -import 'widget/search.dart'; - -class ElectricityBalanceAppCard extends StatefulWidget { - const ElectricityBalanceAppCard({super.key}); - - @override - State createState() => _ElectricityBalanceAppCardState(); -} - -class _ElectricityBalanceAppCardState extends State { - final onRoomBalanceChanged = ElectricityBalanceInit.storage.listenRoomBalanceChange(); - late final EventSubscription $refreshEvent; - - @override - initState() { - super.initState(); - onRoomBalanceChanged.addListener(updateRoomAndBalance); - $refreshEvent = lifeEventBus.addListener(() async { - await refresh(active: true); - }); - if (Settings.life.electricity.autoRefresh) { - refresh(active: false); - } - } - - @override - dispose() { - onRoomBalanceChanged.removeListener(updateRoomAndBalance); - $refreshEvent.cancel(); - super.dispose(); - } - - void updateRoomAndBalance() { - setState(() {}); - } - - /// The electricity balance is refreshed approximately every 15 minutes. - Future refresh({required bool active}) async { - final selectedRoom = ElectricityBalanceInit.storage.selectedRoom; - if (selectedRoom == null) return; - try { - ElectricityBalanceInit.storage.lastBalance = await ElectricityBalanceInit.service.getBalance(selectedRoom); - } catch (error) { - if (active) { - if (!mounted) return; - context.showSnackBar(content: i18n.refreshFailedTip.text()); - } - return; - } - if (active) { - if (!mounted) return; - context.showSnackBar(content: i18n.refreshSuccessTip.text()); - } - } - - @override - Widget build(BuildContext context) { - final selectedRoom = ElectricityBalanceInit.storage.selectedRoom; - final balance = ElectricityBalanceInit.storage.lastBalance; - return AppCard( - view: selectedRoom != null && balance != null - ? buildBalanceCard( - balance: balance, - selectedRoom: selectedRoom, - ) - : const SizedBox(), - title: i18n.title.text(), - subtitle: selectedRoom == null ? null : "#$selectedRoom".text(), - leftActions: [ - FilledButton.icon( - onPressed: () async { - final $searchHistory = ValueNotifier(ElectricityBalanceInit.storage.searchHistory ?? const []); - $searchHistory.addListener(() { - ElectricityBalanceInit.storage.searchHistory = $searchHistory.value; - }); - final room = await searchRoom( - ctx: context, - $searchHistory: $searchHistory, - roomList: R.roomList, - ); - $searchHistory.dispose(); - if (room == null) return; - if (ElectricityBalanceInit.storage.selectedRoom != room) { - ElectricityBalanceInit.storage.selectNewRoom(room); - await refresh(active: true); - } - }, - label: i18n.searchRoom.text(), - icon: const Icon(Icons.search), - ), - ], - rightActions: [ - if (balance != null && selectedRoom != null && !isCupertino) - IconButton( - tooltip: i18n.share, - onPressed: () async { - await shareBalance(balance: balance, selectedRoom: selectedRoom, context: context); - }, - icon: const Icon(Icons.share_outlined), - ), - ], - ); - } - - Widget buildBalanceCard({ - required ElectricityBalance balance, - required String selectedRoom, - }) { - if (!isCupertino) { - return buildCard(balance); - } - return Builder( - builder: (ctx) => CupertinoContextMenu.builder( - enableHapticFeedback: true, - actions: [ - CupertinoContextMenuAction( - trailingIcon: CupertinoIcons.share, - onPressed: () async { - Navigator.of(context, rootNavigator: true).pop(); - await shareBalance(balance: balance, selectedRoom: selectedRoom, context: ctx); - }, - child: i18n.share.text(), - ), - CupertinoContextMenuAction( - trailingIcon: CupertinoIcons.delete, - isDestructiveAction: true, - onPressed: () async { - Navigator.of(context, rootNavigator: true).pop(); - await HapticFeedback.heavyImpact(); - ElectricityBalanceInit.storage.selectedRoom = null; - }, - child: i18n.delete.text(), - ), - ], - builder: (ctx, animation) { - return buildCard(balance); - }, - ), - ); - } - - Widget buildCard(ElectricityBalance balance) { - return Dismissible( - direction: DismissDirection.endToStart, - key: const ValueKey("Balance"), - onDismissed: (dir) async { - await HapticFeedback.heavyImpact(); - ElectricityBalanceInit.storage.selectedRoom = null; - }, - child: ElectricityBalanceCard( - balance: balance, - ).sized(h: 120), - ); - } -} - -Future shareBalance({ - required ElectricityBalance balance, - required String selectedRoom, - required BuildContext context, -}) async { - final text = - "#$selectedRoom: ${i18n.unit.rmb(balance.balance.toStringAsFixed(2))}, ${i18n.unit.powerKwh(balance.remainingPower.toStringAsFixed(2))}"; - await Share.share( - text, - sharePositionOrigin: context.getSharePositionOrigin(), - ); -} diff --git a/lib/life/electricity/init.dart b/lib/life/electricity/init.dart deleted file mode 100644 index 123274825..000000000 --- a/lib/life/electricity/init.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'service/electricity.dart'; -import 'storage/electricity.dart'; - -class ElectricityBalanceInit { - static late ElectricityStorage storage; - static late ElectricityService service; - - static void init() { - service = const ElectricityService(); - storage = const ElectricityStorage(); - } -} diff --git a/lib/life/electricity/service/electricity.dart b/lib/life/electricity/service/electricity.dart deleted file mode 100644 index a5474fecc..000000000 --- a/lib/life/electricity/service/electricity.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'dart:convert'; - -import 'package:dio/dio.dart'; -import 'package:sit/init.dart'; -import '../entity/balance.dart'; - -const _balanceUrl = "https://xgfy.sit.edu.cn/unifri-flow/WF/Comm/ProcessRequest.do?DoType=DBAccess_RunSQLReturnTable"; - -class ElectricityService { - Dio get dio => Init.dio; - - const ElectricityService(); - - Future getBalance(String room) async { - final response = await dio.post( - _balanceUrl, - queryParameters: { - "SQL": "select * from sys_room_balance where RoomName='$room';", - }, - options: Options( - headers: { - "Cookie": "FK_Dept=B1101", - }, - ), - ); - final data = jsonDecode(response.data as String) as List; - final list = data.map((e) => ElectricityBalance.fromJson(e)).toList(); - return list.first; - } -} diff --git a/lib/life/electricity/storage/electricity.dart b/lib/life/electricity/storage/electricity.dart deleted file mode 100644 index 1d9008ad7..000000000 --- a/lib/life/electricity/storage/electricity.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/foundation.dart'; -import 'package:hive_flutter/hive_flutter.dart'; -import 'package:sit/storage/hive/init.dart'; - -import '../entity/balance.dart'; - -class _K { - static const selectedRoom = "/selectedRoom"; - static const lastBalance = "/lastBalance"; - static const searchHistory = "/searchHistory"; -} - -class ElectricityStorage { - Box get box => HiveInit.electricity; - final int maxHistoryLength; - - const ElectricityStorage({ - this.maxHistoryLength = 20, - }); - - String? get selectedRoom => box.get(_K.selectedRoom); - - set selectedRoom(String? newV) { - box.put(_K.selectedRoom, newV); - if (newV == null) { - box.put(_K.lastBalance, null); - } - } - - ValueListenable listenRoomBalanceChange() => box.listenable(keys: [_K.selectedRoom, _K.lastBalance]); - - ElectricityBalance? get lastBalance => box.get(_K.lastBalance); - - set lastBalance(ElectricityBalance? newV) => box.put(_K.lastBalance, newV); - - List? get searchHistory => box.get(_K.searchHistory); - - set searchHistory(List? newV) { - if (newV != null) { - newV = newV.sublist(0, min(newV.length, maxHistoryLength)); - } - box.put(_K.searchHistory, newV); - } -} - -extension ElectricityStorageX on ElectricityStorage { - void addSearchHistory(String room) { - final searchHistory = this.searchHistory ?? []; - if (searchHistory.any((e) => e == room)) return; - searchHistory.insert(0, room); - this.searchHistory = searchHistory; - } - - void selectNewRoom(String room) { - selectedRoom = room; - addSearchHistory(room); - } -} diff --git a/lib/life/electricity/widget/card.dart b/lib/life/electricity/widget/card.dart deleted file mode 100644 index aa7ba0ef7..000000000 --- a/lib/life/electricity/widget/card.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:sit/design/animation/number.dart'; -import '../entity/balance.dart'; -import 'package:rettulf/rettulf.dart'; - -import '../i18n.dart'; - -class ElectricityBalanceCard extends StatelessWidget { - final ElectricityBalance balance; - final double? warningBalance; - final Color warningColor; - - const ElectricityBalanceCard({ - super.key, - required this.balance, - this.warningBalance = 10.0, - this.warningColor = Colors.redAccent, - }); - - @override - Widget build(BuildContext context) { - final warningBalance = this.warningBalance; - final balance = this.balance; - final balanceColor = warningBalance == null || warningBalance < balance.balance ? null : warningColor; - final style = context.textTheme.titleMedium; - return [ - ListTile( - leading: const Icon(Icons.offline_bolt), - titleTextStyle: style, - title: i18n.remainingPower.text(), - trailing: AnimatedNumber( - value: balance.remainingPower, - builder: (ctx, value) => i18n.unit.powerKwh(value.toStringAsFixed(2)).text(style: style), - ), - ), - ListTile( - leading: Icon(Icons.savings, color: balanceColor), - titleTextStyle: style?.copyWith(color: balanceColor), - title: i18n.balance.text(), - trailing: AnimatedNumber( - value: balance.balance, - builder: (ctx, value) => i18n.unit.rmb(value.toStringAsFixed(2)).text( - style: style?.copyWith(color: balanceColor), - ), - ), - ), - ].column(maa: MainAxisAlignment.spaceEvenly).inCard(); - } -} diff --git a/lib/life/electricity/widget/search.dart b/lib/life/electricity/widget/search.dart deleted file mode 100644 index 6fee3a8cb..000000000 --- a/lib/life/electricity/widget/search.dart +++ /dev/null @@ -1,87 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:sit/design/adaptive/swipe.dart'; -import 'package:sit/life/electricity/entity/room.dart'; -import 'package:sit/widgets/search.dart'; -import 'package:rettulf/rettulf.dart'; -import '../i18n.dart'; - -const _emptyIndicator = Object(); - -Future searchRoom({ - String? initial, - required BuildContext ctx, - required ValueNotifier> $searchHistory, - required List roomList, -}) async { - final result = await showSearch( - context: ctx, - query: initial, - delegate: ItemSearchDelegate.highlight( - searchHistory: $searchHistory, - candidateBuilder: (ctx, items, highlight, selectIt) { - return ListView.builder( - itemCount: items.length, - itemBuilder: (ctx, i) { - final item = items[i]; - final (full, highlighted) = highlight(item); - final room = DormitoryRoom.fromFullString(full); - return ListTile( - title: HighlightedText( - full: full, - highlighted: highlighted, - baseStyle: ctx.textTheme.bodyLarge, - ), - subtitle: room.l10n().text(), - onTap: () { - selectIt(item); - }, - ); - }, - ); - }, - historyBuilder: (ctx, items, stringify, selectIt) { - return ListView.builder( - itemCount: items.length, - itemBuilder: (ctx, i) { - final item = items[i]; - final full = stringify(item); - final room = DormitoryRoom.fromFullString(full); - return SwipeToDismiss( - right: SwipeToDismissAction( - label: i18n.delete, - action: () { - final newList = List.of($searchHistory.value); - newList.removeAt(i); - $searchHistory.value = newList; - }, - ), - childKey: ValueKey(item), - child: ListTile( - title: HighlightedText( - full: full, - baseStyle: ctx.textTheme.bodyLarge, - ), - subtitle: room.l10n().text(), - onTap: () { - selectIt(item); - }, - ), - ); - }, - ); - }, - candidates: roomList, - queryProcessor: _keepOnlyNumber, - keyboardType: TextInputType.number, - invalidSearchTip: i18n.searchInvalidTip, - childAspectRatio: 2, - maxCrossAxisExtent: 150.0, - emptyIndicator: _emptyIndicator, - ), - ); - return result is String ? result : null; -} - -String _keepOnlyNumber(String raw) { - return String.fromCharCodes(raw.codeUnits.where((e) => e >= 48 && e < 58)); -} diff --git a/lib/life/event.dart b/lib/life/event.dart deleted file mode 100644 index d05caeae4..000000000 --- a/lib/life/event.dart +++ /dev/null @@ -1,3 +0,0 @@ -import 'package:sit/utils/async_event.dart'; - -final lifeEventBus = AsyncEventEmitter(); diff --git a/lib/life/expense_records/Description.md b/lib/life/expense_records/Description.md deleted file mode 100644 index 56b825970..000000000 --- a/lib/life/expense_records/Description.md +++ /dev/null @@ -1,67 +0,0 @@ -# Expense Tracker - -## Remote - -1. Fetching the data, in json, form school website. -2. Mapping the raw data to "raw" dart classes, such as `TransactionRaw` - in [remote.dart](entity/remote.dart). -3. Analyzing the "raw" dart classes, and transform them into several classes and enums, such - as `Transaction` and `TranscationType` in [local.dart](entity/local.dart). - -## Local - -### Adding extra data - -1. Adding TypeMark, `Food`, `TopUp`, `Subsidy` and so on, which can be modified manually by users. - -### Persistence - -- Option A: Serializing the local classes into Hive with generated TypeAdapter. -- Option B: Serializing the local classes in json for the future needs. - -## 本地缓存 - -### 本地缓存策略概述 - -requestSet = 使用请求的时间区间 - -cachedSet = 本地已缓存的时间区间 - -targetSet = requestSet - cachedSet = 新的时间区间 - -若targetSet为空集,则直接走缓存,否则拉取targetSet时间区间的消费情况并加入本地缓存 - -### 缓存层存储设计 - -所有交易记录的索引,记录所有的交易时间,需要保证有序 - -+ /expense/transactionTsList - -所有交易记录 - -+ /expense/transactions/:id - - id为主键,不能重复,可认为交易时间不会重复,故可选用交易时间的时间戳的hex为主键 - -已缓存的时间区间 - -由于可能用户在某一段时间区间内确实未进行消费,故这里必须持久化存储已缓存的时间区间 - -+ /expense/cachedTsRange/start - -+ /expense/cachedTsRange/end - -### 代码结构设计 - -建立抽象的Fetch接口,Remote层实现Fetch接口来拉取数据,Cache层也需要实现Fetch接口来拉取数据。 - -Storage层用来封装抽象`缓存层存储设计`的接口和实现。 - -Cache层的类构造函数需要传入`Remote层Fetch接口实现`和`持久化存储接口实现`。 - -Cache层自身也作为一个Fetch接口的实现,其中的fetch方法需要基于remote层与持久化层编写缓存策略的代码逻辑,体现了一种装饰器模式的思想。 - -## Display - -Transactions are page-split by month to display with an endless lazy column. - diff --git a/lib/life/expense_records/entity/local.dart b/lib/life/expense_records/entity/local.dart deleted file mode 100644 index a7dc3a416..000000000 --- a/lib/life/expense_records/entity/local.dart +++ /dev/null @@ -1,130 +0,0 @@ -import 'dart:convert'; - -import 'package:copy_with_extension/copy_with_extension.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:sit/storage/hive/type_id.dart'; -import 'package:unicons/unicons.dart'; - -import 'remote.dart'; - -part 'local.g.dart'; - -@HiveType(typeId: CacheHiveType.expenseTransaction) -@CopyWith(skipFields: true) -class Transaction { - /// The compound of [TransactionRaw.date] and [TransactionRaw.time]. - @HiveField(0) - final DateTime timestamp; - - /// [TransactionRaw.customerId] - @HiveField(1) - final int consumerId; - - @HiveField(2) - final TransactionType type; - @HiveField(3) - final double balanceBefore; - @HiveField(4) - final double balanceAfter; - - /// It's absolute - @HiveField(5) - final double deltaAmount; - @HiveField(6) - final String deviceName; - @HiveField(7) - final String note; - - const Transaction({ - required this.timestamp, - required this.consumerId, - required this.type, - required this.balanceBefore, - required this.balanceAfter, - required this.deltaAmount, - required this.deviceName, - required this.note, - }); - - @override - String toString() { - return jsonEncode({ - "datetime": timestamp.toString(), - "consumerId": consumerId, - "type": type.toString(), - "balanceBefore": balanceBefore, - "balanceAfter": balanceAfter, - "deltaAmount": deltaAmount, - "deviceName": deviceName, - "note": note, - }); - } -} - -final _textInBrackets = RegExp(r'\([^)]*\)'); - -extension TransactionX on Transaction { - bool get isConsume => - (balanceAfter - balanceBefore) < 0 && type != TransactionType.topUp && type != TransactionType.subsidy; - - String? get bestTitle { - if (deviceName.isNotEmpty) { - return deviceName; - } else if (note.isNotEmpty) { - return note; - } else { - return null; - } - } - - String shortDeviceName() { - return deviceName.replaceAll(_textInBrackets, ''); - } - - Color get billColor => isConsume ? Colors.redAccent : Colors.green; - - String toReadableString() { - if (deltaAmount == 0) { - return deltaAmount.toStringAsFixed(2); - } else { - return "${isConsume ? '-' : '+'}${deltaAmount.toStringAsFixed(2)}"; - } - } -} - -@HiveType(typeId: CacheHiveType.expenseTransactionType) -enum TransactionType { - @HiveField(0) - water((UniconsLine.water_glass, Color(0xff8acde1)), isConsume: true), - @HiveField(1) - shower((Icons.shower_outlined, Color(0xFF2196F3)), isConsume: true), - @HiveField(2) - food((Icons.restaurant, Color(0xffe78d32)), isConsume: true), - @HiveField(3) - store((Icons.store_outlined, Color(0xFF0DAB30)), isConsume: true), - @HiveField(4) - topUp((Icons.savings, Colors.blue), isConsume: false), - @HiveField(5) - subsidy((Icons.handshake_outlined, Color(0xffdd2e22)), isConsume: false), - @HiveField(6) - coffee((Icons.coffee_rounded, Color(0xFF6F4E37)), isConsume: true), - @HiveField(7) - library((Icons.import_contacts_outlined, Color(0xffa75f1d)), isConsume: true), - @HiveField(8) - other((Icons.menu_rounded, Colors.grey), isConsume: true); - - final bool isConsume; - final (IconData icon, Color) style; - - IconData get icon => style.$1; - - Color get color => style.$2; - - const TransactionType( - this.style, { - required this.isConsume, - }); - - String localized() => "expenseRecords.type.$name".tr(); -} diff --git a/lib/life/expense_records/entity/local.g.dart b/lib/life/expense_records/entity/local.g.dart deleted file mode 100644 index cafde43c6..000000000 --- a/lib/life/expense_records/entity/local.g.dart +++ /dev/null @@ -1,222 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'local.dart'; - -// ************************************************************************** -// CopyWithGenerator -// ************************************************************************** - -abstract class _$TransactionCWProxy { - /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. - /// - /// Usage - /// ```dart - /// Transaction(...).copyWith(id: 12, name: "My name") - /// ```` - Transaction call({ - DateTime? timestamp, - int? consumerId, - TransactionType? type, - double? balanceBefore, - double? balanceAfter, - double? deltaAmount, - String? deviceName, - String? note, - }); -} - -/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfTransaction.copyWith(...)`. -class _$TransactionCWProxyImpl implements _$TransactionCWProxy { - const _$TransactionCWProxyImpl(this._value); - - final Transaction _value; - - @override - - /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. - /// - /// Usage - /// ```dart - /// Transaction(...).copyWith(id: 12, name: "My name") - /// ```` - Transaction call({ - Object? timestamp = const $CopyWithPlaceholder(), - Object? consumerId = const $CopyWithPlaceholder(), - Object? type = const $CopyWithPlaceholder(), - Object? balanceBefore = const $CopyWithPlaceholder(), - Object? balanceAfter = const $CopyWithPlaceholder(), - Object? deltaAmount = const $CopyWithPlaceholder(), - Object? deviceName = const $CopyWithPlaceholder(), - Object? note = const $CopyWithPlaceholder(), - }) { - return Transaction( - timestamp: timestamp == const $CopyWithPlaceholder() || timestamp == null - ? _value.timestamp - // ignore: cast_nullable_to_non_nullable - : timestamp as DateTime, - consumerId: consumerId == const $CopyWithPlaceholder() || consumerId == null - ? _value.consumerId - // ignore: cast_nullable_to_non_nullable - : consumerId as int, - type: type == const $CopyWithPlaceholder() || type == null - ? _value.type - // ignore: cast_nullable_to_non_nullable - : type as TransactionType, - balanceBefore: balanceBefore == const $CopyWithPlaceholder() || balanceBefore == null - ? _value.balanceBefore - // ignore: cast_nullable_to_non_nullable - : balanceBefore as double, - balanceAfter: balanceAfter == const $CopyWithPlaceholder() || balanceAfter == null - ? _value.balanceAfter - // ignore: cast_nullable_to_non_nullable - : balanceAfter as double, - deltaAmount: deltaAmount == const $CopyWithPlaceholder() || deltaAmount == null - ? _value.deltaAmount - // ignore: cast_nullable_to_non_nullable - : deltaAmount as double, - deviceName: deviceName == const $CopyWithPlaceholder() || deviceName == null - ? _value.deviceName - // ignore: cast_nullable_to_non_nullable - : deviceName as String, - note: note == const $CopyWithPlaceholder() || note == null - ? _value.note - // ignore: cast_nullable_to_non_nullable - : note as String, - ); - } -} - -extension $TransactionCopyWith on Transaction { - /// Returns a callable class that can be used as follows: `instanceOfTransaction.copyWith(...)`. - // ignore: library_private_types_in_public_api - _$TransactionCWProxy get copyWith => _$TransactionCWProxyImpl(this); -} - -// ************************************************************************** -// TypeAdapterGenerator -// ************************************************************************** - -class TransactionAdapter extends TypeAdapter { - @override - final int typeId = 51; - - @override - Transaction read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return Transaction( - timestamp: fields[0] as DateTime, - consumerId: fields[1] as int, - type: fields[2] as TransactionType, - balanceBefore: fields[3] as double, - balanceAfter: fields[4] as double, - deltaAmount: fields[5] as double, - deviceName: fields[6] as String, - note: fields[7] as String, - ); - } - - @override - void write(BinaryWriter writer, Transaction obj) { - writer - ..writeByte(8) - ..writeByte(0) - ..write(obj.timestamp) - ..writeByte(1) - ..write(obj.consumerId) - ..writeByte(2) - ..write(obj.type) - ..writeByte(3) - ..write(obj.balanceBefore) - ..writeByte(4) - ..write(obj.balanceAfter) - ..writeByte(5) - ..write(obj.deltaAmount) - ..writeByte(6) - ..write(obj.deviceName) - ..writeByte(7) - ..write(obj.note); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is TransactionAdapter && runtimeType == other.runtimeType && typeId == other.typeId; -} - -class TransactionTypeAdapter extends TypeAdapter { - @override - final int typeId = 50; - - @override - TransactionType read(BinaryReader reader) { - switch (reader.readByte()) { - case 0: - return TransactionType.water; - case 1: - return TransactionType.shower; - case 2: - return TransactionType.food; - case 3: - return TransactionType.store; - case 4: - return TransactionType.topUp; - case 5: - return TransactionType.subsidy; - case 6: - return TransactionType.coffee; - case 7: - return TransactionType.library; - case 8: - return TransactionType.other; - default: - return TransactionType.water; - } - } - - @override - void write(BinaryWriter writer, TransactionType obj) { - switch (obj) { - case TransactionType.water: - writer.writeByte(0); - break; - case TransactionType.shower: - writer.writeByte(1); - break; - case TransactionType.food: - writer.writeByte(2); - break; - case TransactionType.store: - writer.writeByte(3); - break; - case TransactionType.topUp: - writer.writeByte(4); - break; - case TransactionType.subsidy: - writer.writeByte(5); - break; - case TransactionType.coffee: - writer.writeByte(6); - break; - case TransactionType.library: - writer.writeByte(7); - break; - case TransactionType.other: - writer.writeByte(8); - break; - } - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is TransactionTypeAdapter && runtimeType == other.runtimeType && typeId == other.typeId; -} diff --git a/lib/life/expense_records/entity/remote.dart b/lib/life/expense_records/entity/remote.dart deleted file mode 100644 index 2e777ae58..000000000 --- a/lib/life/expense_records/entity/remote.dart +++ /dev/null @@ -1,89 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; - -part 'remote.g.dart'; - -/// The analysis of expense tracker is [here](https://github.com/SIT-kite/expense-tracker). -@JsonSerializable(createToJson: false) -class DataPackRaw { - @JsonKey(name: "retcode") - final int code; - @JsonKey(name: "retcount") - final int count; - @JsonKey(name: "retdata") - final List transactions; - @JsonKey(name: "retmsg") - final String message; - - const DataPackRaw({ - required this.code, - required this.count, - required this.transactions, - required this.message, - }); - - factory DataPackRaw.fromJson(Map json) => _$DataPackRawFromJson(json); -} - -@JsonSerializable(createToJson: false) -class TransactionRaw { - /// transaction name - /// example: "pos消费", "支付宝充值", "补助领取", "批量销户" or "卡冻结", "余额转移", "下发补助" or "补助撤销" - @JsonKey(name: "transname") - final String name; - - /// example: "20221102" - /// transaction data - /// format: yyyymmdd - @JsonKey(name: "transdate") - final String date; - - /// transaction time - /// example: "114745" - /// format: hhmmss - @JsonKey(name: "transtime") - final String time; - - /// customer id - /// example: 11045158 - @JsonKey(name: "custid") - final int customerId; - - /// transaction flag - @JsonKey(name: "transflag") - final int flag; - - /// card before balance - /// example: 76.5 - @JsonKey(name: "cardbefbal") - final double balanceBeforeTransaction; - - /// card after balance - /// example: 70.5 - @JsonKey(name: "cardaftbal") - final double balanceAfterTransaction; - - /// the amount of this transaction performed - /// It's absolute. - /// example: 6 - @JsonKey(name: "amount") - final double amount; - - /// device name - /// example: "奉贤一食堂一楼汇多pos4(新)", "多媒体-3-4号楼", "上海应用技术学院" - @JsonKey(name: "devicename") - final String? deviceName; - - const TransactionRaw({ - required this.date, - required this.time, - required this.customerId, - required this.flag, - required this.balanceBeforeTransaction, - required this.balanceAfterTransaction, - required this.amount, - required this.deviceName, - required this.name, - }); - - factory TransactionRaw.fromJson(Map json) => _$TransactionRawFromJson(json); -} diff --git a/lib/life/expense_records/entity/remote.g.dart b/lib/life/expense_records/entity/remote.g.dart deleted file mode 100644 index e8a3a74fb..000000000 --- a/lib/life/expense_records/entity/remote.g.dart +++ /dev/null @@ -1,27 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'remote.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -DataPackRaw _$DataPackRawFromJson(Map json) => DataPackRaw( - code: json['retcode'] as int, - count: json['retcount'] as int, - transactions: - (json['retdata'] as List).map((e) => TransactionRaw.fromJson(e as Map)).toList(), - message: json['retmsg'] as String, - ); - -TransactionRaw _$TransactionRawFromJson(Map json) => TransactionRaw( - date: json['transdate'] as String, - time: json['transtime'] as String, - customerId: json['custid'] as int, - flag: json['transflag'] as int, - balanceBeforeTransaction: (json['cardbefbal'] as num).toDouble(), - balanceAfterTransaction: (json['cardaftbal'] as num).toDouble(), - amount: (json['amount'] as num).toDouble(), - deviceName: json['devicename'] as String?, - name: json['transname'] as String, - ); diff --git a/lib/life/expense_records/entity/statistics.dart b/lib/life/expense_records/entity/statistics.dart deleted file mode 100644 index d4a5b761f..000000000 --- a/lib/life/expense_records/entity/statistics.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; - -enum StatisticsMode { - week, - month, - year; - - String l10nName() => "expenseRecords.statsMode.$name".tr(); -} diff --git a/lib/life/expense_records/i18n.dart b/lib/life/expense_records/i18n.dart deleted file mode 100644 index ee9c774df..000000000 --- a/lib/life/expense_records/i18n.dart +++ /dev/null @@ -1,58 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:sit/l10n/common.dart'; - -const i18n = _I18n(); - -class _I18n with CommonI18nMixin { - const _I18n(); - - static const ns = "expenseRecords"; - final unit = const UnitI18n(); - final stats = const _Stats(); - final view = const _View(); - - String get title => "$ns.title".tr(); - - String get noTransactionsTip => "$ns.noTransactionsTip".tr(); - - String get refreshSuccessTip => "$ns.refreshSuccessTip".tr(); - - String get refreshFailedTip => "$ns.refreshFailedTip".tr(); - - String get check => "$ns.check".tr(); - - String get statistics => "$ns.statistics".tr(); - - String balanceInCard(String amount) => "$ns.balanceInCard".tr(args: [amount]); - - String lastTransaction(String amount, String place) => "$ns.lastTransaction".tr(namedArgs: { - "amount": amount, - "place": place, - }); - - String income(String amount) => "$ns.income".tr(args: [amount]); - - String outcome(String amount) => "$ns.outcome".tr(args: [amount]); -} - -class _Stats { - const _Stats(); - - static const ns = "${_I18n.ns}.stats"; - - String get title => "$ns.title".tr(); - - String get categories => "$ns.categories".tr(); - - String get total => "$ns.total".tr(); -} - -class _View { - const _View(); - - static const ns = "${_I18n.ns}.view"; - - String get balance => "$ns.balance".tr(); - - String get rmb => "$ns.rmb".tr(); -} diff --git a/lib/life/expense_records/index.dart b/lib/life/expense_records/index.dart deleted file mode 100644 index c19face10..000000000 --- a/lib/life/expense_records/index.dart +++ /dev/null @@ -1,112 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:sit/credentials/widgets/oa_scope.dart'; -import 'package:sit/design/widgets/app.dart'; -import 'package:sit/design/adaptive/dialog.dart'; -import 'package:sit/life/event.dart'; -import 'package:sit/settings/settings.dart'; -import 'package:sit/life/expense_records/init.dart'; -import 'package:sit/utils/async_event.dart'; -import 'utils.dart'; -import 'widget/balance.dart'; -import 'package:rettulf/rettulf.dart'; - -import "i18n.dart"; -import 'widget/transaction.dart'; - -class ExpenseRecordsAppCard extends StatefulWidget { - const ExpenseRecordsAppCard({super.key}); - - @override - State createState() => _ExpenseRecordsAppCardState(); -} - -class _ExpenseRecordsAppCardState extends State { - final $lastTransaction = ExpenseRecordsInit.storage.listenLastTransaction(); - late final EventSubscription $refreshEvent; - - @override - void initState() { - $lastTransaction.addListener(onLatestChanged); - $refreshEvent = lifeEventBus.addListener(() async { - await refresh(active: true); - }); - super.initState(); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - if (Settings.life.expense.autoRefresh) { - refresh(active: false); - } - } - - @override - void dispose() { - $lastTransaction.removeListener(onLatestChanged); - $refreshEvent.cancel(); - super.dispose(); - } - - void onLatestChanged() { - setState(() {}); - } - - Future refresh({required bool active}) async { - final oaCredential = context.auth.credentials; - if (oaCredential == null) return; - try { - await fetchAndSaveTransactionUntilNow( - studentId: oaCredential.account, - ); - } catch (error) { - if (active) { - if (!mounted) return; - context.showSnackBar(content: i18n.refreshFailedTip.text()); - } - return; - } - if (active) { - if (!mounted) return; - context.showSnackBar(content: i18n.refreshSuccessTip.text()); - } - } - - @override - Widget build(BuildContext context) { - final lastTransaction = ExpenseRecordsInit.storage.latestTransaction; - return AppCard( - view: lastTransaction == null - ? const SizedBox() - : [ - BalanceCard( - balance: lastTransaction.balanceAfter, - ).expanded(), - TransactionCard( - transaction: lastTransaction, - ).expanded(), - ].row().sized(h: 140), - title: i18n.title.text(), - leftActions: [ - FilledButton.icon( - icon: const Icon(Icons.assignment), - onPressed: () async { - context.push("/expense-records"); - }, - label: i18n.check.text(), - ), - OutlinedButton( - onPressed: lastTransaction == null - ? null - : () async { - context.push("/expense-records/statistics"); - }, - child: i18n.statistics.text(), - ), - ], - ); - } -} diff --git a/lib/life/expense_records/init.dart b/lib/life/expense_records/init.dart deleted file mode 100644 index f5d062a2c..000000000 --- a/lib/life/expense_records/init.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'service/fetch.dart'; -import 'storage/local.dart'; - -class ExpenseRecordsInit { - static late ExpenseFetchService service; - static late ExpenseStorage storage; - - static void init() { - service = const ExpenseFetchService(); - storage = const ExpenseStorage(); - } -} diff --git a/lib/life/expense_records/page/records.dart b/lib/life/expense_records/page/records.dart deleted file mode 100644 index e401f2b71..000000000 --- a/lib/life/expense_records/page/records.dart +++ /dev/null @@ -1,136 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:sit/credentials/widgets/oa_scope.dart'; -import 'package:sit/design/widgets/common.dart'; -import 'package:sit/life/expense_records/storage/local.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:sit/life/expense_records/widget/group.dart'; -import 'package:sit/utils/error.dart'; - -import '../entity/local.dart'; -import '../init.dart'; -import '../i18n.dart'; -import '../utils.dart'; - -class ExpenseRecordsPage extends StatefulWidget { - const ExpenseRecordsPage({super.key}); - - @override - State createState() => _ExpenseRecordsPageState(); -} - -class _ExpenseRecordsPageState extends State { - final $lastTransaction = ExpenseRecordsInit.storage.listenLastTransaction(); - bool isFetching = false; - List? records; - - List<({YearMonth time, List records})>? month2records; - - @override - void initState() { - super.initState(); - $lastTransaction.addListener(onLatestChanged); - updateRecords(ExpenseRecordsInit.storage.getTransactionsByRange()); - } - - @override - void dispose() { - $lastTransaction.removeListener(onLatestChanged); - super.dispose(); - } - - void onLatestChanged() { - setState(() {}); - } - - void updateRecords(List? records) { - if (!mounted) return; - setState(() { - this.records = records; - month2records = records == null ? null : groupTransactionsByMonthYear(records); - }); - } - - Future refresh() async { - final oaCredential = context.auth.credentials; - if (oaCredential == null) return; - setState(() { - isFetching = true; - }); - try { - await fetchAndSaveTransactionUntilNow( - studentId: oaCredential.account, - ); - updateRecords(ExpenseRecordsInit.storage.getTransactionsByRange()); - setState(() { - isFetching = false; - }); - } catch (error, stackTrace) { - debugPrintError(error, stackTrace); - if (!mounted) return; - setState(() { - isFetching = false; - }); - } - } - - @override - Widget build(BuildContext context) { - final month2records = this.month2records; - final lastTransaction = ExpenseRecordsInit.storage.latestTransaction; - return Scaffold( - appBar: AppBar( - title: lastTransaction == null - ? i18n.title.text() - : i18n.balanceInCard(lastTransaction.balanceAfter.toStringAsFixed(2)).text(), - actions: [ - IconButton( - tooltip: i18n.delete, - icon: const Icon(Icons.delete_outlined), - onPressed: () async { - ExpenseRecordsInit.storage.clearIndex(); - await HapticFeedback.heavyImpact(); - updateRecords(null); - }, - ), - ], - centerTitle: true, - bottom: isFetching - ? const PreferredSize( - preferredSize: Size.fromHeight(4), - child: LinearProgressIndicator(), - ) - : null, - ), - body: RefreshIndicator.adaptive( - onRefresh: () async { - await HapticFeedback.heavyImpact(); - await refresh(); - }, - child: CustomScrollView( - slivers: [ - if (month2records == null || month2records.isEmpty) - SliverFillRemaining( - child: LeavingBlank( - icon: Icons.inbox_outlined, - desc: i18n.noTransactionsTip, - ), - ) - else - ...month2records.mapIndexed( - (index, e) { - return TransactionGroupSection( - // expand records in the first month by default. - initialExpanded: index == 0, - time: e.time, - records: e.records, - ); - }, - ), - ], - ), - ), - ); - } -} diff --git a/lib/life/expense_records/page/statistics.dart b/lib/life/expense_records/page/statistics.dart deleted file mode 100644 index e01261640..000000000 --- a/lib/life/expense_records/page/statistics.dart +++ /dev/null @@ -1,298 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:sit/design/widgets/card.dart'; -import 'package:sit/life/expense_records/entity/statistics.dart'; -import 'package:sit/life/expense_records/storage/local.dart'; -import 'package:sit/life/expense_records/utils.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:fl_chart/fl_chart.dart'; - -import '../entity/local.dart'; -import '../i18n.dart'; -import '../init.dart'; -import '../widget/chart.dart'; -import '../widget/selector.dart'; - -class ExpenseStatisticsPage extends StatefulWidget { - const ExpenseStatisticsPage({super.key}); - - @override - State createState() => _ExpenseStatisticsPageState(); -} - -class _ExpenseStatisticsPageState extends State { - late List records; - late double total; - late Map records, double total, double proportion})> type2transactions; - late int selectedYear; - late int selectedMonth; - - @override - void initState() { - super.initState(); - final now = DateTime.now(); - selectedYear = now.year; - selectedMonth = now.month; - refreshRecords(); - } - - void refreshRecords() { - records = ExpenseRecordsInit.storage.getTransactionsByRange( - start: DateTime(selectedYear, selectedMonth, 1), - end: DateTime(selectedYear, selectedMonth + 1), - ) ?? - const []; - records.retainWhere((record) => record.type.isConsume); - final type2transactions = records.groupListsBy((record) => record.type); - final type2total = type2transactions.map((type, records) => MapEntry(type, accumulateTransactionAmount(records))); - total = type2total.values.sum; - this.type2transactions = type2transactions.map((type, records) { - final (income: _, :outcome) = accumulateTransactionIncomeOutcome(records); - return MapEntry(type, (records: records, total: outcome, proportion: (type2total[type] ?? 0) / total)); - }); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - body: CustomScrollView( - slivers: [ - SliverAppBar( - floating: true, - title: i18n.stats.title.text(), - actions: [ - SegmentedButton( - showSelectedIcon: false, - style: ButtonStyle( - padding: MaterialStateProperty.all(const EdgeInsets.symmetric(horizontal: 4)), - ), - segments: StatisticsMode.values - .map((e) => ButtonSegment( - value: e, - label: e.l10nName().text(), - )) - .toList(), - selected: {StatisticsMode.week}, - onSelectionChanged: (newSelection) { - setState(() {}); - }, - ) - ], - ), - SliverToBoxAdapter( - child: _buildChartView(), - ), - SliverToBoxAdapter( - child: ExpensePieChart(records: type2transactions), - ), - ], - ), - ); - - final now = DateTime.now(); - final years = _getYear(records); - final months = _getMonth(records, years, selectedYear); - return Scaffold( - body: CustomScrollView( - slivers: [ - SliverAppBar( - pinned: true, - expandedHeight: 200, - flexibleSpace: FlexibleSpaceBar( - title: i18n.stats.title.text(), - centerTitle: true, - background: YearMonthSelector( - years: years, - months: months, - initialYear: now.year, - initialMonth: now.month, - ), - ), - ), - SliverToBoxAdapter( - child: _buildChartView(), - ), - SliverToBoxAdapter( - child: ExpensePieChart(records: type2transactions), - ), - ], - ), - ); - } - - List _getYear(List expenseBillDesc) { - List years = []; - final now = DateTime.now(); - final currentYear = now.year; - final int startYear = expenseBillDesc.isNotEmpty ? expenseBillDesc.last.timestamp.year : currentYear; - for (int year = startYear; year <= currentYear; year++) { - years.add(year); - } - return years; - } - - List _getMonth(List expenseBill, List years, int year) { - List result = []; - final now = DateTime.now(); - if (now.year == year) { - for (int month = 1; month <= now.month; month++) { - result.add(month); - } - } else if (years.first == year) { - for (int month = expenseBill.last.timestamp.month; month <= 12; month++) { - result.add(month); - } - } else { - result = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; - } - return result; - } - - static int getDayCountOfMonth(int year, int month) { - final int daysFeb = (year % 400 == 0 || (year % 4 == 0 && year % 100 != 0)) ? 29 : 28; - List days = [31, daysFeb, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; - return days[month - 1]; - } - - Widget _buildChartView() { - // TODO: take current month into account - // 得到该年该月有多少天, 生成数组记录每一天的消费. - final List daysAmount = List.filled(getDayCountOfMonth(selectedYear, selectedMonth), 0.00); - // 便利该月消费情况, 加到上述统计列表中. - for (final record in records) { - daysAmount[record.timestamp.day - 1] += record.deltaAmount; - } - return OutlinedCard( - child: AspectRatio( - aspectRatio: 1.5, - child: BaseLineChartWidget( - bottomTitles: List.generate(daysAmount.length, (i) => (i + 1).toString()), - values: daysAmount, - ).padSymmetric(v: 12, h: 8), - ), - ); - } - - Widget buildWeekChart() { - return const BaseLineChartWidget( - bottomTitles: [], - values: [], - ); - } - - Widget buildMonthChart() { - return const BaseLineChartWidget( - bottomTitles: [], - values: [], - ); - } - - Widget buildYearChart() { - return const BaseLineChartWidget( - bottomTitles: [], - values: [], - ); - } -} - -class BaseLineChartWidget extends StatelessWidget { - final List bottomTitles; - final List values; - - const BaseLineChartWidget({ - super.key, - required this.bottomTitles, - required this.values, - }); - - ///底部标题栏 - Widget bottomTitle(BuildContext ctx, double value, TitleMeta mate) { - if ((value * 10).toInt() % 10 == 5) { - return const SizedBox(); - } - - return SideTitleWidget( - axisSide: mate.axisSide, - child: Text( - bottomTitles[value.toInt()], - style: ctx.textTheme.bodySmall?.copyWith( - color: Colors.blueGrey, - ), - ), - ); - } - - ///左边部标题栏 - Widget leftTitle(BuildContext ctx, double value, TitleMeta mate) { - const style = TextStyle( - color: Colors.blueGrey, - fontSize: 11, - ); - String text = '¥${value.toStringAsFixed(2)}'; - return SideTitleWidget( - axisSide: mate.axisSide, - child: Text(text, style: style), - ); - } - - @override - Widget build(BuildContext context) { - return LineChart( - LineChartData( - ///触摸控制 - lineTouchData: const LineTouchData( - touchTooltipData: LineTouchTooltipData( - tooltipBgColor: Colors.transparent, - ), - touchSpotThreshold: 10, - ), - borderData: FlBorderData( - border: const Border( - bottom: BorderSide.none, - ), - ), - lineBarsData: [ - LineChartBarData( - isStrokeCapRound: true, - belowBarData: BarAreaData( - show: true, - color: context.colorScheme.primary.withOpacity(0.15), - ), - spots: values - .map((e) => (e * 100).toInt() / 100) // 保留两位小数 - .toList() - .asMap() - .entries - .map((e) => FlSpot(e.key.toDouble(), e.value)) - .toList(), - color: context.colorScheme.primary, - isCurved: true, - preventCurveOverShooting: true, - barWidth: 1, - ), - ], - - ///图表线表线框 - titlesData: FlTitlesData( - show: true, - rightTitles: const AxisTitles(), - leftTitles: AxisTitles( - sideTitles: SideTitles( - showTitles: true, - reservedSize: 50, - getTitlesWidget: (v, meta) => leftTitle(context, v, meta), - ), - ), - topTitles: const AxisTitles(), - bottomTitles: AxisTitles( - sideTitles: SideTitles( - showTitles: true, - reservedSize: 55, - getTitlesWidget: (v, meta) => bottomTitle(context, v, meta), - ), - ), - ), - ), - ); - } -} diff --git a/lib/life/expense_records/service/fetch.dart b/lib/life/expense_records/service/fetch.dart deleted file mode 100644 index dde9b9d68..000000000 --- a/lib/life/expense_records/service/fetch.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'dart:collection'; -import 'dart:convert'; - -import 'package:crypto/crypto.dart'; -import 'package:dio/dio.dart'; -import 'package:sit/init.dart'; - -import 'package:sit/session/sso.dart'; - -import '../entity/local.dart'; -import '../entity/remote.dart'; -import 'parser.dart'; - -class ExpenseFetchService { - String al2(int v) => v < 10 ? "0$v" : "$v"; - - String format(DateTime d) => "${d.year}${al2(d.month)}${al2(d.day)}${al2(d.hour)}${al2(d.minute)}${al2(d.second)}"; - - static const String magicNumber = "adc4ac6822fd462780f878b86cb94688"; - static const urlPath = "https://xgfy.sit.edu.cn/yktapi/services/querytransservice/querytrans"; - - SsoSession get session => Init.ssoSession; - - const ExpenseFetchService(); - - Future> fetch({ - required String studentID, - required DateTime from, - required DateTime to, - }) async { - final curTs = format(DateTime.now()); - final fromTs = format(from); - final toTs = format(to); - final auth = composeAuth(studentID, fromTs, toTs, curTs); - - final res = await session.request( - urlPath, - options: Options( - contentType: 'text/plain', - method: "POST", - ), - para: { - 'timestamp': curTs, - 'starttime': fromTs, - 'endtime': toTs, - 'sign': auth, - 'sign_method': 'HMAC', - 'stuempno': studentID, - }, - ); - final raw = parseDataPack(res.data); - final list = raw.transactions.map(parseFull).toList(); - return list; - } - - DataPackRaw parseDataPack(dynamic data) { - final res = HashMap.of(data); - final retdataRaw = res["retdata"]; - final retdata = json.decode(retdataRaw); - res["retdata"] = retdata; - return DataPackRaw.fromJson(res); - } - - String composeAuth(String studentId, String from, String to, String cur) { - final full = studentId + from + to + cur; - final msg = utf8.encode(full); - final key = utf8.encode(magicNumber); - final hmac = Hmac(sha1, key); - return hmac.convert(msg).toString(); - } -} diff --git a/lib/life/expense_records/service/parser.dart b/lib/life/expense_records/service/parser.dart deleted file mode 100644 index 827a16e2e..000000000 --- a/lib/life/expense_records/service/parser.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:sit/school/utils.dart'; - -import '../entity/local.dart'; -import '../entity/remote.dart'; - -const deviceName2Type = { - '开水': TransactionType.water, - '浴室': TransactionType.shower, - '咖啡吧': TransactionType.coffee, - '食堂': TransactionType.food, - '超市': TransactionType.store, - '图书馆': TransactionType.library, -}; - -TransactionType parseType(Transaction trans) { - if (trans.note.contains("补助")) { - return TransactionType.subsidy; - } else if (trans.note.contains("充值") || trans.note.contains("余额转移") || !trans.isConsume) { - return TransactionType.topUp; - } else if (trans.note.contains("消费") || trans.isConsume) { - for (MapEntry entry in deviceName2Type.entries) { - String name = entry.key; - TransactionType type = entry.value; - if (trans.deviceName.contains(name)) { - return type; - } - } - } - return TransactionType.other; -} - -Transaction parseFull(TransactionRaw raw) { - final transaction = Transaction( - timestamp: parseDatetime(raw), - balanceBefore: raw.balanceBeforeTransaction, - balanceAfter: raw.balanceAfterTransaction, - deltaAmount: raw.amount.abs(), - deviceName: mapChinesePunctuations(raw.deviceName ?? ""), - note: raw.name, - consumerId: raw.customerId, - type: TransactionType.other, - ); - return transaction.copyWith( - type: parseType(transaction), - ); -} - -DateTime parseDatetime(TransactionRaw raw) { - final date = raw.date; - final year = int.parse(date.substring(0, 4)); - final month = int.parse(date.substring(4, 6)); - final day = int.parse(date.substring(6, 8)); - - final time = raw.time; - final hour = int.parse(time.substring(0, 2)); - final min = int.parse(time.substring(2, 4)); - final sec = int.parse(time.substring(4, 6)); - return DateTime(year, month, day, hour, min, sec); -} diff --git a/lib/life/expense_records/storage/local.dart b/lib/life/expense_records/storage/local.dart deleted file mode 100644 index c1be21a92..000000000 --- a/lib/life/expense_records/storage/local.dart +++ /dev/null @@ -1,80 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:hive_flutter/hive_flutter.dart'; -import 'package:sit/storage/hive/init.dart'; - -import '../entity/local.dart'; - -class _K { - static const transactionTsList = '/transactionTsList'; - - static String transaction(DateTime timestamp) { - final id = (timestamp.millisecondsSinceEpoch ~/ 1000).toRadixString(16); - return '/transactions/$id'; - } - - // Don't use lastFetchedTs, and just fetch all translations - static const lastFetchedTs = "/lastFetchedTs"; - static const latestTransaction = "/latestTransaction"; -} - -class ExpenseStorage { - Box get box => HiveInit.expense; - - const ExpenseStorage(); - - /// 所有交易记录的索引,记录所有的交易时间,需要保证有序,以实现二分查找 - List? get transactionTsList => (box.get(_K.transactionTsList) as List?)?.cast(); - - set transactionTsList(List? newV) => box.put(_K.transactionTsList, newV); - - ValueListenable listenTransactionTsList() => box.listenable(keys: [_K.transactionTsList]); - - /// 通过某个时刻来获得交易记录 - Transaction? getTransactionByTs(DateTime ts) => box.get(_K.transaction(ts)); - - setTransactionByTs(DateTime ts, Transaction? transaction) => box.put(_K.transaction(ts), transaction); - - Transaction? get latestTransaction => box.get(_K.latestTransaction); - - set latestTransaction(Transaction? v) => box.put(_K.latestTransaction, v); - - ValueListenable listenLastTransaction() => box.listenable(keys: [_K.latestTransaction]); -} - -extension ExpenseStorageX on ExpenseStorage { - void clearIndex() { - transactionTsList = null; - latestTransaction = null; - } - - /// Gets the transaction timestamps in range of start to end. - /// [start] is inclusive. - /// [end] is exclusive. - List? getTransactionTsByRange({ - DateTime? start, - DateTime? end, - }) { - final list = transactionTsList; - if (list == null) return null; - if (start == null && end == null) return list; - return list - .where((e) => start == null || e == start || e.isAfter(start)) - .where((e) => end == null || e.isBefore(end)) - .toList(); - } - - List? getTransactionsByRange({ - DateTime? start, - DateTime? end, - }) { - final timestamps = getTransactionTsByRange(start: start, end: end); - if (timestamps == null) return null; - final transactions = []; - for (final ts in timestamps) { - final transaction = getTransactionByTs(ts); - assert(transaction != null, "$ts has no transaction"); - if (transaction != null) transactions.add(transaction); - } - return transactions; - } -} diff --git a/lib/life/expense_records/utils.dart b/lib/life/expense_records/utils.dart deleted file mode 100644 index 5e3525303..000000000 --- a/lib/life/expense_records/utils.dart +++ /dev/null @@ -1,119 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:sit/life/expense_records/entity/local.dart'; - -import 'init.dart'; - -Future fetchAndSaveTransactionUntilNow({ - required String studentId, -}) async { - final storage = ExpenseRecordsInit.storage; - final now = DateTime.now(); - final start = now.copyWith(year: now.year - 4); - final newlyFetched = await ExpenseRecordsInit.service.fetch( - studentID: studentId, - from: start, - to: now, - ); - final oldTsList = storage.transactionTsList ?? const []; - final newTsList = {...newlyFetched.map((e) => e.timestamp), ...oldTsList}.toList(); - // the latest goes first - newTsList.sort((a, b) => -a.compareTo(b)); - for (final transaction in newlyFetched) { - storage.setTransactionByTs(transaction.timestamp, transaction); - } - storage.transactionTsList = newTsList; - final latest = newlyFetched.firstOrNull; - if (latest != null) { - final latestValidBalance = _findLatestValidBalanceTransaction(newlyFetched, newTsList); - // check if the transaction is kept for topping up - if (latestValidBalance != null) { - storage.latestTransaction = latest.copyWith( - balanceBefore: latestValidBalance.balanceBefore, - balanceAfter: latestValidBalance.balanceAfter, - ); - } else { - storage.latestTransaction = latest; - } - } -} - -/// [newlyFetched] is descending by time. -Transaction? _findLatestValidBalanceTransaction(List newlyFetched, List allTsList) { - for (final transaction in newlyFetched) { - if (transaction.type != TransactionType.topUp && transaction.balanceBefore != 0 && transaction.balanceAfter != 0) { - return transaction; - } - } - for (final ts in allTsList) { - final transaction = ExpenseRecordsInit.storage.getTransactionByTs(ts); - if (transaction == null) continue; - if (transaction.type != TransactionType.topUp && transaction.balanceBefore != 0 && transaction.balanceAfter != 0) { - return transaction; - } - } - return null; -} - -typedef YearMonth = ({int year, int month}); - -extension YearMonthX on YearMonth { - int compareTo(YearMonth other, {bool ascending = true}) { - final sign = ascending ? 1 : -1; - return switch (this.year - other.year) { - > 0 => 1 * sign, - < 0 => -1 * sign, - _ => switch (this.month - other.month) { - > 0 => 1 * sign, - < 0 => -1 * sign, - _ => 0, - } - }; - } - - DateTime toDateTime() => DateTime(year, month); -} - -List<({YearMonth time, List records})> groupTransactionsByMonthYear( - List records, -) { - final groupByYearMonth = records - .groupListsBy((r) => (year: r.timestamp.year, month: r.timestamp.month)) - .entries - // the latest goes first - .map((e) => (time: e.key, records: e.value.sorted((a, b) => -a.timestamp.compareTo(b.timestamp)))) - .toList(); - groupByYearMonth.sort((a, b) => a.time.compareTo(b.time, ascending: false)); - return groupByYearMonth; -} - -bool validateTransaction(Transaction t) { - if (t.type == TransactionType.topUp) { - return false; - } - return true; -} - -/// Accumulates the income and outcome. -/// Ignores invalid transactions by [validateTransaction]. -({double income, double outcome}) accumulateTransactionIncomeOutcome(List transactions) { - double income = 0; - double outcome = 0; - for (final t in transactions) { - if (!validateTransaction(t)) continue; - if (t.isConsume) { - outcome += t.deltaAmount; - } else { - income += t.deltaAmount; - } - } - return (income: income, outcome: outcome); -} - -/// Accumulates the [Transaction.deltaAmount]. -double accumulateTransactionAmount(List transactions) { - var total = 0.0; - for (final t in transactions) { - total += t.deltaAmount; - } - return total; -} diff --git a/lib/life/expense_records/widget/balance.dart b/lib/life/expense_records/widget/balance.dart deleted file mode 100644 index 37f1d1fae..000000000 --- a/lib/life/expense_records/widget/balance.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:auto_size_text/auto_size_text.dart'; -import 'package:flutter/material.dart'; -import 'package:sit/design/animation/number.dart'; -import 'package:sit/utils/format.dart'; -import 'package:rettulf/rettulf.dart'; -import "../i18n.dart"; - -class BalanceCard extends StatelessWidget { - final double balance; - final bool removeTrailingZeros; - final double? warningBalance; - final Color warningColor; - - const BalanceCard({ - super.key, - required this.balance, - this.warningBalance = 10.0, - this.warningColor = Colors.redAccent, - this.removeTrailingZeros = false, - }); - - @override - Widget build(BuildContext context) { - final textTheme = context.textTheme; - final warningBalance = this.warningBalance; - final balanceColor = warningBalance == null || warningBalance < balance ? null : warningColor; - return [ - AutoSizeText( - i18n.view.balance, - style: textTheme.titleLarge, - maxLines: 1, - ), - AnimatedNumber( - value: balance, - builder: (context, balance) { - return AutoSizeText( - removeTrailingZeros ? formatWithoutTrailingZeros(balance) : balance.toStringAsFixed(2), - style: textTheme.displayMedium?.copyWith(color: balanceColor), - maxLines: 1, - ); - }), - AutoSizeText( - i18n.view.rmb, - style: textTheme.titleMedium, - maxLines: 1, - ), - ] - .column( - caa: CrossAxisAlignment.start, - maa: MainAxisAlignment.spaceEvenly, - ) - .padAll(10) - .inCard(); - } -} diff --git a/lib/life/expense_records/widget/chart.dart b/lib/life/expense_records/widget/chart.dart deleted file mode 100644 index f7712b362..000000000 --- a/lib/life/expense_records/widget/chart.dart +++ /dev/null @@ -1,93 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:dynamic_color/dynamic_color.dart'; -import 'package:fl_chart/fl_chart.dart'; -import 'package:flutter/material.dart'; -import 'package:sit/design/widgets/card.dart'; -import 'package:rettulf/rettulf.dart'; - -import '../entity/local.dart'; -import "../i18n.dart"; - -class ExpensePieChart extends StatefulWidget { - final Map records, double total, double proportion})> records; - - const ExpensePieChart({ - super.key, - required this.records, - }); - - @override - State createState() => _ExpensePieChartState(); -} - -class _ExpensePieChartState extends State { - int touchedIndex = -1; - - @override - Widget build(BuildContext context) { - assert(widget.records.keys.every((type) => type.isConsume)); - return OutlinedCard( - child: [ - AspectRatio( - aspectRatio: 1.5, - child: buildChart(), - ), - buildLegends().padAll(8).align(at: Alignment.topLeft), - ].column(), - ); - } - - Widget buildChart() { - return PieChart( - PieChartData( - pieTouchData: PieTouchData( - touchCallback: (FlTouchEvent event, pieTouchResponse) { - setState(() { - if (!event.isInterestedForInteractions || - pieTouchResponse == null || - pieTouchResponse.touchedSection == null) { - touchedIndex = -1; - return; - } - touchedIndex = pieTouchResponse.touchedSection!.touchedSectionIndex; - }); - }, - ), - borderData: FlBorderData( - show: false, - ), - sectionsSpace: 0, - centerSpaceRadius: 60, - sections: widget.records.entries.mapIndexed((i, entry) { - final isTouched = i == touchedIndex; - final MapEntry(key: type, value: (records: _, :total, :proportion)) = entry; - final color = type.color.harmonizeWith(context.colorScheme.primary); - return PieChartSectionData( - color: color.withOpacity(isTouched ? 1 : 0.8), - value: total, - title: "${(proportion * 100).toStringAsFixed(2)}%", - titleStyle: context.textTheme.titleSmall, - radius: isTouched ? 55 : 50, - badgeWidget: Icon(type.icon, color: color), - badgePositionPercentageOffset: 1.5, - ); - }).toList(), - ), - ); - } - - Widget buildLegends() { - return widget.records.entries - .map((record) { - final MapEntry(key: type, value: (records: _, :total, proportion: _)) = record; - final color = type.color.harmonizeWith(context.colorScheme.primary); - return RawChip( - avatar: Icon(type.icon, color: color), - labelStyle: TextStyle(color: color), - label: "${type.localized()}: ${i18n.unit.rmb(total.toStringAsFixed(2))}".text(), - ); - }) - .toList() - .wrap(spacing: 4); - } -} diff --git a/lib/life/expense_records/widget/group.dart b/lib/life/expense_records/widget/group.dart deleted file mode 100644 index 669376430..000000000 --- a/lib/life/expense_records/widget/group.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:sit/design/widgets/grouped.dart'; -import 'package:sit/l10n/extension.dart'; -import 'package:rettulf/rettulf.dart'; - -import '../entity/local.dart'; -import '../utils.dart'; -import '../i18n.dart'; -import 'transaction.dart'; - -class TransactionGroupSection extends StatelessWidget { - final bool initialExpanded; - final YearMonth time; - final List records; - - const TransactionGroupSection({ - required this.time, - required this.records, - this.initialExpanded = true, - super.key, - }); - - @override - Widget build(BuildContext context) { - final (:income, :outcome) = accumulateTransactionIncomeOutcome(records); - return GroupedSection( - headerBuilder: (expanded, toggleExpand, defaultTrailing) { - return ListTile( - title: context.formatYmText((time.toDateTime())).text(), - titleTextStyle: context.textTheme.titleMedium, - subtitle: "${i18n.income(income.toStringAsFixed(2))}\n${i18n.outcome(outcome.toStringAsFixed(2))}" - .text(maxLines: 2), - onTap: toggleExpand, - trailing: defaultTrailing, - ); - }, - initialExpanded: initialExpanded, - itemCount: records.length, - itemBuilder: (ctx, i) { - final record = records[i]; - return TransactionTile(record); - }, - ); - } -} diff --git a/lib/life/expense_records/widget/selector.dart b/lib/life/expense_records/widget/selector.dart deleted file mode 100644 index f3dae0fba..000000000 --- a/lib/life/expense_records/widget/selector.dart +++ /dev/null @@ -1,76 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:rettulf/rettulf.dart'; - -class YearMonthSelector extends StatefulWidget { - final List years; - final List months; - final int initialYear; - final int initialMonth; - final bool enableAllYears; - final bool enableAllMonths; - - const YearMonthSelector({ - super.key, - required this.years, - required this.months, - this.enableAllYears = false, - this.enableAllMonths = false, - required this.initialYear, - required this.initialMonth, - }); - - @override - State createState() => _YearMonthSelectorState(); -} - -class _YearMonthSelectorState extends State { - late int selectedYear; - late int selectedMonth; - - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - buildYearSelector(), - buildMonthSelector(), - ], - ); - } - - Widget buildYearSelector() { - return DropdownMenu( - label: "Year".text(), - initialSelection: widget.initialYear, - onSelected: (int? selected) { - if (selected != null && selected != selectedYear) { - setState(() => selectedYear = selected); - } - }, - dropdownMenuEntries: widget.years - .map((year) => DropdownMenuEntry( - value: year, - label: "$year–${year + 1}", - )) - .toList(), - ); - } - - Widget buildMonthSelector() { - return DropdownMenu( - label: "Month".text(), - initialSelection: widget.initialMonth, - onSelected: (int? selected) { - if (selected != null && selected != selectedYear) { - setState(() => selectedYear = selected); - } - }, - dropdownMenuEntries: widget.months - .map((month) => DropdownMenuEntry( - value: month, - label: month.toString(), - )) - .toList(), - ); - } -} diff --git a/lib/life/expense_records/widget/transaction.dart b/lib/life/expense_records/widget/transaction.dart deleted file mode 100644 index 6d3ab964a..000000000 --- a/lib/life/expense_records/widget/transaction.dart +++ /dev/null @@ -1,72 +0,0 @@ -import 'package:auto_size_text/auto_size_text.dart'; -import 'package:flutter/material.dart'; -import 'package:sit/l10n/extension.dart'; -import 'package:rettulf/rettulf.dart'; -import '../entity/local.dart'; -import "../i18n.dart"; - -class TransactionTile extends StatelessWidget { - final Transaction transaction; - - const TransactionTile(this.transaction, {super.key}); - - @override - Widget build(BuildContext context) { - return ListTile( - title: Text(transaction.bestTitle ?? i18n.unknown, style: context.textTheme.titleSmall), - subtitle: context.formatYmdhmsNum(transaction.timestamp).text(), - leading: transaction.type.icon.make(color: transaction.type.color, size: 32), - trailing: transaction.toReadableString().text( - style: context.textTheme.titleLarge?.copyWith(color: transaction.billColor), - ), - ); - } -} - -class TransactionCard extends StatelessWidget { - final Transaction transaction; - - const TransactionCard({ - super.key, - required this.transaction, - }); - - @override - Widget build(BuildContext context) { - final textTheme = context.textTheme; - final billColor = transaction.isConsume ? null : transaction.billColor; - return [ - [ - transaction.type.icon.make(color: transaction.type.color).padOnly(r: 8), - AutoSizeText( - context.formatMdhmNum(transaction.timestamp), - style: textTheme.titleMedium, - maxLines: 1, - ).expanded(), - ].row(), - [ - AutoSizeText( - transaction.toReadableString(), - style: textTheme.displayMedium?.copyWith(color: billColor), - maxLines: 1, - ).expanded(), - AutoSizeText( - i18n.view.rmb, - style: textTheme.titleMedium?.copyWith(color: billColor), - maxLines: 1, - ), - ].row(caa: CrossAxisAlignment.end), - AutoSizeText( - transaction.shortDeviceName(), - style: textTheme.titleMedium, - maxLines: 1, - ), - ] - .column( - caa: CrossAxisAlignment.start, - maa: MainAxisAlignment.spaceEvenly, - ) - .padAll(10) - .inCard(); - } -} diff --git a/lib/life/i18n.dart b/lib/life/i18n.dart deleted file mode 100644 index 91287ff56..000000000 --- a/lib/life/i18n.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:sit/l10n/common.dart'; - -const i18n = _I18n(); - -class _I18n with CommonI18nMixin { - const _I18n(); - - static const ns = "life"; - - String get navigation => "$ns.navigation".tr(); -} diff --git a/lib/life/index.dart b/lib/life/index.dart deleted file mode 100644 index 51a575fe2..000000000 --- a/lib/life/index.dart +++ /dev/null @@ -1,94 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:sit/credentials/entity/login_status.dart'; -import 'package:sit/credentials/widgets/oa_scope.dart'; -import 'package:sit/life/electricity/index.dart'; -import 'package:sit/life/expense_records/index.dart'; -import 'package:sit/settings/settings.dart'; -import 'package:rettulf/rettulf.dart'; - -import 'event.dart'; -import "i18n.dart"; - -class LifePage extends StatefulWidget { - const LifePage({super.key}); - - @override - State createState() => _LifePageState(); -} - -class _LifePageState extends State { - LoginStatus? loginStatus; - final $campus = Settings.listenCampus(); - - @override - void initState() { - $campus.addListener(refresh); - super.initState(); - } - - @override - void dispose() { - $campus.removeListener(refresh); - super.dispose(); - } - - @override - void didChangeDependencies() { - final newLoginStatus = context.auth.loginStatus; - if (loginStatus != newLoginStatus) { - setState(() { - loginStatus = newLoginStatus; - }); - } - super.didChangeDependencies(); - } - - void refresh() { - setState(() {}); - } - - @override - Widget build(BuildContext context) { - final campus = Settings.campus; - return Scaffold( - resizeToAvoidBottomInset: false, - body: NestedScrollView( - floatHeaderSlivers: true, - headerSliverBuilder: (context, bool innerBoxIsScrolled) { - return [ - SliverOverlapAbsorber( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), - sliver: SliverAppBar( - title: i18n.navigation.text(), - forceElevated: innerBoxIsScrolled, - ), - ), - ]; - }, - body: RefreshIndicator.adaptive( - triggerMode: RefreshIndicatorTriggerMode.anywhere, - onRefresh: () async { - debugPrint("Life page refreshed"); - await HapticFeedback.heavyImpact(); - await lifeEventBus.notifyListeners(); - }, - child: CustomScrollView( - slivers: [ - if (loginStatus != LoginStatus.never) - const SliverToBoxAdapter( - child: ExpenseRecordsAppCard(), - ), - if (campus.capability.enableElectricity) - const SliverToBoxAdapter( - child: ElectricityBalanceAppCard(), - ), - // FIXME: https://github.com/flutter/flutter/issues/36158 - const SliverFillRemaining(), - ], - ), - ), - ), - ); - } -} diff --git a/lib/life/settings.dart b/lib/life/settings.dart deleted file mode 100644 index ac96e0123..000000000 --- a/lib/life/settings.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:hive/hive.dart'; - -const _kElectricityAutoRefresh = true; -const _kExpenseRecordsAutoRefresh = true; - -class LifeSettings { - final Box box; - - LifeSettings(this.box); - - late final electricity = _Electricity(box); - late final expense = _ExpenseRecords(box); - - static const ns = "/life"; -} - -class _ElectricityK { - static const ns = "${LifeSettings.ns}/electricity"; - static const autoRefresh = "$ns/autoRefresh"; -} - -class _Electricity { - final Box box; - - const _Electricity(this.box); - - bool get autoRefresh => box.get(_ElectricityK.autoRefresh) ?? _kElectricityAutoRefresh; - - set autoRefresh(bool foo) => box.put(_ElectricityK.autoRefresh, foo); -} - -class _ExpenseK { - static const ns = "${LifeSettings.ns}/expenseRecords"; - static const autoRefresh = "$ns/autoRefresh"; -} - -class _ExpenseRecords { - final Box box; - - const _ExpenseRecords(this.box); - - bool get autoRefresh => box.get(_ExpenseK.autoRefresh) ?? _kExpenseRecordsAutoRefresh; - - set autoRefresh(bool foo) => box.put(_ExpenseK.autoRefresh, foo); -} diff --git a/lib/login/aggregated.dart b/lib/login/aggregated.dart deleted file mode 100644 index ea0680c83..000000000 --- a/lib/login/aggregated.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:sit/credentials/entity/credential.dart'; -import 'package:sit/credentials/entity/login_status.dart'; -import 'package:sit/credentials/init.dart'; -import 'package:sit/credentials/utils.dart'; -import 'package:sit/init.dart'; -import 'package:sit/settings/settings.dart'; - -import 'init.dart'; - -class LoginAggregated { - static Future login(Credentials credentials) async { - final userType = estimateOaUserType(credentials.account); - await Init.ssoSession.loginLocked(credentials); - // set user's real name to signature by default. - final personName = await LoginInit.authServerService.getPersonName(); - Settings.lastSignature ??= personName; - CredentialsInit.storage.oaCredentials = credentials; - CredentialsInit.storage.oaLoginStatus = LoginStatus.validated; - CredentialsInit.storage.oaLastAuthTime = DateTime.now(); - CredentialsInit.storage.oaUserType = userType; - } -} diff --git a/lib/login/i18n.dart b/lib/login/i18n.dart deleted file mode 100644 index 7f661a425..000000000 --- a/lib/login/i18n.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:sit/credentials/i18n.dart'; -import 'package:sit/l10n/common.dart'; - -const i18n = LoginI18n(); - -class LoginI18n with CommonI18nMixin { - const LoginI18n(); - - final network = const NetworkI18n(); - final credentials = const CredentialsI18n(); - static const ns = "login"; - - String get welcomeHeader => "$ns.welcomeHeader".tr(); - - String get loginOa => "$ns.loginOa".tr(); - - String get credentialsValidatedTip => "$ns.credentialsValidatedTip".tr(); - - String get accountHint => "$ns.accountHint".tr(); - - String get formatError => "$ns.formatError".tr(); - - String get validateInputAccountPwdRequest => "$ns.validateInputAccountPwdRequest".tr(); - - String get loggedInTip => "$ns.loggedInTip".tr(); - - String get notLoggedIn => "$ns.notLoggedIn".tr(); - - String get invalidAccountFormat => "$ns.invalidAccountFormat".tr(); - - String get offlineModeBtn => "$ns.offlineModeBtn".tr(); - - String get oaPwdHint => "$ns.oaPwdHint".tr(); - - String get failedWarn => "$ns.failedWarn".tr(); - - String get accountOrPwdErrorTip => "$ns.accountOrPwdErrorTip".tr(); - - String get unknownAuthErrorTip => "$ns.unknownAuthErrorTip".tr(); - - String get captchaErrorTip => "$ns.captchaErrorTip".tr(); - - String get accountFrozenTip => "$ns.accountFrozenTip".tr(); - - String get schoolServerUnconnectedTip => "$ns.schoolServerUnconnectedTip".tr(); - - String get loginRequired => "$ns.loginRequired".tr(); - - String get neverLoggedInTip => "$ns.neverLoggedInTip".tr(); -} diff --git a/lib/login/init.dart b/lib/login/init.dart deleted file mode 100644 index 79ddd076d..000000000 --- a/lib/login/init.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'service/authserver.dart'; - -class LoginInit { - static late AuthServerService authServerService; - - static void init() { - authServerService = const AuthServerService(); - } -} diff --git a/lib/login/page/index.dart b/lib/login/page/index.dart deleted file mode 100644 index a4510b3be..000000000 --- a/lib/login/page/index.dart +++ /dev/null @@ -1,252 +0,0 @@ -import 'package:connectivity_plus/connectivity_plus.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:go_router/go_router.dart'; -import 'package:sit/credentials/entity/credential.dart'; -import 'package:sit/credentials/entity/login_status.dart'; -import 'package:sit/credentials/init.dart'; -import 'package:sit/credentials/utils.dart'; -import 'package:sit/credentials/widgets/oa_scope.dart'; -import 'package:sit/design/adaptive/dialog.dart'; -import 'package:sit/login/utils.dart'; -import 'package:sit/school/widgets/campus.dart'; -import 'package:rettulf/rettulf.dart'; - -import '../aggregated.dart'; -import '../i18n.dart'; -import '../widgets/forgot_pwd.dart'; - -const _forgotLoginPasswordUrl = - "https://authserver.sit.edu.cn/authserver/getBackPasswordMainPage.do?service=https%3A%2F%2Fmyportal.sit.edu.cn%3A443%2F"; - -class LoginPage extends StatefulWidget { - final bool isGuarded; - - const LoginPage({super.key, required this.isGuarded}); - - @override - State createState() => _LoginPageState(); -} - -class _LoginPageState extends State { - final $account = TextEditingController(); - final $password = TextEditingController(); - final _formKey = GlobalKey(); - bool isPasswordClear = false; - bool isLoggingIn = false; - - @override - void initState() { - super.initState(); - } - - @override - void dispose() { - $account.dispose(); - $password.dispose(); - super.dispose(); - } - - @override - void didChangeDependencies() { - final oaCredential = context.auth.credentials; - if (oaCredential != null) { - $account.text = oaCredential.account; - $password.text = oaCredential.password; - } - super.didChangeDependencies(); - } - - /// 用户点击登录按钮后 - Future login() async { - final account = $account.text; - final password = $password.text; - final userType = estimateOaUserType(account); - bool formValid = (_formKey.currentState as FormState).validate() && userType != null; - if (!formValid || account.isEmpty || password.isEmpty) { - await context.showTip( - title: i18n.formatError, - desc: i18n.validateInputAccountPwdRequest, - ok: i18n.close, - serious: true, - ); - return; - } - - if (!mounted) return; - setState(() => isLoggingIn = true); - final connectionType = await Connectivity().checkConnectivity(); - if (connectionType == ConnectivityResult.none) { - if (!mounted) return; - setState(() => isLoggingIn = false); - await context.showTip( - title: i18n.network.error, - desc: i18n.network.noAccessTip, - ok: i18n.close, - serious: true, - ); - return; - } - - try { - await LoginAggregated.login(Credentials(account: account, password: password)); - if (!mounted) return; - setState(() => isLoggingIn = false); - context.go("/"); - } catch (error, stackTrace) { - if (!mounted) return; - setState(() => isLoggingIn = false); - if (error is Exception) { - await handleLoginException(context: context, error: error, stackTrace: stackTrace); - } - } - } - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: () { - // dismiss the keyboard when tap out of TextField. - FocusScope.of(context).requestFocus(FocusNode()); - }, - child: Scaffold( - appBar: AppBar( - title: widget.isGuarded ? i18n.loginRequired.text() : const CampusSelector(), - actions: [ - IconButton( - icon: const Icon(Icons.settings), - onPressed: () { - context.push("/settings"); - }, - ), - ], - bottom: isLoggingIn - ? const PreferredSize( - preferredSize: Size.fromHeight(4), - child: LinearProgressIndicator(), - ) - : null, - ), - body: buildBody(), - //to avoid overflow when keyboard is up. - bottomNavigationBar: const ForgotPasswordButton(url: _forgotLoginPasswordUrl), - ), - ); - } - - Widget buildBody() { - return [ - widget.isGuarded - ? const Icon( - Icons.person_off_outlined, - size: 120, - ) - : i18n.welcomeHeader.text( - style: context.textTheme.displayMedium?.copyWith(fontWeight: FontWeight.bold), - textAlign: TextAlign.center, - ), - Padding(padding: EdgeInsets.only(top: 40.h)), - // Form field: username and password. - buildLoginForm(), - SizedBox(height: 10.h), - buildLoginButton(), - ].column(mas: MainAxisSize.min).scrolled(physics: const NeverScrollableScrollPhysics()).padH(25.h).center(); - } - - Widget buildLoginForm() { - return Form( - autovalidateMode: AutovalidateMode.always, - key: _formKey, - child: AutofillGroup( - child: Column( - children: [ - TextFormField( - controller: $account, - autofillHints: const [AutofillHints.username], - textInputAction: TextInputAction.next, - autocorrect: false, - autofocus: true, - readOnly: isLoggingIn, - enableSuggestions: false, - validator: (account) => studentIdValidator(account, () => i18n.invalidAccountFormat), - decoration: InputDecoration( - labelText: i18n.credentials.account, - hintText: i18n.accountHint, - icon: const Icon(Icons.person), - ), - ), - TextFormField( - controller: $password, - keyboardType: isPasswordClear ? TextInputType.visiblePassword : null, - autofillHints: const [AutofillHints.password], - textInputAction: TextInputAction.send, - readOnly: isLoggingIn, - contextMenuBuilder: (ctx, state) { - return AdaptiveTextSelectionToolbar.editableText( - editableTextState: state, - ); - }, - autocorrect: false, - autofocus: true, - enableSuggestions: false, - obscureText: !isPasswordClear, - onFieldSubmitted: (inputted) async { - await login(); - }, - decoration: InputDecoration( - labelText: i18n.credentials.oaPwd, - hintText: i18n.oaPwdHint, - icon: const Icon(Icons.lock), - suffixIcon: IconButton( - icon: Icon(isPasswordClear ? Icons.visibility : Icons.visibility_off), - onPressed: () { - setState(() { - isPasswordClear = !isPasswordClear; - }); - }, - ), - ), - ), - ], - ), - ), - ); - } - - Widget buildLoginButton() { - return [ - $account >> - (ctx, account) => FilledButton.icon( - // Online - onPressed: !isLoggingIn && account.text.isNotEmpty - ? () { - // un-focus the text field. - FocusScope.of(context).requestFocus(FocusNode()); - login(); - } - : null, - icon: const Icon(Icons.login), - label: i18n.credentials.login.text(), - ), - if (!widget.isGuarded) - OutlinedButton( - // Offline - onPressed: () { - CredentialsInit.storage.oaLoginStatus = LoginStatus.offline; - context.go("/"); - }, - child: i18n.offlineModeBtn.text(), - ), - ].row(caa: CrossAxisAlignment.center, maa: MainAxisAlignment.spaceAround); - } -} - -/// Only allow student ID/ work number. -String? studentIdValidator(String? account, String Function() invalidMessage) { - if (account != null && account.isNotEmpty) { - if (estimateOaUserType(account) == null) { - return invalidMessage(); - } - } - return null; -} diff --git a/lib/login/service/authserver.dart b/lib/login/service/authserver.dart deleted file mode 100644 index 2143c2dd4..000000000 --- a/lib/login/service/authserver.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:beautiful_soup_dart/beautiful_soup.dart'; -import 'package:dio/dio.dart'; -import 'package:sit/init.dart'; - -import 'package:sit/session/sso.dart'; -import 'package:sit/utils/error.dart'; - -class AuthServerService { - SsoSession get session => Init.ssoSession; - - const AuthServerService(); - - Future getPersonName() async { - try { - final response = await session.request( - 'https://authserver.sit.edu.cn/authserver/index.do', - options: Options( - method: "GET", - ), - ); - final html = BeautifulSoup(response.data); - final resultDesktop = html.find('div', class_: 'auth_username')?.text ?? ''; - final resultMobile = html.find('div', class_: 'index-nav-name')?.text ?? ''; - - final result = (resultMobile + resultDesktop).trim(); - return result.isNotEmpty ? result : null; - } catch (error, stackTrace) { - debugPrintError(error, stackTrace); - return null; - } - } -} diff --git a/lib/login/utils.dart b/lib/login/utils.dart deleted file mode 100644 index 4b244f7c3..000000000 --- a/lib/login/utils.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:dio/dio.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:sit/credentials/error.dart'; -import 'package:sit/design/adaptive/dialog.dart'; -import 'package:sit/session/sso.dart'; -import 'package:sit/utils/error.dart'; -import "./i18n.dart"; - -Future handleLoginException({ - required BuildContext context, - required Exception error, - required StackTrace stackTrace, -}) async { - debugPrintError(error, stackTrace); - if (!context.mounted) return; - if (error is CredentialsException) { - await context.showTip( - serious: true, - title: i18n.failedWarn, - desc: switch (error.type) { - CredentialsErrorType.accountPassword => i18n.accountOrPwdErrorTip, - CredentialsErrorType.captcha => i18n.captchaErrorTip, - CredentialsErrorType.frozen => i18n.accountFrozenTip, - }, - ok: i18n.close, - ); - return; - } - if (error is DioException) { - await context.showTip( - serious: true, - title: i18n.failedWarn, - desc: i18n.schoolServerUnconnectedTip, - ok: i18n.close, - ); - return; - } - if (error is LoginCaptchaCancelledException) { - if (!context.mounted) return; - return; - } - if (!context.mounted) return; - await context.showTip( - serious: true, - title: i18n.failedWarn, - desc: i18n.unknownAuthErrorTip, - ok: i18n.close, - ); - return; -} diff --git a/lib/login/widgets/forgot_pwd.dart b/lib/login/widgets/forgot_pwd.dart deleted file mode 100644 index 609229de5..000000000 --- a/lib/login/widgets/forgot_pwd.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:sit/utils/guard_launch.dart'; -import '../i18n.dart'; - -class ForgotPasswordButton extends StatelessWidget { - final String url; - - const ForgotPasswordButton({ - super.key, - required this.url, - }); - - @override - Widget build(BuildContext context) { - return PlatformTextButton( - child: i18n.credentials.forgotPwd.text( - style: const TextStyle(color: Colors.grey), - ), - onPressed: () { - guardLaunchUrlString(context, url); - }, - ); - } -} diff --git a/lib/main.dart b/lib/main.dart deleted file mode 100644 index faec0c8ea..000000000 --- a/lib/main.dart +++ /dev/null @@ -1,144 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:sit/files.dart'; -import 'package:sit/migration/foundation.dart'; -import 'package:sit/network/proxy.dart'; -import 'package:sit/platform/windows/windows.dart'; -import 'package:sit/storage/hive/init.dart'; -import 'package:sit/init.dart'; -import 'package:sit/migration/migrations.dart'; -import 'package:sit/platform/desktop.dart'; -import 'package:sit/school/yellow_pages/entity/contact.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:sit/settings/meta.dart'; -import 'package:sit/settings/settings.dart'; -import 'package:sit/entity/version.dart'; -import 'package:sit/storage/prefs.dart'; -import 'package:system_theme/system_theme.dart'; -import 'package:version/version.dart'; - -import 'app.dart'; - -import 'l10n/yaml_assets_loader.dart'; -import 'r.dart'; - -void main() async { - // debugRepaintRainbowEnabled = true; - // debugRepaintTextRainbowEnabled = true; - // debugPaintSizeEnabled = true; - WidgetsFlutterBinding.ensureInitialized(); - final prefs = await SharedPreferences.getInstance(); - final lastSize = prefs.getLastWindowSize(); - await DesktopInit.init(size: lastSize); - await WindowsInit.registerCustomScheme(R.scheme); - if (prefs.getInstallTime() == null) { - await prefs.setInstallTime(DateTime.now()); - } - // Initialize the window size before others for a better experience when loading. - await SystemTheme.accentColor.load(); - await EasyLocalization.ensureInitialized(); - Migrations.init(); - - if (!kIsWeb) { - Files.cache = await getApplicationCacheDirectory(); - debugPrint("Cache ${Files.cache}"); - Files.temp = await getTemporaryDirectory(); - debugPrint("Temp ${Files.temp}"); - Files.internal = await getApplicationSupportDirectory(); - debugPrint("Internal ${Files.internal}"); - Files.user = await getApplicationDocumentsDirectory(); - debugPrint("User ${Files.user}"); - } - await Files.init(); - // Perform migrations - R.currentVersion = await getCurrentVersion(); - final currentVersion = R.currentVersion.full; - final lastVersionRaw = prefs.getLastVersion(); - final lastVersion = lastVersionRaw != null ? Version.parse(lastVersionRaw) : currentVersion; - final migrations = Migrations.match(from: lastVersion, to: currentVersion); - - await migrations.perform(MigrationPhrase.beforeHive); - await prefs.setLastVersion(lastVersion.toString()); - - R.roomList = await _loadRoomNumberList(); - R.userAgentList = await _loadUserAgents(); - R.yellowPages = await _loadYellowPages(); - - // Initialize Hive - if (!kIsWeb) { - await HiveInit.initLocalStorage( - coreDir: Files.internal.subDir("hive", R.hiveStorageVersion), - cacheDir: Files.cache.subDir("hive", R.hiveStorageVersion), - ); - } - HiveInit.initAdapters(); - await HiveInit.initBox(); - - // Setup Settings and Meta - if (kDebugMode) { - Settings.isDeveloperMode = true; - } - // The last time when user launch this app - Meta.lastLaunchTime = Meta.thisLaunchTime; - Meta.thisLaunchTime = DateTime.now(); - await migrations.perform(MigrationPhrase.afterHive); - Init.registerCustomEditor(); - HttpOverrides.global = SitHttpOverrides(); - await Init.initNetwork(); - await Init.initModules(); - runApp( - const MimirApp().withEasyLocalization().withScreenUtils(), - ); -} - -final _yamlAssetsLoader = YamlAssetLoader(); - -extension _AppX on Widget { - Widget withEasyLocalization() { - return EasyLocalization( - supportedLocales: R.supportedLocales, - path: 'assets/l10n', - fallbackLocale: R.defaultLocale, - useFallbackTranslations: true, - assetLoader: _yamlAssetsLoader, - child: this, - ); - } - - Widget withScreenUtils() { - return ScreenUtilInit( - designSize: const Size(360, 690), - minTextAdapt: true, - splitScreenMode: true, - builder: (context, child) { - return this; - }, - ); - } -} - -Future> _loadRoomNumberList() async { - String jsonData = await rootBundle.loadString("assets/room_list.json"); - List list = await jsonDecode(jsonData); - return list.map((e) => e.toString()).toList(); -} - -Future> _loadUserAgents() async { - String jsonData = await rootBundle.loadString("assets/user_agent.json"); - List list = await jsonDecode(jsonData); - return list.cast(); -} - -Future> _loadYellowPages() async { - String jsonData = await rootBundle.loadString("assets/yellow_pages.json"); - List list = await jsonDecode(jsonData); - return list.map((e) => SchoolContact.fromJson(e)).toList().cast(); -} diff --git a/lib/me/edu_email/entity/email.dart b/lib/me/edu_email/entity/email.dart deleted file mode 100644 index 8b1378917..000000000 --- a/lib/me/edu_email/entity/email.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/lib/me/edu_email/i18n.dart b/lib/me/edu_email/i18n.dart deleted file mode 100644 index 0fc0dec2a..000000000 --- a/lib/me/edu_email/i18n.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:sit/credentials/i18n.dart'; -import 'package:sit/l10n/common.dart'; - -const i18n = _I18n(); - -class _I18n with CommonI18nMixin { - const _I18n(); - - static const ns = "eduEmail"; - final action = const _Action(); - final login = const _Login(); - final inbox = const _Inbox(); - final outbox = const _Outbox(); - final info = const _Info(); - - String get title => "$ns.title".tr(); - - String get noContent => "$ns.noContent".tr(); - - String get noSubject => "$ns.noSubject".tr(); - - String get pluralSenderTailing => "$ns.pluralSenderTailing".tr(); - - String get text => "$ns.text".tr(); -} - -class _Action { - const _Action(); - - static const ns = "${_I18n.ns}.action"; - - String get login => "$ns.login".tr(); - - String get inbox => "$ns.inbox".tr(); - - String get outbox => "$ns.outbox".tr(); -} - -class _Login { - const _Login(); - - final credentials = const CredentialsI18n(); - - static const ns = "${_I18n.ns}.login"; - - String get title => "$ns.title".tr(); - - String get passwordHint => "$ns.passwordHint".tr(); - - String get addressHint => "$ns.addressHint".tr(); - - String get invalidEmailAddressFormatTip => "$ns.invalidEmailAddressFormatTip".tr(); - - String get failedWarn => "$ns.failedWarn.title".tr(); - - String get failedWarnDesc => "$ns.failedWarn.desc".tr(); -} - -class _Info { - const _Info(); - - static const ns = "${_I18n.ns}.info"; - - String get emailAddress => "$ns.emailAddress".tr(); -} - -class _Outbox { - const _Outbox(); - - static const ns = "${_I18n.ns}.outbox"; - - String get title => "$ns.title".tr(); -} - -class _Inbox { - const _Inbox(); - - static const ns = "${_I18n.ns}.inbox"; - - String get title => "$ns.title".tr(); -} diff --git a/lib/me/edu_email/index.dart b/lib/me/edu_email/index.dart deleted file mode 100644 index 7d47c3e53..000000000 --- a/lib/me/edu_email/index.dart +++ /dev/null @@ -1,70 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:sit/credentials/init.dart'; -import 'package:sit/design/widgets/app.dart'; -import 'package:rettulf/rettulf.dart'; - -import "i18n.dart"; - -class EduEmailAppCard extends StatefulWidget { - const EduEmailAppCard({super.key}); - - @override - State createState() => _EduEmailAppCardState(); -} - -class _EduEmailAppCardState extends State { - final $credentials = CredentialsInit.storage.listenEduEmailChange(); - - @override - void initState() { - $credentials.addListener(onCredentialsChanged); - super.initState(); - } - - @override - void dispose() { - $credentials.removeListener(onCredentialsChanged); - super.dispose(); - } - - void onCredentialsChanged() { - setState(() {}); - } - - @override - Widget build(BuildContext context) { - final credentials = CredentialsInit.storage.eduEmailCredentials; - final email = credentials?.account.toString(); - return AppCard( - title: i18n.title.text(), - subtitle: email != null ? SelectableText(email) : null, - leftActions: credentials == null - ? [ - FilledButton.icon( - onPressed: () { - context.push("/edu-email/login"); - }, - icon: const Icon(Icons.login), - label: i18n.action.login.text(), - ), - ] - : [ - FilledButton.icon( - onPressed: () { - context.push("/edu-email/inbox"); - }, - icon: const Icon(Icons.inbox), - label: i18n.action.inbox.text(), - ), - OutlinedButton.icon( - onPressed: () { - context.push("/edu-email/outbox"); - }, - icon: const Icon(Icons.outbox), - label: i18n.action.outbox.text(), - ), - ], - ); - } -} diff --git a/lib/me/edu_email/init.dart b/lib/me/edu_email/init.dart deleted file mode 100644 index 28cdcc349..000000000 --- a/lib/me/edu_email/init.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'service/email.dart'; -import 'storage/email.dart'; - -class EduEmailInit { - static late EduEmailStorage storage; - static late MailService service; - - static void init() { - storage = const EduEmailStorage(); - service = MailService(); - } -} diff --git a/lib/me/edu_email/page/details.dart b/lib/me/edu_email/page/details.dart deleted file mode 100644 index b0760d47b..000000000 --- a/lib/me/edu_email/page/details.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'package:enough_mail/enough_mail.dart'; -import 'package:enough_mail_html/enough_mail_html.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; -import 'package:sit/l10n/extension.dart'; -import 'package:sit/widgets/html.dart'; -import 'package:rettulf/rettulf.dart'; - -import '../i18n.dart'; - -// TODO: Better UI -class EduEmailDetailsPage extends StatelessWidget { - final MimeMessage message; - - const EduEmailDetailsPage(this.message, {super.key}); - - @override - Widget build(BuildContext context) { - final subject = message.decodeSubject() ?? i18n.noSubject; - return Scaffold( - body: SelectionArea( - child: CustomScrollView( - slivers: [ - SliverAppBar( - floating: true, - title: subject.text(), - ), - SliverToBoxAdapter( - child: MailMetaCard(message), - ), - SliverPadding( - padding: const EdgeInsets.all(8), - sliver: RestyledHtmlWidget( - _generateHtml(context, message), - renderMode: RenderMode.sliverList, - ), - ) - ], - ), - ), - ); - } -} - -String _generateHtml(BuildContext context, MimeMessage mimeMessage) { - return mimeMessage.transformToHtml( - blockExternalImages: false, - preferPlainText: true, - enableDarkMode: context.isDarkMode, - emptyMessageText: i18n.noContent, - ); -} - -class MailMetaCard extends StatelessWidget { - final MimeMessage message; - - const MailMetaCard(this.message, {super.key}); - - @override - Widget build(BuildContext context) { - final subject = message.decodeSubject() ?? i18n.noSubject; - final sender = message.decodeSender(); - var senderText = sender[0].toString(); - if (sender.length > 1) { - senderText += i18n.pluralSenderTailing; - } - final date = message.decodeDate(); - final dateText = date != null ? context.formatYmdhmsNum(date) : ''; - return [Text(subject), Text('$senderText\n$dateText')].column().inCard(); - } -} diff --git a/lib/me/edu_email/page/inbox.dart b/lib/me/edu_email/page/inbox.dart deleted file mode 100644 index 7c4f8f8cf..000000000 --- a/lib/me/edu_email/page/inbox.dart +++ /dev/null @@ -1,101 +0,0 @@ -import 'package:enough_mail/enough_mail.dart'; -import 'package:flutter/material.dart'; -import 'package:sit/credentials/entity/credential.dart'; -import 'package:sit/credentials/init.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:sit/utils/error.dart'; - -import '../init.dart'; -import '../i18n.dart'; -import '../widgets/item.dart'; - -// TODO: Send email -class EduEmailInboxPage extends StatefulWidget { - const EduEmailInboxPage({super.key}); - - @override - State createState() => _EduEmailInboxPageState(); -} - -class _EduEmailInboxPageState extends State { - List? messages; - Credentials? credential = CredentialsInit.storage.eduEmailCredentials; - final onEduEmailChanged = CredentialsInit.storage.listenEduEmailChange(); - - @override - void initState() { - super.initState(); - onEduEmailChanged.addListener(updateCredential); - refresh(); - } - - @override - void dispose() { - onEduEmailChanged.removeListener(updateCredential); - super.dispose(); - } - - void updateCredential() { - final newCredential = CredentialsInit.storage.eduEmailCredentials; - setState(() { - credential = newCredential; - }); - if (newCredential != null) { - refresh(); - } - } - - Future refresh() async { - final credential = this.credential; - if (credential == null) return; - try { - await EduEmailInit.service.login(credential); - } catch (error, stackTrace) { - debugPrintError(error, stackTrace); - CredentialsInit.storage.eduEmailCredentials = null; - return; - } - try { - final result = await EduEmailInit.service.getInboxMessage(30); - final messages = result.messages; - // The more recent the time, the smaller the index in the list. - messages.sort((a, b) { - return a.decodeDate()!.isAfter(b.decodeDate()!) ? -1 : 1; - }); - if (!mounted) return; - setState(() { - this.messages = messages; - }); - } catch (error, stackTrace) { - debugPrintError(error, stackTrace); - } - } - - @override - Widget build(BuildContext context) { - final messages = this.messages; - return Scaffold( - body: CustomScrollView( - slivers: [ - SliverAppBar( - floating: true, - title: i18n.inbox.title.text(), - bottom: credential != null && messages == null - ? const PreferredSize( - preferredSize: Size.fromHeight(4), - child: LinearProgressIndicator(), - ) - : null, - ), - if (messages != null) - SliverList.builder( - itemCount: messages.length, - itemBuilder: (ctx, i) { - return EmailItem(messages[i]); - }, - ) - ], - ), - ); - } -} diff --git a/lib/me/edu_email/page/login.dart b/lib/me/edu_email/page/login.dart deleted file mode 100644 index 50104409d..000000000 --- a/lib/me/edu_email/page/login.dart +++ /dev/null @@ -1,169 +0,0 @@ -import 'package:email_validator/email_validator.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:go_router/go_router.dart'; -import 'package:sit/credentials/entity/credential.dart'; -import 'package:sit/credentials/init.dart'; -import 'package:sit/design/adaptive/dialog.dart'; -import 'package:sit/login/widgets/forgot_pwd.dart'; -import 'package:sit/r.dart'; -import 'package:rettulf/rettulf.dart'; -import '../init.dart'; -import '../i18n.dart'; - -const _forgotLoginPasswordUrl = - "http://imap.mail.sit.edu.cn//edu_reg/retrieve/redirect?redirectURL=http://imap.mail.sit.edu.cn/coremail/index.jsp"; - -class EduEmailLoginPage extends StatefulWidget { - const EduEmailLoginPage({super.key}); - - @override - State createState() => _EduEmailLoginPageState(); -} - -class _EduEmailLoginPageState extends State { - final initialAccount = CredentialsInit.storage.oaCredentials?.account; - late final $username = TextEditingController(text: initialAccount); - final $password = TextEditingController(); - final _formKey = GlobalKey(); - bool isPasswordClear = false; - bool isLoggingIn = false; - - @override - void dispose() { - $username.dispose(); - $password.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: () { - // dismiss the keyboard when tap out of TextField. - FocusScope.of(context).requestFocus(FocusNode()); - }, - child: Scaffold( - appBar: AppBar( - title: i18n.login.title.text(), - bottom: isLoggingIn - ? const PreferredSize( - preferredSize: Size.fromHeight(4), - child: LinearProgressIndicator(), - ) - : null, - ), - body: buildBody(), - bottomNavigationBar: const ForgotPasswordButton(url: _forgotLoginPasswordUrl), - ), - ); - } - - Widget buildBody() { - return [ - buildForm(), - SizedBox(height: 10.h), - buildLoginButton(), - ].column(mas: MainAxisSize.min).scrolled(physics: const NeverScrollableScrollPhysics()).padH(25.h).center(); - } - - Widget buildForm() { - return Form( - autovalidateMode: AutovalidateMode.always, - key: _formKey, - child: Column( - children: [ - TextFormField( - controller: $username, - textInputAction: TextInputAction.next, - autofocus: true, - readOnly: !kDebugMode && initialAccount != null, - autocorrect: false, - enableSuggestions: false, - validator: (username) { - if (username == null) return null; - if (EmailValidator.validate(R.formatEduEmail(username: username))) return null; - return i18n.login.invalidEmailAddressFormatTip; - }, - decoration: InputDecoration( - labelText: i18n.info.emailAddress, - hintText: i18n.login.addressHint, - suffixText: "@${R.eduEmailDomain}", - icon: const Icon(Icons.alternate_email_outlined), - ), - ), - TextFormField( - controller: $password, - autofocus: true, - keyboardType: isPasswordClear ? TextInputType.visiblePassword : null, - textInputAction: TextInputAction.send, - contextMenuBuilder: (ctx, state) { - return AdaptiveTextSelectionToolbar.editableText( - editableTextState: state, - ); - }, - autocorrect: false, - enableSuggestions: false, - obscureText: !isPasswordClear, - onFieldSubmitted: (inputted) async { - if (!isLoggingIn) { - await onLogin(); - } - }, - decoration: InputDecoration( - labelText: i18n.login.credentials.password, - icon: const Icon(Icons.lock), - suffixIcon: IconButton( - icon: Icon(isPasswordClear ? Icons.visibility : Icons.visibility_off), - onPressed: () { - setState(() { - isPasswordClear = !isPasswordClear; - }); - }, - ), - ), - ), - ], - ), - ); - } - - Widget buildLoginButton() { - return $username >> - (ctx, account) => FilledButton.icon( - // Online - onPressed: !isLoggingIn && account.text.isNotEmpty - ? () async { - // un-focus the text field. - FocusScope.of(context).requestFocus(FocusNode()); - await onLogin(); - } - : null, - icon: const Icon(Icons.login), - label: i18n.login.credentials.login.text().padAll(5), - ); - } - - Future onLogin() async { - final credential = Credentials( - account: R.formatEduEmail(username: $username.text), - password: $password.text, - ); - try { - if (!mounted) return; - setState(() => isLoggingIn = true); - await EduEmailInit.service.login(credential); - CredentialsInit.storage.eduEmailCredentials = credential; - if (!mounted) return; - setState(() => isLoggingIn = false); - context.replace("/edu-email/inbox"); - } catch (err) { - if (!mounted) return; - await context.showTip(title: i18n.login.failedWarn, desc: i18n.login.failedWarnDesc, ok: i18n.ok); - if (!mounted) return; - setState(() => isLoggingIn = false); - return; - } - } -} diff --git a/lib/me/edu_email/page/outbox.dart b/lib/me/edu_email/page/outbox.dart deleted file mode 100644 index ddd1b4492..000000000 --- a/lib/me/edu_email/page/outbox.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:rettulf/rettulf.dart'; -import '../i18n.dart'; - -class EduEmailOutboxPage extends StatefulWidget { - const EduEmailOutboxPage({super.key}); - - @override - State createState() => _EduEmailOutboxPageState(); -} - -class _EduEmailOutboxPageState extends State { - @override - Widget build(BuildContext context) { - return Scaffold( - body: CustomScrollView( - slivers: [ - SliverAppBar( - title: i18n.outbox.title.text(), - ), - ], - ), - ); - } -} diff --git a/lib/me/edu_email/service/email.dart b/lib/me/edu_email/service/email.dart deleted file mode 100644 index 97f3451ec..000000000 --- a/lib/me/edu_email/service/email.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:enough_mail/enough_mail.dart'; -import 'package:sit/credentials/entity/credential.dart'; - -const _server = "imap.mail.sit.edu.cn"; -const _port = 993; - -class MailService { - final ImapClient _client = ImapClient(isLogEnabled: true, onBadCertificate: (_) => true); - - Future> login(Credentials credential) async { - await _client.connectToServer(_server, _port, isSecure: true); - return await _client.login(credential.account, credential.password); - } - - Future getInboxMessage([int count = 30]) async { - final mailboxes = await _client.listMailboxes(); - await _client.selectInbox(); - return await _client.fetchRecentMessages(messageCount: count); - } -} diff --git a/lib/me/edu_email/storage/email.dart b/lib/me/edu_email/storage/email.dart deleted file mode 100644 index 1a4214afa..000000000 --- a/lib/me/edu_email/storage/email.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'package:hive/hive.dart'; -import 'package:sit/storage/hive/init.dart'; - -class EduEmailStorage { - Box get box => HiveInit.eduEmail; - - const EduEmailStorage(); -} diff --git a/lib/me/edu_email/widgets/item.dart b/lib/me/edu_email/widgets/item.dart deleted file mode 100644 index e57032ca7..000000000 --- a/lib/me/edu_email/widgets/item.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'package:enough_mail/enough_mail.dart'; -import 'package:flutter/material.dart'; -import 'package:sit/l10n/extension.dart'; - -import '../page/details.dart'; - -// TODO: Migration -class EmailItem extends StatelessWidget { - final MimeMessage _message; - - const EmailItem(this._message, {super.key}); - - @override - Widget build(BuildContext context) { - final titleStyle = Theme.of(context).textTheme.titleMedium; - final subtitleStyle = Theme.of(context).textTheme.bodyMedium; - - final subjectText = _message.decodeSubject() ?? ''; - final sender = _message.decodeSender(); - final senderText = sender[0].toString() + (sender.length > 1 ? '...' : ''); - final date = _message.decodeDate(); - final dateText = date != null ? context.formatYmdNum(date) : ''; - - return ListTile( - title: Text(subjectText, style: titleStyle, maxLines: 1, overflow: TextOverflow.fade), - subtitle: Text(senderText, style: subtitleStyle), - trailing: Text(dateText, style: subtitleStyle), - onTap: () { - Navigator.of(context).push(MaterialPageRoute(builder: (_) => EduEmailDetailsPage(_message))); - }, - ); - } -} diff --git a/lib/me/i18n.dart b/lib/me/i18n.dart deleted file mode 100644 index 1598f791c..000000000 --- a/lib/me/i18n.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:sit/l10n/common.dart'; - -const i18n = _I18n(); - -class _I18n with CommonI18nMixin { - const _I18n(); - - static const ns = "me"; - - String get navigation => "$ns.navigation".tr(); -} diff --git a/lib/me/index.dart b/lib/me/index.dart deleted file mode 100644 index da5414800..000000000 --- a/lib/me/index.dart +++ /dev/null @@ -1,126 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:go_router/go_router.dart'; -import 'package:sit/design/adaptive/dialog.dart'; -import 'package:sit/me/edu_email/index.dart'; -import 'package:sit/me/widgets/greeting.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:sit/qrcode/handle.dart'; -import 'package:sit/settings/settings.dart'; -import 'package:sit/utils/guard_launch.dart'; -import 'package:url_launcher/url_launcher_string.dart'; -import "i18n.dart"; - -const _qGroupNumber = "917740212"; -const _joinQGroupUri = - "mqqapi://card/show_pslcard?src_type=internal&version=1&uin=$_qGroupNumber&card_type=group&source=qrcode"; - -class MePage extends StatefulWidget { - const MePage({super.key}); - - @override - State createState() => _MePageState(); -} - -class _MePageState extends State { - @override - Widget build(BuildContext context) { - return Scaffold( - resizeToAvoidBottomInset: false, - body: CustomScrollView( - slivers: [ - SliverAppBar( - titleTextStyle: context.textTheme.headlineSmall, - actions: [ - buildScannerAction(), - IconButton( - icon: const Icon(Icons.settings), - onPressed: () { - context.push("/settings"); - }, - ), - ], - ), - const SliverToBoxAdapter( - child: Greeting(), - ), - const SliverToBoxAdapter( - child: EduEmailAppCard(), - ), - SliverList.list(children: [ - buildGroupInvitationTile(), - ListTile( - leading: const Icon(Icons.videogame_asset), - title: "2048 Game".text(), - trailing: const Icon(Icons.navigate_next), - onTap: () async { - await context.push("/game/2048"); - }, - ) - ]), - ], - ), - ); - } - - Widget buildGroupInvitationTile() { - return ListTile( - title: "预览版 QQ交流群".text(), - subtitle: _qGroupNumber.text(), - trailing: [ - IconButton( - onPressed: () async { - try { - await launchUrlString(_joinQGroupUri); - } catch (_) {} - }, - icon: const Icon(Icons.group), - ), - IconButton( - tooltip: i18n.copy, - onPressed: () async { - await Clipboard.setData(const ClipboardData(text: _qGroupNumber)); - if (!mounted) return; - context.showSnackBar(content: "已复制到剪贴板".text()); - }, - icon: const Icon(Icons.copy), - ), - ].row(mas: MainAxisSize.min), - onTap: () async { - try { - await launchUrlString(_joinQGroupUri); - } catch (_) {} - }, - ); - } - - Widget buildScannerAction() { - return IconButton( - onPressed: () async { - final res = await context.push("/tools/scanner"); - if (!mounted) return; - if (Settings.isDeveloperMode) { - await context.showTip(title: "Result", desc: res.toString(), ok: i18n.ok); - } - if (!mounted) return; - if (res == null) return; - if (res is String) { - final result = await onHandleQrCodeData(context: context, data: res); - if (result == QrCodeHandleResult.success) { - return; - } - if (!mounted) return; - final maybeUri = Uri.tryParse(res); - if (maybeUri != null) { - await guardLaunchUrlString(context, res); - return; - } - await context.showTip(title: "Result", desc: res.toString(), ok: i18n.ok); - } else { - await context.showTip(title: "Result", desc: res.toString(), ok: i18n.ok); - } - }, - icon: const Icon(Icons.qr_code_scanner_outlined), - ); - } -} diff --git a/lib/me/widgets/greeting.dart b/lib/me/widgets/greeting.dart deleted file mode 100644 index 7ec3ce1d5..000000000 --- a/lib/me/widgets/greeting.dart +++ /dev/null @@ -1,103 +0,0 @@ -import 'dart:async'; - -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:sit/credentials/widgets/oa_scope.dart'; -import 'package:sit/entity/campus.dart'; -import 'package:sit/l10n/common.dart'; -import 'package:sit/settings/settings.dart'; -import 'package:sit/utils/timer.dart'; -import 'package:rettulf/rettulf.dart'; - -class Greeting extends StatefulWidget { - const Greeting({super.key}); - - @override - State createState() => _GreetingState(); -} - -class _GreetingState extends State { - int? studyDays; - Campus campus = Settings.campus; - - Timer? dayWatcher; - DateTime? _admissionDate; - - @override - void initState() { - super.initState(); - - /// Rebuild the study days when date is changed. - dayWatcher = runPeriodically(const Duration(minutes: 1), (timer) { - final admissionDate = _admissionDate; - if (admissionDate != null) { - final now = DateTime.now(); - setState(() { - studyDays = now.difference(admissionDate).inDays; - }); - } - }); - } - - @override - void didChangeDependencies() { - // 如果用户不是新生或老师,那么就显示学习天数 - if (context.auth.credentials != null) { - setState(() { - studyDays = _getStudyDaysAndInitState(); - }); - } - super.didChangeDependencies(); - } - - @override - void dispose() { - dayWatcher?.cancel(); - super.dispose(); - } - - int _getStudyDaysAndInitState() { - final oaCredential = context.auth.credentials; - if (oaCredential != null) { - final id = oaCredential.account; - - if (id.isNotEmpty) { - final admissionYearTrailing = int.tryParse(id.substring(0, 2)); - if (admissionYearTrailing != null) { - int admissionYear = 2000 + admissionYearTrailing; - final admissionDate = DateTime(admissionYear, 9, 1); - _admissionDate = admissionDate; - - /// 计算入学时间, 默认按 9 月 1 日开学来算. 年份 admissionYear 是完整的年份, 如 2018. - return DateTime.now().difference(admissionDate).inDays; - } - } - } - return 0; - } - - @override - Widget build(BuildContext context) { - final days = studyDays; - return ListTile( - titleTextStyle: context.textTheme.titleMedium, - title: _i18n.headerA.text(), - subtitleTextStyle: context.textTheme.headlineSmall, - subtitle: _i18n.headerB((days ?? 0) + 1).text(), - ); - } -} - -const _i18n = _I18n(); - -class _I18n { - const _I18n(); - - static const ns = "greeting"; - - final campus = const CampusI10n(); - - String get headerA => "$ns.headerA".tr(); - - String headerB(int day) => "$ns.headerB".plural(day, args: [day.toString()]); -} diff --git a/lib/migration/all/cache.dart b/lib/migration/all/cache.dart deleted file mode 100644 index 2227e99b2..000000000 --- a/lib/migration/all/cache.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:sit/storage/hive/init.dart'; -import 'package:sit/migration/foundation.dart'; - -// ignore: non_constant_identifier_names -final ClearCacheMigration = _ClearCacheMigrationImpl(); - -class _ClearCacheMigrationImpl extends Migration { - @override - Future perform(MigrationPhrase phrase) async { - await HiveInit.clearCache(); - } -} diff --git a/lib/migration/foundation.dart b/lib/migration/foundation.dart deleted file mode 100644 index 15106c777..000000000 --- a/lib/migration/foundation.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:version/version.dart'; - -enum MigrationPhrase { - beforeHive, - afterHive, -} - -/// Migration happens after Hive is initialized, but before all other initializations. -/// If the interval is long enough, each migration between two versions will be performed in sequence. -abstract class Migration { - /// Perform the migration for a specific version. - Future perform(MigrationPhrase phrase); - - Migration operator +(Migration then) => ChainedMigration([this, then]); -} - -class ChainedMigration extends Migration { - final List migrations; - - ChainedMigration(this.migrations); - - @override - Future perform(MigrationPhrase phrase) async { - for (final migration in migrations) { - await migration.perform(phrase); - } - } -} - -class _MigrationEntry implements Comparable<_MigrationEntry> { - final Version version; - final Migration migration; - - _MigrationEntry(this.version, this.migration); - - @override - int compareTo(_MigrationEntry other) { - throw version.compareTo(other.version); - } -} - -class MigrationManager { - final List<_MigrationEntry> _migrations = []; - - /// Add a migration when - void addWhen(Version version, {required Migration perform}) { - _migrations.add(_MigrationEntry(version, perform)); - } - - /// [from] is exclusive. - /// [to] is inclusive. - List collectBetween(Version from, Version to) { - _migrations.sort(); - int start = _migrations.indexWhere((m) => m.version == from); - if (start > 0 && start <= _migrations.length) { - return _migrations.sublist(start).map((e) => e.migration).toList(); - } else { - return []; - } - } -} diff --git a/lib/migration/migrations.dart b/lib/migration/migrations.dart deleted file mode 100644 index 40915c901..000000000 --- a/lib/migration/migrations.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'package:version/version.dart'; - -import 'all/cache.dart'; -import 'foundation.dart'; - -class Migrations { - static final _manager = MigrationManager(); - static Migration? _onNullVersion; - - static void init() { - Version(1, 0, 0) << ClearCacheMigration; - } - - static MigrationMatch match({ - required Version? from, - required Version? to, - }) { - final result = []; - if (from == null) { - final onNullVersion = _onNullVersion; - if (onNullVersion != null) { - result.add(onNullVersion); - } - } else if (to != null) { - final all = _manager.collectBetween(from, to); - result.addAll(all); - } - return MigrationMatch(result); - } -} - -class MigrationMatch { - final List _migrations; - - const MigrationMatch(this._migrations); - - Future perform(MigrationPhrase phrase) async { - for (final migration in _migrations) { - await migration.perform(phrase); - } - } -} - -extension _MigrationEx on Version { - void operator <<(Migration migration) { - Migrations._manager.addWhen(this, perform: migration); - } -} diff --git a/lib/network/checker.dart b/lib/network/checker.dart deleted file mode 100644 index 5081ce8fc..000000000 --- a/lib/network/checker.dart +++ /dev/null @@ -1,266 +0,0 @@ -import 'dart:async'; - -import 'package:check_vpn_connection/check_vpn_connection.dart'; -import 'package:connectivity_plus/connectivity_plus.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; -import 'package:sit/design/adaptive/foundation.dart'; -import 'package:sit/design/animation/animated.dart'; -import 'package:sit/init.dart'; -import 'package:sit/settings/settings.dart'; -import 'package:sit/utils/error.dart'; -import 'package:sit/utils/timer.dart'; -import 'package:rettulf/rettulf.dart'; - -enum ConnectivityStatus { - none, - connecting, - connected, - disconnected; -} - -class ConnectivityChecker extends StatefulWidget { - final double iconSize; - final String? initialDesc; - final VoidCallback onConnected; - final Duration? autoStartDelay; - - /// Whether it's connected will be turned. - /// Throw any error if connection fails. - final Future Function() check; - - const ConnectivityChecker({ - super.key, - this.iconSize = 120, - this.initialDesc, - required this.onConnected, - required this.check, - this.autoStartDelay, - }); - - @override - State createState() => _ConnectivityCheckerState(); -} - -const _type2Icon = { - ConnectivityResult.bluetooth: Icons.bluetooth, - ConnectivityResult.wifi: Icons.wifi, - ConnectivityResult.ethernet: Icons.lan, - ConnectivityResult.mobile: Icons.signal_cellular_alt, - ConnectivityResult.none: Icons.signal_wifi_statusbar_null_outlined, - ConnectivityResult.vpn: Icons.vpn_key, -}; - -IconData getConnectionTypeIcon(ConnectivityResult? type, {IconData? fallback}) { - return _type2Icon[type] ?? fallback ?? Icons.wifi_find_outlined; -} - -class _ConnectivityCheckerState extends State { - ConnectivityStatus status = ConnectivityStatus.none; - ConnectivityResult? connectionType; - late Timer networkChecker; - - @override - void initState() { - super.initState(); - networkChecker = runPeriodically(const Duration(milliseconds: 1000), (Timer t) async { - var type = await Connectivity().checkConnectivity(); - if (type == ConnectivityResult.wifi || type == ConnectivityResult.ethernet) { - if (Settings.proxy.anyEnabled || await CheckVpnConnection.isVpnActive()) { - type = ConnectivityResult.vpn; - } - } - if (connectionType != type) { - if (!mounted) return; - setState(() { - connectionType = type; - }); - } - }); - final autoStartDelay = widget.autoStartDelay; - if (autoStartDelay != null) { - Future.delayed(autoStartDelay).then((value) { - startCheck(); - }); - } - } - - @override - Widget build(BuildContext context) { - return [ - AnimatedSize( - duration: const Duration(milliseconds: 300), - child: buildIndicatorArea(context).animatedSwitched(), - ), - AnimatedSize( - duration: const Duration(milliseconds: 300), - child: buildStatus(context).animatedSwitched(), - ), - buildButton(context), - ].column(maa: MainAxisAlignment.spaceAround, caa: CrossAxisAlignment.center).center().padAll(20); - } - - Future startCheck() async { - if (!mounted) return; - setState(() { - networkChecker.cancel(); - status = ConnectivityStatus.connecting; - }); - try { - final connected = await widget.check(); - if (!mounted) return; - setState(() { - if (connected) { - status = ConnectivityStatus.connected; - } else { - status = ConnectivityStatus.disconnected; - } - }); - } catch (error, stackTrace) { - debugPrintError(error, stackTrace); - if (!mounted) return; - setState(() { - status = ConnectivityStatus.disconnected; - }); - } - } - - Widget buildStatus(BuildContext ctx) { - final tip = switch (status) { - ConnectivityStatus.none => widget.initialDesc ?? _i18n.status.none, - ConnectivityStatus.connecting => _i18n.status.connecting, - ConnectivityStatus.connected => _i18n.status.connected, - ConnectivityStatus.disconnected => _i18n.status.disconnected, - }; - return tip.text(key: ValueKey(status), style: ctx.textTheme.titleLarge, textAlign: TextAlign.center); - } - - Widget buildButton(BuildContext ctx) { - final (tip, onTap) = switch (status) { - ConnectivityStatus.none => (_i18n.button.none, startCheck), - ConnectivityStatus.connecting => (_i18n.button.connecting, null), - ConnectivityStatus.connected => (_i18n.button.connected, widget.onConnected), - ConnectivityStatus.disconnected => (_i18n.button.disconnected, startCheck), - }; - return PlatformTextButton( - onPressed: onTap, - child: tip.text( - key: ValueKey(status), - style: TextStyle(fontSize: context.textTheme.titleMedium?.fontSize), - ), - ); - } - - Widget buildIndicatorArea(BuildContext ctx) { - switch (status) { - case ConnectivityStatus.none: - return buildIcon(ctx, getConnectionTypeIcon(connectionType)); - case ConnectivityStatus.connecting: - return const CircularProgressIndicator( - key: ValueKey("Waiting"), - strokeWidth: 14, - ).sizedAll(widget.iconSize); - case ConnectivityStatus.connected: - return buildIcon(ctx, Icons.check_rounded); - case ConnectivityStatus.disconnected: - return buildIcon(ctx, Icons.public_off_rounded); - } - } - - Widget buildIcon(BuildContext ctx, IconData icon, [Key? key]) { - key ??= ValueKey(icon); - return Icon( - icon, - size: widget.iconSize, - color: ctx.colorScheme.primary, - ).sizedAll( - key: key, - widget.iconSize, - ); - } - - @override - void dispose() { - networkChecker.cancel(); - super.dispose(); - } -} - -const _i18n = _NetworkCheckerI18n(); - -class _NetworkCheckerI18n { - const _NetworkCheckerI18n(); - - static const ns = "networkChecker"; - - final button = const _NetworkCheckerI18nEntry("button"); - final status = const _NetworkCheckerI18nEntry("status"); - - String get testConnection => "$ns.testConnection.title".tr(); - - String get testConnectionDesc => "$ns.testConnection.desc".tr(); -} - -class _NetworkCheckerI18nEntry { - final String scheme; - - String get _ns => "${_NetworkCheckerI18n.ns}.$scheme"; - - const _NetworkCheckerI18nEntry(this.scheme); - - String get connected => "$_ns.connected".tr(); - - String get connecting => "$_ns.connecting".tr(); - - String get disconnected => "$_ns.disconnected".tr(); - - String get none => "$_ns.none".tr(); -} - -class TestConnectionTile extends StatefulWidget { - const TestConnectionTile({super.key}); - - @override - State createState() => _TestConnectionTileState(); -} - -class _TestConnectionTileState extends State { - var testState = ConnectivityStatus.none; - - @override - Widget build(BuildContext context) { - return ListTile( - enabled: testState != ConnectivityStatus.connecting, - leading: const Icon(Icons.network_check), - title: _i18n.testConnection.text(), - subtitle: _i18n.testConnectionDesc.text(), - trailing: Padding( - padding: const EdgeInsets.all(8), - child: switch (testState) { - ConnectivityStatus.connecting => const CircularProgressIndicator.adaptive(), - ConnectivityStatus.connected => const Icon(Icons.check, color: Colors.green), - ConnectivityStatus.disconnected => Icon(Icons.public_off_rounded, color: context.$red$), - _ => null, - }), - onTap: () async { - setState(() { - testState = ConnectivityStatus.connecting; - }); - final bool connected; - try { - connected = await Init.ssoSession.checkConnectivity(); - if (!mounted) return; - setState(() { - testState = connected ? ConnectivityStatus.connected : ConnectivityStatus.disconnected; - }); - } catch (error) { - if (!mounted) return; - setState(() { - testState = ConnectivityStatus.disconnected; - }); - } - }, - ); - } -} diff --git a/lib/network/dio.dart b/lib/network/dio.dart deleted file mode 100644 index f9ff1d59e..000000000 --- a/lib/network/dio.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'dart:math'; - -import 'package:cookie_jar/cookie_jar.dart'; -import 'package:dio/dio.dart'; -import 'package:dio_cookie_manager/dio_cookie_manager.dart'; -import 'package:fk_user_agent/fk_user_agent.dart'; -import 'package:flutter/foundation.dart'; -import 'package:sit/r.dart'; - -final _rand = Random(); - -class DioInit { - static Future init({ - required CookieJar cookieJar, - BaseOptions? config, - }) async { - final dio = Dio(); - if (!kIsWeb) { - dio.interceptors.add(CookieManager(cookieJar)); - } - if (config != null) { - dio.options = config; - } - await initUserAgentString(dio: dio); - - return dio; - } - - static String getRandomUa() { - return R.userAgentList[_rand.nextInt(R.userAgentList.length)].trim(); - } - - static Future initUserAgentString({ - required Dio dio, - }) async { - try { - // 如果非IOS/Android,则该函数将抛异常 - await FkUserAgent.init(); - // 更新 dio 设置的 user-agent 字符串 - dio.options.headers['User-Agent'] = FkUserAgent.webViewUserAgent ?? getRandomUa(); - } catch (e) { - // Desktop端将进入该异常 - dio.options.headers['User-Agent'] = getRandomUa(); - } - } -} diff --git a/lib/network/i18n.dart b/lib/network/i18n.dart deleted file mode 100644 index c148058ef..000000000 --- a/lib/network/i18n.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:sit/credentials/i18n.dart'; -import 'package:sit/l10n/common.dart'; - -const i18n = _I18n(); - -class _I18n with CommonI18nMixin { - const _I18n(); - - static const ns = "networkTool"; - final easyconnect = const _Easyconnect(); - final network = const NetworkI18n(); - final credential = const CredentialsI18n(); - - String get title => "$ns.title".tr(); - - String get subtitle => "$ns.subtitle".tr(); - - String get openWlanSettingsBtn => "$ns.openWlanSettingsBtn".tr(); - - String get noAccessTip => "$ns.noAccessTip".tr(); - - String get connectionFailedError => "$ns.connectionFailedError".tr(); - - String get connectionFailedButCampusNetworkConnected => "$ns.connectionFailedButCampusNetworkConnected".tr(); - - String get connectedByProxy => "$ns.connectedByProxy".tr(); - - String get connectedByVpn => "$ns.connectedByVpn".tr(); - - String get connectedByWlan => "$ns.connectedByWlan".tr(); - - String get connectedByEthernet => "$ns.connectedByEthernet".tr(); -} - -class _Easyconnect { - const _Easyconnect(); - - static const ns = "easyconnect"; - - String get launchBtn => "$ns.launchBtn".tr(); - - String get launchFailed => "$ns.launchFailed".tr(); - - String get launchFailedDesc => "$ns.launchFailedDesc".tr(); -} diff --git a/lib/network/page/connected.dart b/lib/network/page/connected.dart deleted file mode 100644 index 09d316020..000000000 --- a/lib/network/page/connected.dart +++ /dev/null @@ -1,92 +0,0 @@ -import 'dart:async'; - -import 'package:check_vpn_connection/check_vpn_connection.dart'; -import 'package:connectivity_plus/connectivity_plus.dart'; -import 'package:flutter/material.dart'; -import 'package:sit/network/checker.dart'; -import 'package:sit/settings/settings.dart'; -import 'package:sit/utils/timer.dart'; -import 'package:rettulf/rettulf.dart'; - -import '../service/network.dart'; -import '../widgets/status.dart'; -import '../i18n.dart'; - -class ConnectedInfo extends StatefulWidget { - const ConnectedInfo({super.key}); - - @override - State createState() => _ConnectedInfoState(); -} - -class _ConnectedInfoState extends State { - ConnectivityResult? connectionType; - late Timer connectionTypeChecker; - late Timer statusChecker; - CampusNetworkStatus? status; - - @override - void initState() { - super.initState(); - connectionTypeChecker = runPeriodically(const Duration(milliseconds: 500), (Timer t) async { - var type = await Connectivity().checkConnectivity(); - if (type == ConnectivityResult.wifi || type == ConnectivityResult.ethernet) { - if (await CheckVpnConnection.isVpnActive()) { - type = ConnectivityResult.vpn; - } - } - if (connectionType != type) { - if (!mounted) return; - setState(() { - connectionType = type; - }); - } - }); - statusChecker = runPeriodically(const Duration(milliseconds: 1000), (Timer t) async { - final status = await Network.checkCampusNetworkStatus(); - if (this.status != status) { - if (!mounted) return; - setState(() { - this.status = status; - }); - } - }); - } - - @override - void dispose() { - connectionTypeChecker.cancel(); - statusChecker.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final useProxy = Settings.proxy.anyEnabled; - return AnimatedSwitcher( - duration: const Duration(milliseconds: 500), - child: [ - Icon( - useProxy ? Icons.vpn_key : getConnectionTypeIcon(connectionType), - size: 120, - ).expanded(flex: 5), - buildTip().expanded(flex: 3), - ].column(caa: CrossAxisAlignment.stretch, key: ValueKey(connectionType)), - ).padAll(10); - } - - Widget buildTip() { - final style = context.textTheme.bodyLarge; - final tip = switch (connectionType) { - ConnectivityResult.wifi => i18n.connectedByWlan, - ConnectivityResult.ethernet => i18n.connectedByEthernet, - ConnectivityResult.vpn => i18n.connectedByVpn, - _ => null, - }; - if (tip == null) return const SizedBox(height: 10); - return [ - tip.text(textAlign: TextAlign.center, style: style), - CampusNetworkStatusInfo(status: status), - ].column().padH(20); - } -} diff --git a/lib/network/page/disconnected.dart b/lib/network/page/disconnected.dart deleted file mode 100644 index b1a817ca2..000000000 --- a/lib/network/page/disconnected.dart +++ /dev/null @@ -1,79 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:sit/utils/timer.dart'; -import 'package:rettulf/rettulf.dart'; - -import '../service/network.dart'; -import '../widgets/quick_button.dart'; -import '../widgets/status.dart'; -import '../i18n.dart'; - -class DisconnectedInfo extends StatefulWidget { - const DisconnectedInfo({super.key}); - - @override - State createState() => _DisconnectedInfoState(); -} - -class _DisconnectedInfoState extends State { - CampusNetworkStatus? status; - late Timer statusChecker; - - @override - void initState() { - super.initState(); - statusChecker = runPeriodically(const Duration(milliseconds: 1000), (Timer t) async { - final status = await Network.checkCampusNetworkStatus(); - if (this.status != status) { - if (!mounted) return; - setState(() { - this.status = status; - }); - } - }); - } - - @override - void dispose() { - statusChecker.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return context.isPortrait ? buildPortrait() : buildLandscape(); - } - - Widget buildPortrait() { - return [ - const Icon(Icons.public_off_outlined, size: 120).expanded(), - [ - buildTip(context), - if (status != null) CampusNetworkStatusInfo(status: status), - ].column().expanded(), - const QuickButtons(), - ].column(caa: CrossAxisAlignment.stretch).padAll(10); - } - - Widget buildLandscape() { - return [ - [ - const Icon(Icons.public_off_outlined, size: 120), - const QuickButtons(), - ].column(maa: MainAxisAlignment.spaceEvenly).expanded(), - [ - buildTip(context), - if (status != null) CampusNetworkStatusInfo(status: status), - ].column().padAll(10).scrolled().expanded(), - ].row(); - } - - Widget buildTip(BuildContext context) { - return Text( - status != null ? i18n.connectionFailedButCampusNetworkConnected : i18n.connectionFailedError, - textAlign: TextAlign.start, - style: context.textTheme.bodyLarge, - ).padH(20); - } -} diff --git a/lib/network/page/index.dart b/lib/network/page/index.dart deleted file mode 100644 index cbf979fa5..000000000 --- a/lib/network/page/index.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:sit/init.dart'; -import 'package:sit/utils/timer.dart'; -import 'package:rettulf/rettulf.dart'; -import 'connected.dart'; -import 'disconnected.dart'; - -import '../i18n.dart'; - -class NetworkToolPage extends StatefulWidget { - const NetworkToolPage({super.key}); - - @override - State createState() => _NetworkToolPageState(); -} - -class _NetworkToolPageState extends State { - bool? isConnected; - late Timer connectivityChecker; - - @override - void initState() { - super.initState(); - // FIXME: Bad practice to use periodically, because the next request will not await the former one. - connectivityChecker = runPeriodically(const Duration(milliseconds: 3000), (Timer t) async { - bool connected; - try { - connected = await Init.ssoSession.checkConnectivity(); - } catch (err) { - connected = false; - } - if (!mounted) return; - if (isConnected != connected) { - setState(() => isConnected = connected); - } - // if (connected) { - // // if connected, check the connection slowly - // await Future.delayed(const Duration(seconds: 3)); - // } else { - // // if not connected, check the connection frequently - // await Future.delayed(const Duration(seconds: 1)); - // } - }); - } - - @override - void dispose() { - connectivityChecker.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: i18n.title.text(), - bottom: const PreferredSize( - preferredSize: Size.fromHeight(4), - child: LinearProgressIndicator(), - ), - ), - body: AnimatedSwitcher( - duration: const Duration(milliseconds: 500), - child: switch (isConnected) { - true => const ConnectedInfo(key: ValueKey("Connected")), - false => const DisconnectedInfo(key: ValueKey("Disconnected")), - null => const SizedBox(key: ValueKey("null")), - }, - ), - ); - } -} diff --git a/lib/network/proxy.dart b/lib/network/proxy.dart deleted file mode 100644 index 41226a96e..000000000 --- a/lib/network/proxy.dart +++ /dev/null @@ -1,105 +0,0 @@ -import 'dart:io'; - -import 'package:flutter/foundation.dart'; -import 'package:sit/settings/settings.dart'; - -class SitHttpOverrides extends HttpOverrides { - SitHttpOverrides(); - - @override - HttpClient createHttpClient(SecurityContext? context) { - final client = super.createHttpClient(context); - client.badCertificateCallback = (cert, host, port) => true; - client.findProxy = (url) { - final host = url.host; - final isSchoolLanRequired = _isSchoolLanRequired(host); - final profiles = _buildProxy(isSchoolLanRequired); - if (profiles.http == null && profiles.https == null && profiles.all == null) { - return 'DIRECT'; - } else { - final env = _toEnvMap(profiles); - if (kDebugMode) { - print("Access $url ${env.isEmpty ? "bypass proxy" : "by proxy $env"}"); - } - // TODO: Socks proxy doesn't work with env - return HttpClient.findProxyFromEnvironment( - url, - environment: env, - ); - } - }; - return client; - } -} - -Map _toEnvMap(({String? http, String? https, String? all}) profiles) { - final (:http, :https, :all) = profiles; - return { - if (http != null) "http_proxy": http, - if (https != null) "https_proxy": https, - if (all != null) "all_proxy": all, - }; -} - -({String? http, String? https, String? all}) _buildProxy(bool isSchoolLanRequired) { - return ( - http: _buildProxyForType(ProxyType.http, isSchoolLanRequired), - https: _buildProxyForType(ProxyType.https, isSchoolLanRequired), - all: _buildProxyForType(ProxyType.all, isSchoolLanRequired), - ); -} - -String? _buildProxyForType(ProxyType type, bool isSchoolLanRequired) { - final profile = Settings.proxy.resolve(type); - final address = profile.address; - if (address == null) return null; - if (!profile.enabled) return null; - if (profile.proxyMode == ProxyMode.global && !isSchoolLanRequired) return null; - return address; -} - -bool _isSchoolNetwork(String host) { - if (host == 'jwxt.sit.edu.cn') { - // 教务系统 - return true; - } else if (host == 'sc.sit.edu.cn') { - // Second class - return true; - } else if (host == 'card.sit.edu.cn') { - // 校园卡 - return true; - } else if (host == 'myportal.sit.edu.cn') { - // OA - return true; - } else if (host == '210.35.66.106') { - // Library - return true; - } else if (host == '210.35.98.178') { - // TODO: what's this - // 门 - return true; - } - return false; -} - -bool _isSchoolLanRequired(String host) { - if (host == 'jwxt.sit.edu.cn') { - // 教务系统 - return true; - } else if (host == 'sc.sit.edu.cn') { - // Second class - return true; - } else if (host == 'card.sit.edu.cn') { - // 校园卡 - return true; - } - if (host == '210.35.66.106') { - // Library - return true; - } else if (host == '210.35.98.178') { - // TODO: what's this - // 门 - return true; - } - return false; -} diff --git a/lib/network/service/network.dart b/lib/network/service/network.dart deleted file mode 100644 index e60fa4d7b..000000000 --- a/lib/network/service/network.dart +++ /dev/null @@ -1,127 +0,0 @@ -import 'dart:convert'; - -import 'package:dio/dio.dart'; -import 'package:json_annotation/json_annotation.dart'; - -part 'network.g.dart'; - -bool _toBool(int num) => num != 0; - -@JsonSerializable(createToJson: false) -class CampusNetworkStatus { - // 1:已登录 - // 0:未登录 - @JsonKey(name: "result", fromJson: _toBool) - final bool loggedIn; - - // 当前的校园网ip - @JsonKey(name: 'v46ip') - final String ip; - - // 当前登录学号 - @JsonKey(name: "uid") - String? studentId; - - CampusNetworkStatus(this.loggedIn, this.ip, {this.studentId}); - - factory CampusNetworkStatus.fromJson(Map json) => _$CampusNetworkStatusFromJson(json); -} - -@JsonSerializable(createToJson: false) -class LogoutResult { - final int result; - - LogoutResult(this.result); - - factory LogoutResult.fromJson(Map json) => _$LogoutResultFromJson(json); -} - -@JsonSerializable() -class LoginResult { - final int result; - - const LoginResult(this.result); - - factory LoginResult.fromJson(Map json) => _$LoginResultFromJson(json); - - Map toJson() => _$LoginResultToJson(this); -} - -class Network { - static const _indexUrl = 'http://172.16.8.70'; - static const _drcomUrl = '$_indexUrl/drcom'; - static const _loginUrl = '$_drcomUrl/login'; - static const _checkStatusUrl = '$_drcomUrl/chkstatus'; - static const _logoutUrl = '$_drcomUrl/logout'; - - static final dio = Dio() - ..options = BaseOptions( - connectTimeout: const Duration(milliseconds: 3000), - sendTimeout: const Duration(milliseconds: 3000), - receiveTimeout: const Duration(milliseconds: 3000), - ); - - static Future> _get(String url, {Map? queryParameters}) async { - var response = await dio.get( - url, - queryParameters: queryParameters, - options: Options( - responseType: ResponseType.plain, - ), - ); - var jsonp = response.data.toString().trim(); - return jsonDecode(jsonp.substring(7, jsonp.length - 1)); - } - - static Future login(String username, String password) async { - return LoginResult.fromJson(await _get( - _loginUrl, - queryParameters: { - 'callback': 'dr1003', - 'DDDDD': username, - 'upass': password, - '0MKKey': '123456', - "R1'": '0', - 'R2': '', - 'R3': '0', - 'R6': '0', - 'para': '00', - 'terminal_type': '1', - 'lang': 'zh-cn', - 'jsVersion': '4.1', - }, - )); - } - - static Future checkCampusNetworkStatus() async { - try { - final payload = await _get( - _checkStatusUrl, - queryParameters: { - 'callback': 'dr1002', - 'lang': 'zh', - 'jsVersion': '4.X', - }, - ); - return CampusNetworkStatus.fromJson(payload); - } catch (error) { - return null; - } - } - - static Future logout() async { - try { - final payload = await _get( - _logoutUrl, - queryParameters: { - 'callback': 'dr1002', - 'jsVersion': '4.1.3', - 'lang': 'zh', - }, - ); - return LogoutResult.fromJson(payload); - } catch (error) { - return null; - } - } -} diff --git a/lib/network/service/network.g.dart b/lib/network/service/network.g.dart deleted file mode 100644 index 5b52a1c35..000000000 --- a/lib/network/service/network.g.dart +++ /dev/null @@ -1,25 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'network.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -CampusNetworkStatus _$CampusNetworkStatusFromJson(Map json) => CampusNetworkStatus( - _toBool(json['result'] as int), - json['v46ip'] as String, - studentId: json['uid'] as String?, - ); - -LogoutResult _$LogoutResultFromJson(Map json) => LogoutResult( - json['result'] as int, - ); - -LoginResult _$LoginResultFromJson(Map json) => LoginResult( - json['result'] as int, - ); - -Map _$LoginResultToJson(LoginResult instance) => { - 'result': instance.result, - }; diff --git a/lib/network/widgets/entry.dart b/lib/network/widgets/entry.dart deleted file mode 100644 index 7590e0ebc..000000000 --- a/lib/network/widgets/entry.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:sit/design/widgets/navigation.dart'; -import '../i18n.dart'; - -class NetworkToolEntryTile extends StatelessWidget { - const NetworkToolEntryTile({super.key}); - - @override - Widget build(BuildContext context) { - return PageNavigationTile( - title: i18n.title.text(), - subtitle: i18n.subtitle.text(), - leading: const Icon(Icons.network_check), - path: "/tools/network-tool", - ); - } -} diff --git a/lib/network/widgets/quick_button.dart b/lib/network/widgets/quick_button.dart deleted file mode 100644 index 90f871915..000000000 --- a/lib/network/widgets/quick_button.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:app_settings/app_settings.dart'; -import 'package:flutter/material.dart'; -import 'package:sit/design/adaptive/dialog.dart'; -import 'package:sit/utils/guard_launch.dart'; -import 'package:rettulf/rettulf.dart'; - -import '../i18n.dart'; - -const easyConnectDownloadUrl = "https://vpn1.sit.edu.cn/com/installClient.html"; - -class QuickButtons extends StatefulWidget { - const QuickButtons({super.key}); - - @override - State createState() => _QuickButtonsState(); -} - -class _QuickButtonsState extends State { - @override - Widget build(BuildContext context) { - return [ - FilledButton( - child: i18n.easyconnect.launchBtn.text(), - onPressed: () async { - final launched = await guardLaunchUrlString(context, 'sangfor://easyconnect'); - if (!launched) { - if (!mounted) return; - final confirm = await context.showRequest( - title: i18n.easyconnect.launchFailed, - desc: i18n.easyconnect.launchFailedDesc, - yes: i18n.download, - no: i18n.notNow, - highlight: true); - if (confirm == true) { - if (!mounted) return; - await guardLaunchUrlString(context, easyConnectDownloadUrl); - } - } - }, - ), - OutlinedButton( - onPressed: () { - AppSettings.openAppSettings(type: AppSettingsType.wifi); - }, - child: i18n.openWlanSettingsBtn.text(), - ), - ].row( - maa: MainAxisAlignment.spaceEvenly, - ); - } -} diff --git a/lib/network/widgets/status.dart b/lib/network/widgets/status.dart deleted file mode 100644 index 33bdac1f6..000000000 --- a/lib/network/widgets/status.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:rettulf/rettulf.dart'; -import '../i18n.dart'; -import '../service/network.dart'; - -class CampusNetworkStatusInfo extends StatelessWidget { - final CampusNetworkStatus? status; - - const CampusNetworkStatusInfo({super.key, required this.status}); - - @override - Widget build(BuildContext context) { - final style = context.textTheme.bodyLarge; - final status = this.status; - var ip = i18n.unknown; - var studentId = i18n.unknown; - if (status != null) { - ip = status.ip; - studentId = status.studentId ?? i18n.unknown; - } - return [ - "${i18n.credential.studentId}: $studentId".text(textAlign: TextAlign.center, style: style), - "${i18n.network.ipAddress}: $ip".text(textAlign: TextAlign.center, style: style), - ].column(); - } -} diff --git a/lib/platform/desktop.dart b/lib/platform/desktop.dart deleted file mode 100644 index d34a6e48b..000000000 --- a/lib/platform/desktop.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:sit/r.dart'; -import 'package:sit/storage/prefs.dart'; -import 'package:universal_platform/universal_platform.dart'; -import 'package:window_manager/window_manager.dart'; - -class DesktopWindowListener extends WindowListener { - @override - void onWindowResized() { - super.onWindowResized(); - saveWindowSize(); - } - - Future saveWindowSize() async { - final prefs = await SharedPreferences.getInstance(); - final curSize = await windowManager.getSize(); - await prefs.setLastWindowSize(curSize); - debugPrint("Saved last window size $curSize"); - } -} - -class DesktopInit { - static Future init({ - Size? size, - }) async { - if (!UniversalPlatform.isDesktop) return; - windowManager.addListener(DesktopWindowListener()); - await windowManager.ensureInitialized(); - final options = WindowOptions( - title: R.appName, - size: size ?? R.defaultWindowSize, - center: true, - minimumSize: R.minWindowSize, - ); - windowManager.waitUntilReadyToShow(options).then((_) async { - await windowManager.show(); - await windowManager.focus(); - }); - } - - static Future resizeTo(Size newSize, {bool center = true}) async { - await windowManager.setSize(newSize); - if (center) { - await windowManager.center(); - } - } - - static setTitle(String title) async { - if (UniversalPlatform.isDesktop) { - await windowManager.setTitle(title); - } - } -} diff --git a/lib/platform/windows/win32.dart b/lib/platform/windows/win32.dart deleted file mode 100644 index d540b37c5..000000000 --- a/lib/platform/windows/win32.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'dart:io'; - -import 'package:universal_platform/universal_platform.dart'; -import 'package:win32_registry/win32_registry.dart'; - -Future registerCustomSchemeWin32(String scheme) async { - if (!UniversalPlatform.isWindows) return; - final appPath = Platform.resolvedExecutable; - - final protocolRegKey = 'Software\\Classes\\$scheme'; - const protocolRegValue = RegistryValue( - 'URL Protocol', - RegistryValueType.string, - '', - ); - const protocolCmdRegKey = 'shell\\open\\command'; - final protocolCmdRegValue = RegistryValue( - '', - RegistryValueType.string, - '"$appPath" "%1"', - ); - - final regKey = Registry.currentUser.createKey(protocolRegKey); - regKey.createValue(protocolRegValue); - regKey.createKey(protocolCmdRegKey).createValue(protocolCmdRegValue); -} diff --git a/lib/platform/windows/win32_web_mock.dart b/lib/platform/windows/win32_web_mock.dart deleted file mode 100644 index 8c283b0fa..000000000 --- a/lib/platform/windows/win32_web_mock.dart +++ /dev/null @@ -1 +0,0 @@ -Future registerCustomSchemeWin32(String scheme) async {} diff --git a/lib/platform/windows/windows.dart b/lib/platform/windows/windows.dart deleted file mode 100644 index f8ead94d4..000000000 --- a/lib/platform/windows/windows.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'win32_web_mock.dart' if (dart.library.io) 'win32.dart'; - -class WindowsInit { - static Future registerCustomScheme(String scheme) async { - await registerCustomSchemeWin32(scheme); - } -} diff --git a/lib/qrcode/handle.dart b/lib/qrcode/handle.dart deleted file mode 100644 index 15fbf61d1..000000000 --- a/lib/qrcode/handle.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:sit/qrcode/protocol.dart'; -import 'package:sit/r.dart'; - -enum QrCodeHandleResult { - success, - unhandled, - unrecognized, - invalidFormat; -} - -Future onHandleQrCodeData({ - required BuildContext context, - required String data, -}) async { - final qrCodeData = Uri.tryParse(data); - if (qrCodeData == null) return QrCodeHandleResult.invalidFormat; - // backwards supports. - if (qrCodeData.scheme != R.scheme && qrCodeData.scheme != "sitlife") return QrCodeHandleResult.unrecognized; - for (final handler in DeepLinkHandlerProtocol.all) { - if (handler.match(qrCodeData)) { - await handler.onHandle(context: context, qrCodeData: qrCodeData); - return QrCodeHandleResult.success; - } - } - return QrCodeHandleResult.unhandled; -} diff --git a/lib/qrcode/i18n.dart b/lib/qrcode/i18n.dart deleted file mode 100644 index 319947b77..000000000 --- a/lib/qrcode/i18n.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; - -const i18n = _I18n(); - -class _I18n { - const _I18n(); - - static const ns = "scanner"; - - String get barcodeNotRecognized => "$ns.barcodeNotRecognized".tr(); -} diff --git a/lib/qrcode/page/scanner.dart b/lib/qrcode/page/scanner.dart deleted file mode 100644 index 5fcbbc7a6..000000000 --- a/lib/qrcode/page/scanner.dart +++ /dev/null @@ -1,139 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:go_router/go_router.dart'; -import 'package:image_picker/image_picker.dart'; -import 'package:sit/design/adaptive/dialog.dart'; -import 'package:mobile_scanner/mobile_scanner.dart'; -import 'package:rettulf/rettulf.dart'; - -import '../i18n.dart'; -import '../widgets/overlay.dart'; - -class ScannerPage extends StatefulWidget { - const ScannerPage({super.key}); - - @override - State createState() => _ScannerPageState(); -} - -const _iconSize = 32.0; - -class _ScannerPageState extends State with SingleTickerProviderStateMixin { - final controller = MobileScannerController( - torchEnabled: false, - formats: [BarcodeFormat.qrCode], - facing: CameraFacing.back, - ); - - @override - void initState() { - super.initState(); - SystemChrome.setPreferredOrientations([ - DeviceOrientation.portraitUp, - DeviceOrientation.portraitDown, - ]); - } - - @override - void dispose() { - controller.dispose(); - SystemChrome.setPreferredOrientations([ - DeviceOrientation.landscapeRight, - DeviceOrientation.landscapeLeft, - DeviceOrientation.portraitUp, - DeviceOrientation.portraitDown, - ]); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(), - body: [ - buildScanner(), - const QRScannerOverlay( - overlayColour: Colors.black26, - ), - ].stack(), - bottomNavigationBar: buildControllerView(), - ); - } - - Widget buildScanner() { - return MobileScanner( - controller: controller, - fit: BoxFit.contain, - onDetect: (captured) async { - final qrcode = captured.barcodes.firstOrNull; - if (qrcode != null) { - context.pop(qrcode.rawValue); - // dispose the controller to stop scanning - controller.dispose(); - await HapticFeedback.heavyImpact(); - } - }, - ); - } - - Widget buildControllerView() { - return [ - buildTorchButton(), - buildSwitchButton(), - buildImagePicker(), - ].row( - caa: CrossAxisAlignment.center, - maa: MainAxisAlignment.spaceEvenly, - ); - } - - Widget buildImagePicker() { - return IconButton( - icon: const Icon(Icons.image), - iconSize: _iconSize, - onPressed: () async { - final ImagePicker picker = ImagePicker(); - // Pick an image - final XFile? image = await picker.pickImage(source: ImageSource.gallery); - if (image != null) { - if (!await controller.analyzeImage(image.path)) { - if (!mounted) return; - context.showSnackBar(content: i18n.barcodeNotRecognized.text()); - } - } - }, - ); - } - - Widget buildSwitchButton() { - return IconButton( - iconSize: _iconSize, - icon: controller.cameraFacingState >> - (context, state) { - switch (state) { - case CameraFacing.front: - return const Icon(Icons.camera_front); - case CameraFacing.back: - return const Icon(Icons.camera_rear); - } - }, - onPressed: () => controller.switchCamera(), - ); - } - - Widget buildTorchButton() { - return IconButton( - iconSize: _iconSize, - icon: controller.torchState >> - (context, state) { - switch (state) { - case TorchState.off: - return const Icon(Icons.flash_off, color: Colors.grey); - case TorchState.on: - return const Icon(Icons.flash_on, color: Colors.yellow); - } - }, - onPressed: () => controller.toggleTorch(), - ); - } -} diff --git a/lib/qrcode/page/view.dart b/lib/qrcode/page/view.dart deleted file mode 100644 index 8fd27d5a2..000000000 --- a/lib/qrcode/page/view.dart +++ /dev/null @@ -1,120 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/material.dart'; -import 'package:flutter_svg_provider/flutter_svg_provider.dart'; -import 'package:pretty_qr_code/pretty_qr_code.dart'; -import 'package:qr_flutter/qr_flutter.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:sit/l10n/tr.dart'; - -class _I18n { - const _I18n(); - - static const ns = "qrCode"; - - List get hint => "$ns.hint".trSpan(args: { - "me": const WidgetSpan(child: Icon(Icons.person)), - "scan": const WidgetSpan(child: Icon(Icons.qr_code_scanner)), - }); -} - -const _i18n = _I18n(); - -class QrCodePage extends StatelessWidget { - final String data; - final double? maxSize; - final Widget? title; - - const QrCodePage({ - super.key, - required this.data, - this.maxSize, - this.title, - }); - - @override - Widget build(BuildContext context) { - return Scaffold( - body: CustomScrollView( - slivers: [ - SliverAppBar( - floating: true, - title: title, - ), - SliverToBoxAdapter( - child: LayoutBuilder( - builder: (ctx, box) { - final side = min(box.maxWidth, maxSize ?? 256.0); - return SizedBox( - width: side, - height: side, - child: PlainQrCodeView( - data: data, - ), - ).center(); - }, - ), - ), - SliverToBoxAdapter( - child: RichText( - text: TextSpan( - style: context.textTheme.bodyLarge, - children: _i18n.hint, - ), - ).padAll(10), - ) - ], - ), - ); - } -} - -class PlainQrCodeView extends StatelessWidget { - final String data; - - const PlainQrCodeView({ - super.key, - required this.data, - }); - - @override - Widget build(BuildContext context) { - return QrImageView( - backgroundColor: context.colorScheme.surface, - data: data, - eyeStyle: QrEyeStyle( - eyeShape: QrEyeShape.square, - color: context.colorScheme.onSurface, - ), - dataModuleStyle: QrDataModuleStyle( - dataModuleShape: QrDataModuleShape.square, - color: context.colorScheme.onSurface, - ), - version: QrVersions.auto, - ); - } -} - -class BrandQrCodeView extends StatelessWidget { - final String data; - - const BrandQrCodeView({ - super.key, - required this.data, - }); - - @override - Widget build(BuildContext context) { - return PrettyQrView.data( - data: data, - decoration: PrettyQrDecoration( - shape: PrettyQrSmoothSymbol( - color: context.colorScheme.onSurface, - ), - image: const PrettyQrDecorationImage( - image: Svg("assets/icon.svg"), - ), - ), - ); - } -} diff --git a/lib/qrcode/protocol.dart b/lib/qrcode/protocol.dart deleted file mode 100644 index ca467bd6e..000000000 --- a/lib/qrcode/protocol.dart +++ /dev/null @@ -1,103 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:sit/r.dart'; -import 'package:sit/settings/page/proxy.dart'; -import 'package:sit/timetable/entity/platte.dart'; -import 'package:sit/timetable/page/p13n.dart'; - -/// convert any data to a URI with [R.scheme]. -sealed class DeepLinkHandlerProtocol { - const DeepLinkHandlerProtocol(); - - bool match(Uri encoded); - - Future onHandle({ - required BuildContext context, - required Uri qrCodeData, - }); - - static final List all = [ - const ProxyDeepLink(), - const TimetablePaletteDeepLink(), - ]; -} - -class ProxyDeepLink extends DeepLinkHandlerProtocol { - static const host = "proxy"; - static const path = "/set"; - - const ProxyDeepLink(); - - Uri encode({ - required String? http, - required String? https, - required String? all, - }) => - Uri(scheme: R.scheme, host: host, path: path, queryParameters: { - if (http != null) "http": http, - // shorthand for https if http proxy is identical to https proxy - if (https != null) "https": http == https ? "@http" : https, - if (all != null) "all": all, - }); - - ({Uri? http, Uri? https, Uri? all}) decode(Uri qrCodeData) { - final param = qrCodeData.queryParameters; - final http = param["http"]; - var https = param["https"]; - final all = param["all"]; - // shorthand for https if http proxy is identical to https proxy - if (https == "@http") { - https = http; - } - return ( - http: http == null ? null : Uri.tryParse(http), - https: https == null ? null : Uri.tryParse(https), - all: all == null ? null : Uri.tryParse(all), - ); - } - - @override - bool match(Uri encoded) { - return encoded.host == host && encoded.path == path; - } - - @override - Future onHandle({ - required BuildContext context, - required Uri qrCodeData, - }) async { - final (:http, :https, :all) = decode(qrCodeData); - await onProxyFromQrCode( - context: context, - http: http, - https: https, - all: all, - ); - } -} - -class TimetablePaletteDeepLink implements DeepLinkHandlerProtocol { - static const path = "timetable-palette"; - - const TimetablePaletteDeepLink(); - - Uri encode(TimetablePalette palette) => Uri(scheme: R.scheme, path: path, query: palette.encodeBase64()); - - TimetablePalette decode(Uri qrCodeData) => TimetablePalette.decodeFromBase64(qrCodeData.query); - - @override - bool match(Uri encoded) { - return encoded.path == path; - } - - @override - Future onHandle({ - required BuildContext context, - required Uri qrCodeData, - }) async { - final palette = decode(qrCodeData); - await onTimetablePaletteFromQrCode( - context: context, - palette: palette, - ); - } -} diff --git a/lib/qrcode/widgets/overlay.dart b/lib/qrcode/widgets/overlay.dart deleted file mode 100644 index 24ef488aa..000000000 --- a/lib/qrcode/widgets/overlay.dart +++ /dev/null @@ -1,142 +0,0 @@ -import 'package:flutter/material.dart'; - -// grab from https://gist.github.com/r-yeates/0bad6b8a07e01520a1b3ceba32bad77d - -class QRScannerOverlay extends StatelessWidget { - const QRScannerOverlay({Key? key, required this.overlayColour}) : super(key: key); - - final Color overlayColour; - - @override - Widget build(BuildContext context) { - double scanArea = - (MediaQuery.of(context).size.width < 400 || MediaQuery.of(context).size.height < 400) ? 200.0 : 330.0; - return Stack(children: [ - ColorFiltered( - colorFilter: ColorFilter.mode(overlayColour, BlendMode.srcOut), // This one will create the magic - child: Stack( - children: [ - Container( - decoration: const BoxDecoration( - color: Colors.red, - backgroundBlendMode: BlendMode.dstOut), // This one will handle background + difference out - ), - Align( - alignment: Alignment.center, - child: Container( - height: scanArea, - width: scanArea, - decoration: BoxDecoration( - color: Colors.red, - borderRadius: BorderRadius.circular(20), - ), - ), - ), - ], - ), - ), - Align( - alignment: Alignment.center, - child: CustomPaint( - foregroundPainter: BorderPainter(), - child: SizedBox( - width: scanArea + 25, - height: scanArea + 25, - ), - ), - ), - ]); - } -} - -// Creates the white borders -class BorderPainter extends CustomPainter { - @override - void paint(Canvas canvas, Size size) { - const width = 4.0; - const radius = 20.0; - const tRadius = 3 * radius; - final rect = Rect.fromLTWH( - width, - width, - size.width - 2 * width, - size.height - 2 * width, - ); - final rrect = RRect.fromRectAndRadius(rect, const Radius.circular(radius)); - const clippingRect0 = Rect.fromLTWH( - 0, - 0, - tRadius, - tRadius, - ); - final clippingRect1 = Rect.fromLTWH( - size.width - tRadius, - 0, - tRadius, - tRadius, - ); - final clippingRect2 = Rect.fromLTWH( - 0, - size.height - tRadius, - tRadius, - tRadius, - ); - final clippingRect3 = Rect.fromLTWH( - size.width - tRadius, - size.height - tRadius, - tRadius, - tRadius, - ); - - final path = Path() - ..addRect(clippingRect0) - ..addRect(clippingRect1) - ..addRect(clippingRect2) - ..addRect(clippingRect3); - - canvas.clipPath(path); - canvas.drawRRect( - rrect, - Paint() - ..color = Colors.white - ..style = PaintingStyle.stroke - ..strokeWidth = width, - ); - } - - @override - bool shouldRepaint(CustomPainter oldDelegate) { - return false; - } -} - -class BarReaderSize { - static double width = 200; - static double height = 200; -} - -class OverlayWithHolePainter extends CustomPainter { - @override - void paint(Canvas canvas, Size size) { - final paint = Paint()..color = Colors.black54; - canvas.drawPath( - Path.combine( - PathOperation.difference, - Path()..addRect(Rect.fromLTWH(0, 0, size.width, size.height)), - Path() - ..addOval(Rect.fromCircle(center: Offset(size.width - 44, size.height - 44), radius: 40)) - ..close(), - ), - paint); - } - - @override - bool shouldRepaint(CustomPainter oldDelegate) { - return false; - } -} - -@override -bool shouldRepaint(CustomPainter oldDelegate) { - return false; -} diff --git a/lib/r.dart b/lib/r.dart deleted file mode 100644 index 743525633..000000000 --- a/lib/r.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'dart:ui'; - -import 'package:flutter/foundation.dart'; -import 'package:sit/school/yellow_pages/entity/contact.dart'; -import 'package:sit/entity/version.dart'; - -class R { - const R._(); - - static const scheme = "life.mysit"; - static const hiveStorageVersion = "2.0.0+14"; - static const appId = "life.mysit.SITLife"; - static const appName = "SIT Life"; - static late AppVersion currentVersion; - - /// For debugging iOS on other platforms. - static const debugCupertino = kDebugMode ? true : false; - - /// The default window size is small enough for any modern desktop device. - static const Size defaultWindowSize = Size(500, 800); - - /// If the window was resized to too small accidentally, this will keep a minimum function area. - static const Size minWindowSize = Size(300, 400); - - static const eduEmailDomain = "mail.sit.edu.cn"; - - static String formatEduEmail({required String username}) { - return "$username@$eduEmailDomain"; - } - - static late List roomList; - static late List userAgentList; - static late List yellowPages; - static const enLocale = Locale('en'); - static const zhHansLocale = Locale.fromSubtags(languageCode: "zh", scriptCode: "Hans"); - static const zhHantLocale = Locale.fromSubtags(languageCode: "zh", scriptCode: "Hant"); - static const defaultLocale = zhHansLocale; - static const supportedLocales = [ - enLocale, - zhHansLocale, - zhHantLocale, - ]; -} diff --git a/lib/route.dart b/lib/route.dart deleted file mode 100644 index 6e7e99902..000000000 --- a/lib/route.dart +++ /dev/null @@ -1,476 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; -import 'package:go_router/go_router.dart'; -import 'package:sit/credentials/entity/login_status.dart'; -import 'package:sit/credentials/widgets/oa_scope.dart'; -import 'package:sit/game/2048/index.dart'; -import 'package:sit/index.dart'; -import 'package:sit/me/edu_email/page/login.dart'; -import 'package:sit/me/edu_email/page/outbox.dart'; -import 'package:sit/school/class2nd/entity/attended.dart'; -import 'package:sit/school/exam_result/page/gpa.dart'; -import 'package:sit/school/exam_result/page/result.pg.dart'; -import 'package:sit/school/library/page/history.dart'; -import 'package:sit/school/library/page/login.dart'; -import 'package:sit/school/library/page/borrowing.dart'; -import 'package:sit/school/ywb/page/service.dart'; -import 'package:sit/school/ywb/page/application.dart'; -import 'package:sit/settings/page/about.dart'; -import 'package:sit/settings/page/life.dart'; -import 'package:sit/settings/page/proxy.dart'; -import 'package:sit/settings/page/school.dart'; -import 'package:sit/settings/page/storage.dart'; -import 'package:sit/life/expense_records/page/records.dart'; -import 'package:sit/life/expense_records/page/statistics.dart'; -import 'package:sit/life/index.dart'; -import 'package:sit/login/page/index.dart'; -import 'package:sit/me/edu_email/page/inbox.dart'; -import 'package:sit/network/page/index.dart'; -import 'package:sit/timetable/page/background.dart'; -import 'package:sit/timetable/page/cell_style.dart'; -import 'package:sit/widgets/not_found.dart'; -import 'package:sit/school/oa_announce/entity/announce.dart'; -import 'package:sit/school/oa_announce/page/details.dart'; -import 'package:sit/school/exam_arrange/page/list.dart'; -import 'package:sit/school/oa_announce/page/list.dart'; -import 'package:sit/qrcode/page/scanner.dart'; -import 'package:sit/school/class2nd/page/details.dart'; -import 'package:sit/school/class2nd/page/activity.dart'; -import 'package:sit/school/class2nd/page/attended.dart'; -import 'package:sit/school/exam_result/page/evaluation.dart'; -import 'package:sit/school/exam_result/page/result.ug.dart'; -import 'package:sit/school/yellow_pages/page/index.dart'; -import 'package:sit/settings/page/credentials.dart'; -import 'package:sit/settings/page/developer.dart'; -import 'package:sit/settings/page/index.dart'; -import 'package:sit/me/index.dart'; -import 'package:sit/school/index.dart'; -import 'package:sit/settings/page/timetable.dart'; -import 'package:sit/timetable/page/import.dart'; -import 'package:sit/timetable/page/index.dart'; -import 'package:sit/timetable/page/mine.dart'; -import 'package:sit/timetable/page/p13n.dart'; -import 'package:sit/widgets/image.dart'; -import 'package:sit/widgets/webview/page.dart'; - -final $Key = GlobalKey(); -final $TimetableShellKey = GlobalKey(); -final $LifeShellKey = GlobalKey(); -final $SchoolShellKey = GlobalKey(); -final $MeShellKey = GlobalKey(); - -bool isLoginGuarded(BuildContext ctx) { - final auth = ctx.auth; - return auth.loginStatus != LoginStatus.validated && auth.credentials == null; -} - -String? _loginRequired(BuildContext ctx, GoRouterState state) { - if (isLoginGuarded(ctx)) return "/login?guard=true"; - return null; -} - -FutureOr _redirectRoot(BuildContext ctx, GoRouterState state) { - final auth = ctx.auth; - if (auth.loginStatus == LoginStatus.never) { -// allow to access settings page. - if (state.matchedLocation.startsWith("/tools")) return null; - if (state.matchedLocation.startsWith("/settings")) return null; -// allow to access browser page. - if (state.matchedLocation == "/browser") return null; - return "/login"; - } - return null; -} - -Widget _onError(BuildContext context, GoRouterState state) { - return NotFoundPage(state.uri.toString()); -} - -final _timetableRoute = GoRoute( - path: "/timetable", -// Timetable is the home page. - builder: (ctx, state) => const TimetablePage(), - routes: [ - GoRoute( - path: "import", - builder: (ctx, state) => const ImportTimetablePage(), - redirect: _loginRequired, - ), - GoRoute( - path: "mine", - builder: (ctx, state) => const MyTimetableListPage(), - ), - GoRoute( - path: "p13n", - builder: (ctx, state) => const TimetableP13nPage(), - routes: [ - GoRoute( - path: "custom", - builder: (ctx, state) => const TimetableP13nPage(tab: TimetableP13nTab.custom), - ), - GoRoute( - path: "builtin", - builder: (ctx, state) => const TimetableP13nPage(tab: TimetableP13nTab.builtin), - ), - ], - ), - GoRoute( - path: "cell-style", - builder: (ctx, state) => const TimetableCellStyleEditor(), - ), - GoRoute( - path: "background", - builder: (ctx, state) => const TimetableBackgroundEditor(), - ), - ], -); - -final _schoolRoute = GoRoute( - path: "/school", - builder: (ctx, state) => const SchoolPage(), -); -final _lifeRoute = GoRoute( - path: "/life", - builder: (ctx, state) => const LifePage(), -); -final _meRoute = GoRoute( - path: "/me", - builder: (ctx, state) => const MePage(), -); -final _toolsRoutes = [ - GoRoute( - path: "/tools/network-tool", - builder: (ctx, state) => const NetworkToolPage(), - ), - GoRoute( - path: "/tools/scanner", - parentNavigatorKey: $Key, - builder: (ctx, state) => const ScannerPage(), - ), -]; -final _settingsRoute = GoRoute( - path: "/settings", - builder: (ctx, state) => const SettingsPage(), - routes: [ - GoRoute( - path: "credentials", - builder: (ctx, state) => const CredentialsPage(), - ), - GoRoute( - path: "timetable", - builder: (ctx, state) => const TimetableSettingsPage(), - ), - GoRoute( - path: "school", - builder: (ctx, state) => const SchoolSettingsPage(), - ), - GoRoute( - path: "life", - builder: (ctx, state) => const LifeSettingsPage(), - ), - GoRoute( - path: "about", - builder: (ctx, state) => const AboutSettingsPage(), - ), - GoRoute( - path: "proxy", - builder: (ctx, state) => const ProxySettingsPage(), - ), - GoRoute( - path: "developer", - builder: (ctx, state) => const DeveloperOptionsPage(), - routes: [ - GoRoute( - path: "local-storage", - builder: (ctx, state) => const LocalStoragePage(), - ) - ], - ), - ], -); -final _expenseRoute = GoRoute( - path: "/expense-records", - builder: (ctx, state) => const ExpenseRecordsPage(), - redirect: _loginRequired, - routes: [ - GoRoute( - path: "statistics", - builder: (ctx, state) => const ExpenseStatisticsPage(), - redirect: _loginRequired, - ) - ], -); - -final _class2ndRoute = GoRoute( - path: "/class2nd", - builder: (ctx, state) => const ActivityListPage(), - redirect: _loginRequired, - routes: [ - GoRoute( - path: "attended", - builder: (ctx, state) => const AttendedActivityPage(), - redirect: _loginRequired, - ), - GoRoute( - path: "activity-details/:id", - builder: (ctx, state) { - final id = int.tryParse(state.pathParameters["id"] ?? ""); - if (id == null) throw 404; - final enableApply = state.uri.queryParameters["enable-apply"] != null; - final title = state.uri.queryParameters["title"]; - final time = DateTime.tryParse(state.uri.queryParameters["time"] ?? ""); - return Class2ndActivityDetailsPage(activityId: id, title: title, time: time, enableApply: enableApply); - }, - redirect: _loginRequired, - ), - GoRoute( - path: "attended-details", - builder: (ctx, state) { - final extra = state.extra; - if (extra is Class2ndAttendedActivity) { - return Class2ndAttendDetailsPage(extra); - } - throw 404; - }, - redirect: _loginRequired, - ), - ], -); - -final _oaAnnounceRoute = GoRoute( - path: "/oa-announce", - builder: (ctx, state) => const OaAnnounceListPage(), - redirect: _loginRequired, - routes: [ - GoRoute( - path: "details", - builder: (ctx, state) { - final extra = state.extra; - if (extra is OaAnnounceRecord) { - return AnnounceDetailsPage(extra); - } - throw 404; - }, - ), - ], -); -final _eduEmailRoutes = [ - GoRoute( - path: "/yellow-pages", - builder: (ctx, state) => const YellowPagesListPage(), - ), - GoRoute( - path: "/edu-email/login", - builder: (ctx, state) => const EduEmailLoginPage(), - ), - GoRoute( - path: "/edu-email/inbox", - builder: (ctx, state) => const EduEmailInboxPage(), - ), - GoRoute( - path: "/edu-email/outbox", - builder: (ctx, state) => const EduEmailOutboxPage(), - ), -]; - -final _ywbRoute = GoRoute( - path: "/ywb", - builder: (ctx, state) => const YwbServiceListPage(), - redirect: _loginRequired, - routes: [ - GoRoute( - path: "mine", - builder: (ctx, state) => const YwbMyApplicationListPage(), - ), - ], -); - -final _imageRoute = GoRoute( - path: "/image", - builder: (ctx, state) { - final extra = state.extra; - final data = state.uri.queryParameters["origin"] ?? extra as String?; - if (data != null) { - return ImageViewPage( - data, - title: state.uri.queryParameters["title"], - ); - } - throw 400; - }, -); - -final _loginRoute = GoRoute( - path: "/login", - builder: (ctx, state) { - final guarded = state.uri.queryParameters["guard"] == "true"; - return LoginPage(isGuarded: guarded); - }, -); - -final _teacherEvalRoute = GoRoute( - path: "/teacher-eval", - builder: (ctx, state) => const TeacherEvaluationPage(), - redirect: _loginRequired, -); - -final _libraryRoutes = [ - GoRoute( - path: "/library/login", - builder: (ctx, state) => const LibraryLoginPage(), - ), - GoRoute( - path: "/library/borrowing", - builder: (ctx, state) => const LibraryBorrowingPage(), - ), - GoRoute( - path: "/library/borrowing-history", - builder: (ctx, state) => const LibraryMyBorrowingHistoryPage(), - ), -]; - -final _examArrange = GoRoute( - path: "/exam-arrange", - builder: (ctx, state) => const ExamArrangementListPage(), - redirect: _loginRequired, -); - -final _examResultRoute = GoRoute( - path: "/exam-result", - routes: [ - GoRoute(path: "ug", builder: (ctx, state) => const ExamResultUgPage(), routes: [ - GoRoute( - path: "gpa", - builder: (ctx, state) => const GpaCalculatorPage(), - ), - ]), - GoRoute( - path: "pg", - builder: (ctx, state) => const ExamResultPgPage(), - ), - ], - redirect: _loginRequired, -); - -final _browserRoute = GoRoute( - path: "/browser", - builder: (ctx, state) { - var url = state.uri.queryParameters["url"] ?? state.extra; - if (url is String) { - if (!url.startsWith("http://") && !url.startsWith("https://")) { - url = "http://$url"; - } - return WebViewPage(initialUrl: url); - } - throw 400; - }, -); -final _gameRoutes = [ - GoRoute( - path: "/game/2048", - builder: (ctx, state) => const Game2048Page(), - ), -]; - -GoRouter buildRouter(ValueNotifier $routingConfig) { - return GoRouter.routingConfig( - routingConfig: $routingConfig, - navigatorKey: $Key, - initialLocation: "/", - debugLogDiagnostics: kDebugMode, - errorBuilder: _onError, - ); -} - -RoutingConfig buildCommonRoutingConfig() { - return RoutingConfig( - redirect: _redirectRoot, - routes: [ - GoRoute( - path: "/", - redirect: (ctx, state) => "/timetable", - ), - StatefulShellRoute.indexedStack( - builder: (ctx, state, navigationShell) { - return MainStagePage(navigationShell: navigationShell); - }, - branches: [ - StatefulShellBranch( - navigatorKey: $TimetableShellKey, - routes: [ - _timetableRoute, - ], - ), - if (!kIsWeb) - StatefulShellBranch( - navigatorKey: $SchoolShellKey, - routes: [ - _schoolRoute, - ], - ), - if (!kIsWeb) - StatefulShellBranch( - navigatorKey: $LifeShellKey, - routes: [ - _lifeRoute, - ], - ), - StatefulShellBranch( - navigatorKey: $MeShellKey, - routes: [ - _meRoute, - ], - ), - ], - ), - _browserRoute, - _expenseRoute, - _settingsRoute, - ..._toolsRoutes, - _class2ndRoute, - _oaAnnounceRoute, - ..._eduEmailRoutes, - _ywbRoute, - _examResultRoute, - _examArrange, - ..._libraryRoutes, - _teacherEvalRoute, - _loginRoute, - _imageRoute, - ..._gameRoutes, - ], - ); -} - -RoutingConfig buildTimetableFocusRouter() { - return RoutingConfig( - redirect: _redirectRoot, - routes: [ - GoRoute( - path: "/", - redirect: (ctx, state) => "/timetable", - ), - _timetableRoute, - _meRoute, - _schoolRoute, - _lifeRoute, - _browserRoute, - _expenseRoute, - _settingsRoute, - ..._toolsRoutes, - _class2ndRoute, - _oaAnnounceRoute, - ..._eduEmailRoutes, - _ywbRoute, - _examResultRoute, - _examArrange, - ..._libraryRoutes, - _teacherEvalRoute, - _loginRoute, - _imageRoute, - ..._gameRoutes, - ], - ); -} diff --git a/lib/school/class2nd/entity/attended.dart b/lib/school/class2nd/entity/attended.dart deleted file mode 100644 index 646286373..000000000 --- a/lib/school/class2nd/entity/attended.dart +++ /dev/null @@ -1,271 +0,0 @@ -import 'dart:core'; - -import 'package:easy_localization/easy_localization.dart'; - -import 'list.dart'; -import 'package:sit/storage/hive/type_id.dart'; - -part 'attended.g.dart'; - -@HiveType(typeId: CacheHiveType.class2ndPointsSummary) -class Class2ndPointsSummary { - /// 主题报告 - @HiveField(0) - final double thematicReport; - - /// 社会实践 - @HiveField(1) - final double practice; - - /// 创新创业创意 - @HiveField(2) - final double creation; - - /// 校园安全文明 - @HiveField(3) - final double schoolSafetyCivilization; - - /// 公益志愿 - @HiveField(4) - final double voluntary; - - /// 校园文化 - @HiveField(5) - final double schoolCulture; - - /// 诚信积分 - @HiveField(6) - final double honestyPoints; - - /// Total points - @HiveField(7) - final double totalPoints; - - const Class2ndPointsSummary({ - this.thematicReport = 0, - this.practice = 0, - this.creation = 0, - this.schoolSafetyCivilization = 0, - this.voluntary = 0, - this.schoolCulture = 0, - this.honestyPoints = 0, - this.totalPoints = 0, - }); - - @override - String toString() { - return { - "lecture": thematicReport, - "practice": practice, - "creation": creation, - "safetyEdu": schoolSafetyCivilization, - "voluntary": voluntary, - "schoolCulture": schoolCulture, - "honestyPoints": honestyPoints, - }.toString(); - } - - List<({Class2ndPointType type, double score})> toName2score() { - return [ - (type: Class2ndPointType.voluntary, score: voluntary), - (type: Class2ndPointType.schoolCulture, score: schoolCulture), - (type: Class2ndPointType.creation, score: creation), - (type: Class2ndPointType.schoolSafetyCivilization, score: schoolSafetyCivilization), - (type: Class2ndPointType.thematicReport, score: thematicReport), - (type: Class2ndPointType.practice, score: practice), - ]; - } -} - -@HiveType(typeId: CacheHiveType.class2ndPointItem) -class Class2ndPointItem { - /// 活动名称 - @HiveField(0) - final String name; - - /// 活动编号 - @HiveField(1) - final int activityId; - - /// 活动类型 - @HiveField(2) - final Class2ndActivityCat category; - - /// 活动时间 - @HiveField(3) - final DateTime? time; - - /// 得分 - @HiveField(4) - final double points; - - /// 诚信分 - @HiveField(5) - final double honestyPoints; - - Class2ndPointType? get pointType => category.pointType; - - const Class2ndPointItem({ - required this.name, - required this.activityId, - required this.category, - required this.time, - required this.points, - required this.honestyPoints, - }); - - @override - String toString() { - return { - "name": name, - "activityId": activityId, - "category": category, - "time": time, - "points": points, - "honestyPoints": honestyPoints, - }.toString(); - } -} - -@HiveType(typeId: CacheHiveType.class2ndActivityApplication) -class Class2ndActivityApplication { - /// 申请编号 - @HiveField(0) - final int applicationId; - - /// 活动编号 - /// -1 if the activity was cancelled. - @HiveField(1) - final int activityId; - - /// 活动标题 - @HiveField(2) - final String title; - - /// 申请时间 - @HiveField(3) - final DateTime time; - - /// 活动状态 - @HiveField(4) - final String status; - - @HiveField(5) - final Class2ndActivityCat category; - - const Class2ndActivityApplication({ - required this.applicationId, - required this.activityId, - required this.title, - required this.time, - required this.status, - required this.category, - }); - - bool get isPassed => status == "通过"; - - @override - String toString() { - return { - "applyId": applicationId, - "activityId": activityId, - "title": title, - "time": time, - "status": status, - "category": category, - }.toString(); - } -} - -@HiveType(typeId: CacheHiveType.class2ndScoreType) -enum Class2ndPointType { - /// 讲座报告 - @HiveField(0) - thematicReport, - - /// 创新创业创意 - @HiveField(1) - creation, - - /// 校园文化 - @HiveField(2) - schoolCulture, - - /// 社会实践 - @HiveField(3) - practice, - - /// 志愿公益 - @HiveField(4) - voluntary, - - /// 校园安全文明 - @HiveField(5) - schoolSafetyCivilization; - - const Class2ndPointType(); - - String l10nShortName() => "class2nd.scoreType.$name.short".tr(); - - String l10nFullName() => "class2nd.scoreType.$name.full".tr(); - - static String allCatL10n() => "class2nd.scoreType.all".tr(); - - static Class2ndPointType? parse(String typeName) { - if (typeName == "主题报告") { - return Class2ndPointType.thematicReport; - } else if (typeName == "社会实践") { - return Class2ndPointType.practice; - } else if (typeName == "创新创业创意") { - return Class2ndPointType.creation; - } else if (typeName == "校园文化") { - return Class2ndPointType.schoolCulture; - } else if (typeName == "公益志愿") { - return Class2ndPointType.voluntary; - } else if (typeName == "校园安全文明") { - return Class2ndPointType.schoolSafetyCivilization; - } - return null; - } -} - -class Class2ndAttendedActivity { - final Class2ndActivityApplication application; - final List scores; - - double? calcTotalPoints() { - if (scores.isEmpty) return null; - return scores.fold(0.0, (pre, e) => pre + e.points); - } - - double? calcTotalHonestyPoints() { - if (scores.isEmpty) return null; - return scores.fold(0.0, (pre, e) => pre + e.honestyPoints); - } - - int get activityId => application.activityId; - - bool get cancelled => application.activityId == -1; - - int get applicationId => application.applicationId; - - Class2ndActivityCat get category => application.category; - - Class2ndPointType? get scoreType => application.category.pointType; - - /// Because the [application.name] might have trailing ellipsis - String get title => scores.firstOrNull?.name ?? application.title; - - const Class2ndAttendedActivity({ - required this.application, - required this.scores, - }); - - @override - String toString() { - return { - "application": application, - "scores": scores, - }.toString(); - } -} diff --git a/lib/school/class2nd/entity/attended.g.dart b/lib/school/class2nd/entity/attended.g.dart deleted file mode 100644 index 4ef35c1ee..000000000 --- a/lib/school/class2nd/entity/attended.g.dart +++ /dev/null @@ -1,211 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'attended.dart'; - -// ************************************************************************** -// TypeAdapterGenerator -// ************************************************************************** - -class Class2ndPointsSummaryAdapter extends TypeAdapter { - @override - final int typeId = 33; - - @override - Class2ndPointsSummary read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return Class2ndPointsSummary( - thematicReport: fields[0] as double, - practice: fields[1] as double, - creation: fields[2] as double, - schoolSafetyCivilization: fields[3] as double, - voluntary: fields[4] as double, - schoolCulture: fields[5] as double, - honestyPoints: fields[6] as double, - totalPoints: fields[7] as double, - ); - } - - @override - void write(BinaryWriter writer, Class2ndPointsSummary obj) { - writer - ..writeByte(8) - ..writeByte(0) - ..write(obj.thematicReport) - ..writeByte(1) - ..write(obj.practice) - ..writeByte(2) - ..write(obj.creation) - ..writeByte(3) - ..write(obj.schoolSafetyCivilization) - ..writeByte(4) - ..write(obj.voluntary) - ..writeByte(5) - ..write(obj.schoolCulture) - ..writeByte(6) - ..write(obj.honestyPoints) - ..writeByte(7) - ..write(obj.totalPoints); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is Class2ndPointsSummaryAdapter && runtimeType == other.runtimeType && typeId == other.typeId; -} - -class Class2ndPointItemAdapter extends TypeAdapter { - @override - final int typeId = 35; - - @override - Class2ndPointItem read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return Class2ndPointItem( - name: fields[0] as String, - activityId: fields[1] as int, - category: fields[2] as Class2ndActivityCat, - time: fields[3] as DateTime?, - points: fields[4] as double, - honestyPoints: fields[5] as double, - ); - } - - @override - void write(BinaryWriter writer, Class2ndPointItem obj) { - writer - ..writeByte(6) - ..writeByte(0) - ..write(obj.name) - ..writeByte(1) - ..write(obj.activityId) - ..writeByte(2) - ..write(obj.category) - ..writeByte(3) - ..write(obj.time) - ..writeByte(4) - ..write(obj.points) - ..writeByte(5) - ..write(obj.honestyPoints); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is Class2ndPointItemAdapter && runtimeType == other.runtimeType && typeId == other.typeId; -} - -class Class2ndActivityApplicationAdapter extends TypeAdapter { - @override - final int typeId = 34; - - @override - Class2ndActivityApplication read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return Class2ndActivityApplication( - applicationId: fields[0] as int, - activityId: fields[1] as int, - title: fields[2] as String, - time: fields[3] as DateTime, - status: fields[4] as String, - category: fields[5] as Class2ndActivityCat, - ); - } - - @override - void write(BinaryWriter writer, Class2ndActivityApplication obj) { - writer - ..writeByte(6) - ..writeByte(0) - ..write(obj.applicationId) - ..writeByte(1) - ..write(obj.activityId) - ..writeByte(2) - ..write(obj.title) - ..writeByte(3) - ..write(obj.time) - ..writeByte(4) - ..write(obj.status) - ..writeByte(5) - ..write(obj.category); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is Class2ndActivityApplicationAdapter && runtimeType == other.runtimeType && typeId == other.typeId; -} - -class Class2ndPointTypeAdapter extends TypeAdapter { - @override - final int typeId = 36; - - @override - Class2ndPointType read(BinaryReader reader) { - switch (reader.readByte()) { - case 0: - return Class2ndPointType.thematicReport; - case 1: - return Class2ndPointType.creation; - case 2: - return Class2ndPointType.schoolCulture; - case 3: - return Class2ndPointType.practice; - case 4: - return Class2ndPointType.voluntary; - case 5: - return Class2ndPointType.schoolSafetyCivilization; - default: - return Class2ndPointType.thematicReport; - } - } - - @override - void write(BinaryWriter writer, Class2ndPointType obj) { - switch (obj) { - case Class2ndPointType.thematicReport: - writer.writeByte(0); - break; - case Class2ndPointType.creation: - writer.writeByte(1); - break; - case Class2ndPointType.schoolCulture: - writer.writeByte(2); - break; - case Class2ndPointType.practice: - writer.writeByte(3); - break; - case Class2ndPointType.voluntary: - writer.writeByte(4); - break; - case Class2ndPointType.schoolSafetyCivilization: - writer.writeByte(5); - break; - } - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is Class2ndPointTypeAdapter && runtimeType == other.runtimeType && typeId == other.typeId; -} diff --git a/lib/school/class2nd/entity/details.dart b/lib/school/class2nd/entity/details.dart deleted file mode 100644 index 63bd5b016..000000000 --- a/lib/school/class2nd/entity/details.dart +++ /dev/null @@ -1,87 +0,0 @@ -import 'package:sit/storage/hive/type_id.dart'; - -part 'details.g.dart'; - -@HiveType(typeId: CacheHiveType.activityDetails) -class Class2ndActivityDetails { - /// Activity id - @HiveField(0) - final int id; - - /// Activity title - @HiveField(1) - final String title; - - /// Activity start time - @HiveField(2) - final DateTime startTime; - - /// Sign start time - @HiveField(3) - final DateTime signStartTime; - - /// Sign end time - @HiveField(4) - final DateTime signEndTime; - - /// Place - @HiveField(5) - final String? place; - - /// Duration - @HiveField(6) - final String? duration; - - /// Activity manager - @HiveField(7) - final String? principal; - - /// Manager yellow_pages(phone) - @HiveField(8) - final String? contactInfo; - - /// Activity organizer - @HiveField(9) - final String? organizer; - - /// Activity undertaker - @HiveField(10) - final String? undertaker; - - /// Description in text. - @HiveField(11) - final String? description; - - const Class2ndActivityDetails({ - required this.id, - required this.title, - required this.startTime, - required this.signStartTime, - required this.signEndTime, - this.place, - this.duration, - this.principal, - this.contactInfo, - this.organizer, - this.undertaker, - this.description, - }); - - @override - String toString() { - return { - "id": id, - "title": title, - "startTime": startTime, - "signStartTime": signStartTime, - "signEndTime": signEndTime, - "place": place, - "duration": duration, - "principal": principal, - "contactInfo": contactInfo, - "organizer": organizer, - "undertaker": undertaker, - "description": description, - }.toString(); - } -} diff --git a/lib/school/class2nd/entity/details.g.dart b/lib/school/class2nd/entity/details.g.dart deleted file mode 100644 index 370f98ba2..000000000 --- a/lib/school/class2nd/entity/details.g.dart +++ /dev/null @@ -1,72 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'details.dart'; - -// ************************************************************************** -// TypeAdapterGenerator -// ************************************************************************** - -class Class2ndActivityDetailsAdapter extends TypeAdapter { - @override - final int typeId = 31; - - @override - Class2ndActivityDetails read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return Class2ndActivityDetails( - id: fields[0] as int, - title: fields[1] as String, - startTime: fields[2] as DateTime, - signStartTime: fields[3] as DateTime, - signEndTime: fields[4] as DateTime, - place: fields[5] as String?, - duration: fields[6] as String?, - principal: fields[7] as String?, - contactInfo: fields[8] as String?, - organizer: fields[9] as String?, - undertaker: fields[10] as String?, - description: fields[11] as String?, - ); - } - - @override - void write(BinaryWriter writer, Class2ndActivityDetails obj) { - writer - ..writeByte(12) - ..writeByte(0) - ..write(obj.id) - ..writeByte(1) - ..write(obj.title) - ..writeByte(2) - ..write(obj.startTime) - ..writeByte(3) - ..write(obj.signStartTime) - ..writeByte(4) - ..write(obj.signEndTime) - ..writeByte(5) - ..write(obj.place) - ..writeByte(6) - ..write(obj.duration) - ..writeByte(7) - ..write(obj.principal) - ..writeByte(8) - ..write(obj.contactInfo) - ..writeByte(9) - ..write(obj.organizer) - ..writeByte(10) - ..write(obj.undertaker) - ..writeByte(11) - ..write(obj.description); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is Class2ndActivityDetailsAdapter && runtimeType == other.runtimeType && typeId == other.typeId; -} diff --git a/lib/school/class2nd/entity/list.dart b/lib/school/class2nd/entity/list.dart deleted file mode 100644 index 96a160830..000000000 --- a/lib/school/class2nd/entity/list.dart +++ /dev/null @@ -1,150 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; - -import 'package:sit/storage/hive/type_id.dart'; - -import 'attended.dart'; - -part 'list.g.dart'; - -@HiveType(typeId: CacheHiveType.activityCat) -enum Class2ndActivityCat { - /// 讲座报告 - @HiveField(0) - lecture( - "001", - Class2ndPointType.thematicReport, - ), - - /// 主题教育 - @HiveField(1) - thematicEdu("ff808081674ec4720167ce60dda77cea"), - - /// 创新创业创意 - @HiveField(2) - creation( - "ff8080814e241104014eb867e1481dc3", - Class2ndPointType.creation, - ), - - /// 校园文化活动 - @HiveField(3) - schoolCultureActivity( - "8ab17f543fe626a8013fe6278a880001", - Class2ndPointType.schoolCulture, - ), - - /// 校园文明 - @HiveField(4) - schoolCivilization( - "8F963F2A04013A66E0540021287E4866", - Class2ndPointType.schoolSafetyCivilization, - ), - - /// 社会实践 - @HiveField(5) - practice( - "8ab17f543fe62d5d013fe62efd3a0002", - Class2ndPointType.practice, - ), - - /// 志愿公益 - @HiveField(6) - voluntary( - "8ab17f543fe62d5d013fe62e6dc70001", - Class2ndPointType.voluntary, - ), - - /// 安全教育网络教学 - @HiveField(7) - onlineSafetyEdu( - "402881de5d62ba57015d6320f1a7000c", - Class2ndPointType.schoolSafetyCivilization, - ), - - /// 会议(无学分) - @HiveField(8) - conference("ff8080814e241104014fedbbf7fd329d"), - - /// 校园文化竞赛活动 - @HiveField(9) - schoolCultureCompetition( - "8ab17f2a3fe6585e013fe6596c300001", - Class2ndPointType.schoolCulture, - ), - - /// 论文专利 - @HiveField(10) - paperAndPatent( - "8ab17f533ff05c27013ff06d10bf0001", - Class2ndPointType.creation, - ); - - final String id; - final Class2ndPointType? pointType; - - const Class2ndActivityCat(this.id, [this.pointType]); - - String l10nName() => "class2nd.activityCat.$name".tr(); - - static String allCatL10n() => "class2nd.activityCat.all".tr(); - - /// Don't Change this. - /// Strings from school API - static Class2ndActivityCat? parse(String catName) { - if (catName == "讲座报告") { - return Class2ndActivityCat.lecture; - } else if (catName == "主题教育") { - return Class2ndActivityCat.lecture; - } else if (catName == "校园文化活动") { - return Class2ndActivityCat.schoolCultureActivity; - } else if (catName == "校园文化竞赛活动") { - return Class2ndActivityCat.schoolCultureCompetition; - } else if (catName == "创新创业创意") { - return Class2ndActivityCat.creation; - } else if (catName == "论文专利") { - return Class2ndActivityCat.paperAndPatent; - } else if (catName == "社会实践") { - return Class2ndActivityCat.practice; - } else if (catName == "志愿公益") { - return Class2ndActivityCat.voluntary; - } else if (catName.contains("安全教育网络教学")) { - // To prevent ellipsis - return Class2ndActivityCat.onlineSafetyEdu; - } else if (catName == "校园文明") { - return Class2ndActivityCat.schoolCivilization; - } else if (catName.contains("会议")) { - return Class2ndActivityCat.conference; - } - return null; - } -} - -@HiveType(typeId: CacheHiveType.activity) -class Class2ndActivity { - /// Activity id - @HiveField(0) - final int id; - - /// Title - @HiveField(1) - final String title; - - /// Date - @HiveField(2) - final DateTime time; - - const Class2ndActivity({ - required this.id, - required this.title, - required this.time, - }); - - @override - String toString() { - return { - "id": id, - "fullTitle": title, - "time": time, - }.toString(); - } -} diff --git a/lib/school/class2nd/entity/list.g.dart b/lib/school/class2nd/entity/list.g.dart deleted file mode 100644 index 243841462..000000000 --- a/lib/school/class2nd/entity/list.g.dart +++ /dev/null @@ -1,127 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'list.dart'; - -// ************************************************************************** -// TypeAdapterGenerator -// ************************************************************************** - -class Class2ndActivityAdapter extends TypeAdapter { - @override - final int typeId = 30; - - @override - Class2ndActivity read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return Class2ndActivity( - id: fields[0] as int, - title: fields[1] as String, - time: fields[2] as DateTime, - ); - } - - @override - void write(BinaryWriter writer, Class2ndActivity obj) { - writer - ..writeByte(3) - ..writeByte(0) - ..write(obj.id) - ..writeByte(1) - ..write(obj.title) - ..writeByte(2) - ..write(obj.time); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is Class2ndActivityAdapter && runtimeType == other.runtimeType && typeId == other.typeId; -} - -class Class2ndActivityCatAdapter extends TypeAdapter { - @override - final int typeId = 32; - - @override - Class2ndActivityCat read(BinaryReader reader) { - switch (reader.readByte()) { - case 0: - return Class2ndActivityCat.lecture; - case 1: - return Class2ndActivityCat.thematicEdu; - case 2: - return Class2ndActivityCat.creation; - case 3: - return Class2ndActivityCat.schoolCultureActivity; - case 4: - return Class2ndActivityCat.schoolCivilization; - case 5: - return Class2ndActivityCat.practice; - case 6: - return Class2ndActivityCat.voluntary; - case 7: - return Class2ndActivityCat.onlineSafetyEdu; - case 8: - return Class2ndActivityCat.conference; - case 9: - return Class2ndActivityCat.schoolCultureCompetition; - case 10: - return Class2ndActivityCat.paperAndPatent; - default: - return Class2ndActivityCat.lecture; - } - } - - @override - void write(BinaryWriter writer, Class2ndActivityCat obj) { - switch (obj) { - case Class2ndActivityCat.lecture: - writer.writeByte(0); - break; - case Class2ndActivityCat.thematicEdu: - writer.writeByte(1); - break; - case Class2ndActivityCat.creation: - writer.writeByte(2); - break; - case Class2ndActivityCat.schoolCultureActivity: - writer.writeByte(3); - break; - case Class2ndActivityCat.schoolCivilization: - writer.writeByte(4); - break; - case Class2ndActivityCat.practice: - writer.writeByte(5); - break; - case Class2ndActivityCat.voluntary: - writer.writeByte(6); - break; - case Class2ndActivityCat.onlineSafetyEdu: - writer.writeByte(7); - break; - case Class2ndActivityCat.conference: - writer.writeByte(8); - break; - case Class2ndActivityCat.schoolCultureCompetition: - writer.writeByte(9); - break; - case Class2ndActivityCat.paperAndPatent: - writer.writeByte(10); - break; - } - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is Class2ndActivityCatAdapter && runtimeType == other.runtimeType && typeId == other.typeId; -} diff --git a/lib/school/class2nd/i18n.dart b/lib/school/class2nd/i18n.dart deleted file mode 100644 index 264533b2c..000000000 --- a/lib/school/class2nd/i18n.dart +++ /dev/null @@ -1,110 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:sit/l10n/common.dart'; - -const i18n = _I18n(); - -class _I18n with CommonI18nMixin { - const _I18n(); - - static const ns = "class2nd"; - - final apply = const _Apply(); - final attended = const _Attended(); - final info = const _Info(); - - String get title => "$ns.title".tr(); - - String get noAttendedActivities => "$ns.noAttendedActivities".tr(); - - String get noActivities => "$ns.noActivities".tr(); - - String get activityAction => "$ns.activity".tr(); - - String get attendedAction => "$ns.attended.title".tr(); - - String get refreshSuccessTip => "$ns.refreshSuccessTip".tr(); - - String get refreshFailedTip => "$ns.refreshFailedTip".tr(); - - String get noDetails => "$ns.noDetails".tr(); - - String get infoTab => "$ns.tab.info".tr(); - - String get descriptionTab => "$ns.tab.description".tr(); - - String get viewDetails => "$ns.viewDetails".tr(); -} - -class _Apply { - const _Apply(); - - static const ns = "${_I18n.ns}.apply"; - - String get btn => "$ns.btn".tr(); - - String get replyTip => "$ns.replyTip".tr(); - - String get applyRequest => "$ns.applyRequest".tr(); - - String get applyRequestDesc => "$ns.applyRequestDesc".tr(); -} - -class _Attended { - const _Attended(); - - static const ns = "${_I18n.ns}.attended"; - - String get title => "$ns.title".tr(); -} - -class _Info { - const _Info(); - - static const ns = "${_I18n.ns}.info"; - - String get applicationId => "$ns.applicationId".tr(); - - String get activityId => "$ns.activityId".tr(); - - String applicationOf(int applicationId) => "$ns.applicationOf".tr( - args: [applicationId.toString()], - ); - - String activityOf(int activityId) => "$ns.activityOf".tr( - args: [activityId.toString()], - ); - - String get name => "$ns.name".tr(); - - String get tags => "$ns.tags".tr(); - - String get category => "$ns.category".tr(); - - String get scoreType => "$ns.scoreType".tr(); - - String get honestyPoints => "$ns.honestyPoints".tr(); - - String get totalPoints => "$ns.totalPoints".tr(); - - String get applicationTime => "$ns.applicationTime".tr(); - - String get status => "$ns.status".tr(); - - String get duration => "$ns.duration".tr(); - - String get location => "$ns.location".tr(); - - String get organizer => "$ns.organizer".tr(); - - String get principal => "$ns.principal".tr(); - - String get signInTime => "$ns.signInTime".tr(); - - String get signOutTime => "$ns.signOutTime".tr(); - - String get startTime => "$ns.startTime".tr(); - - String get undertaker => "$ns.undertaker".tr(); - - String get contactInfo => "$ns.contactInfo".tr(); -} diff --git a/lib/school/class2nd/index.dart b/lib/school/class2nd/index.dart deleted file mode 100644 index f2e50a41b..000000000 --- a/lib/school/class2nd/index.dart +++ /dev/null @@ -1,174 +0,0 @@ -import 'dart:async'; - -import 'package:collection/collection.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:sit/credentials/widgets/oa_scope.dart'; -import 'package:sit/design/adaptive/multiplatform.dart'; -import 'package:sit/design/widgets/app.dart'; -import 'package:sit/design/adaptive/dialog.dart'; -import 'package:sit/school/class2nd/widgets/summary.dart'; -import 'package:sit/school/event.dart'; -import 'package:sit/school/utils.dart'; -import 'package:sit/utils/async_event.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:share_plus/share_plus.dart'; - -import 'entity/attended.dart'; -import "i18n.dart"; -import 'init.dart'; - -class Class2ndAppCard extends StatefulWidget { - const Class2ndAppCard({super.key}); - - @override - State createState() => _Class2ndAppCardState(); -} - -class _Class2ndAppCardState extends State { - var summary = Class2ndInit.pointStorage.pointsSummary; - final $pointsSummary = Class2ndInit.pointStorage.listenPointsSummary(); - late final EventSubscription $refreshEvent; - - @override - void initState() { - $pointsSummary.addListener(onSummaryChanged); - $refreshEvent = schoolEventBus.addListener(() async { - await refresh(active: true); - }); - super.initState(); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - refresh(active: false); - } - - @override - void dispose() { - $pointsSummary.removeListener(onSummaryChanged); - $refreshEvent.cancel(); - super.dispose(); - } - - void onSummaryChanged() { - setState(() { - summary = Class2ndInit.pointStorage.pointsSummary; - }); - } - - Future refresh({required bool active}) async { - // TODO: Error when school server unavailable. - try { - final summary = await Class2ndInit.pointService.fetchScoreSummary(); - Class2ndInit.pointStorage.pointsSummary = summary; - if (active) { - if (!mounted) return; - context.showSnackBar(content: i18n.refreshSuccessTip.text()); - } - } catch (error) { - if (active) { - if (!mounted) return; - context.showSnackBar(content: i18n.refreshFailedTip.text()); - } - } - } - - Class2ndPointsSummary getTargetScore() { - final admissionYear = getAdmissionYearFromStudentId(context.auth.credentials?.account); - return getTargetScoreOf(admissionYear: admissionYear); - } - - @override - Widget build(BuildContext context) { - final summary = this.summary; - return AppCard( - title: i18n.title.text(), - view: summary == null - ? const SizedBox() - : buildSummeryCard( - summary: summary, - target: getTargetScore(), - ), - subtitle: summary == null - ? null - : [ - "${i18n.info.honestyPoints}: ${summary.honestyPoints}".text(), - "${i18n.info.totalPoints}: ${summary.totalPoints}".text(), - ].column(caa: CrossAxisAlignment.start), - leftActions: [ - FilledButton.icon( - onPressed: () async { - await context.push("/class2nd"); - }, - label: i18n.activityAction.text(), - icon: const Icon(Icons.local_activity), - ), - OutlinedButton( - onPressed: () async { - await context.push("/class2nd/attended"); - }, - child: i18n.attendedAction.text(), - ) - ], - rightActions: [ - if (!isCupertino) - IconButton( - tooltip: i18n.share, - onPressed: summary != null - ? () async { - await shareSummery(summary: summary, target: getTargetScore(), context: context); - } - : null, - icon: const Icon(Icons.share_outlined), - ), - ], - ); - } - - Widget buildSummeryCard({ - required Class2ndPointsSummary summary, - required Class2ndPointsSummary target, - }) { - final card = Class2ndScoreSummeryCard( - targetScore: target, - summary: summary, - ).constrained(maxH: 250); - if (!isCupertino) return card; - return Builder( - builder: (ctx) => CupertinoContextMenu.builder( - enableHapticFeedback: true, - actions: [ - CupertinoContextMenuAction( - trailingIcon: CupertinoIcons.share, - onPressed: () async { - Navigator.of(context, rootNavigator: true).pop(); - await shareSummery(summary: summary, target: target, context: ctx); - }, - child: i18n.share.text(), - ), - ], - builder: (ctx, animation) => card, - ), - ); - } - - Future shareSummery({ - required Class2ndPointsSummary summary, - required Class2ndPointsSummary target, - required BuildContext context, - }) async { - final name2score = summary.toName2score(); - final name2target = target.toName2score(); - final text = name2score - .map((e) => - "${e.type.l10nFullName()}: ${e.score}/${name2target.firstWhereOrNull((t) => t.type == e.type)?.score}") - .join(", "); - await Share.share( - text, - sharePositionOrigin: context.getSharePositionOrigin(), - ); - } -} diff --git a/lib/school/class2nd/init.dart b/lib/school/class2nd/init.dart deleted file mode 100644 index c3a2a52f7..000000000 --- a/lib/school/class2nd/init.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'service/activity.dart'; -import 'service/application.dart'; -import 'service/points.dart'; -import 'storage/activity.dart'; -import 'storage/points.dart'; - -class Class2ndInit { - static late Class2ndPointsService pointService; - static late Class2ndPointsStorage pointStorage; - static late Class2ndActivityService activityService; - static late Class2ndActivityStorage activityStorage; - static late Class2ndApplicationService applicationService; - - static void init() { - pointStorage = const Class2ndPointsStorage(); - pointService = const Class2ndPointsService(); - activityService = const Class2ndActivityService(); - activityStorage = const Class2ndActivityStorage(); - applicationService = const Class2ndApplicationService(); - } -} diff --git a/lib/school/class2nd/page/activity.dart b/lib/school/class2nd/page/activity.dart deleted file mode 100644 index 134f74ae5..000000000 --- a/lib/school/class2nd/page/activity.dart +++ /dev/null @@ -1,202 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:sit/design/widgets/common.dart'; -import 'package:sit/utils/collection.dart'; -import 'package:sit/utils/error.dart'; - -import '../entity/list.dart'; -import '../init.dart'; -import '../utils.dart'; -import '../widgets/activity.dart'; -import '../widgets/search.dart'; -import '../i18n.dart'; - -class ActivityListPage extends StatefulWidget { - const ActivityListPage({super.key}); - - @override - State createState() => _ActivityListPageState(); -} - -class _ActivityListPageState extends State { - final $loadingStates = ValueNotifier(commonClass2ndCategories.map((cat) => false).toList()); - - @override - void dispose() { - $loadingStates.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - bottomNavigationBar: PreferredSize( - preferredSize: const Size.fromHeight(4), - child: $loadingStates >> - (ctx, states) { - return !states.any((state) => state == true) ? const SizedBox() : const LinearProgressIndicator(); - }, - ), - body: DefaultTabController( - length: commonClass2ndCategories.length, - child: NestedScrollView( - floatHeaderSlivers: true, - headerSliverBuilder: (context, innerBoxIsScrolled) { - // These are the slivers that show up in the "outer" scroll view. - return [ - SliverOverlapAbsorber( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), - sliver: SliverAppBar( - floating: true, - title: i18n.title.text(), - forceElevated: innerBoxIsScrolled, - actions: [ - IconButton( - icon: const Icon(Icons.search), - onPressed: () => showSearch(context: context, delegate: ActivitySearchDelegate()), - ), - ], - bottom: TabBar( - isScrollable: true, - tabs: commonClass2ndCategories - .mapIndexed( - (i, e) => Tab( - child: e.l10nName().text(), - ), - ) - .toList(), - ), - ), - ), - ]; - }, - body: TabBarView( - // These are the contents of the tab views, below the tabs. - children: commonClass2ndCategories.mapIndexed((i, cat) { - return ActivityLoadingList( - cat: cat, - onLoadingChanged: (state) { - final newStates = List.of($loadingStates.value); - newStates[i] = state; - $loadingStates.value = newStates; - }, - ); - }).toList(), - ), - ), - ), - ); - } -} - -/// Thanks to the cache, don't worry about that switching tab will re-fetch the activity list. -class ActivityLoadingList extends StatefulWidget { - final Class2ndActivityCat cat; - final ValueChanged onLoadingChanged; - - const ActivityLoadingList({ - super.key, - required this.cat, - required this.onLoadingChanged, - }); - - @override - State createState() => _ActivityLoadingListState(); -} - -class _ActivityLoadingListState extends State with AutomaticKeepAliveClientMixin { - int lastPage = 1; - bool isFetching = false; - late var activities = Class2ndInit.activityStorage.getActivities(widget.cat); - - @override - bool get wantKeepAlive => true; - - @override - void initState() { - super.initState(); - Future.delayed(Duration.zero).then((value) async { - await loadMore(); - }); - } - - @override - Widget build(BuildContext context) { - super.build(context); - final activities = this.activities; - return NotificationListener( - onNotification: (event) { - if (event.metrics.pixels >= event.metrics.maxScrollExtent) { - loadMore(); - } - return true; - }, - child: CustomScrollView( - // CAN'T USE ScrollController, and I don't know why - // controller: scrollController, - slivers: [ - SliverOverlapInjector( - // This is the flip side of the SliverOverlapAbsorber above. - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), - ), - if (activities != null) - if (activities.isEmpty) - SliverFillRemaining( - child: LeavingBlank( - icon: Icons.inbox_outlined, - desc: i18n.noActivities, - ), - ) - else - SliverList.builder( - itemCount: activities.length, - itemBuilder: (ctx, index) { - final activity = activities[index]; - return ActivityCard( - activity, - onTap: () async { - await context.push( - "/class2nd/activity-details/${activity.id}?title=${activity.title}&time=${activity.time}&enable-apply=true", - ); - }, - ); - }, - ), - ], - ), - ); - } - - Future loadMore() async { - if (isFetching) return; - if (!mounted) return; - setState(() { - isFetching = true; - }); - widget.onLoadingChanged(true); - try { - final lastActivities = await Class2ndInit.activityService.getActivityList(widget.cat, lastPage); - final activities = this.activities ?? []; - activities.addAll(lastActivities); - activities.distinctBy((a) => a.id); - // The incoming activities may be the same as before, so distinct is necessary. - activities.sort((a, b) => b.time.compareTo(a.time)); - await Class2ndInit.activityStorage.setActivities(widget.cat, activities); - if (!mounted) return; - setState(() { - lastPage++; - isFetching = false; - }); - widget.onLoadingChanged(false); - } catch (error, stackTrace) { - debugPrintError(error, stackTrace); - if (!mounted) return; - setState(() { - isFetching = false; - }); - widget.onLoadingChanged(false); - } - } -} diff --git a/lib/school/class2nd/page/attended.dart b/lib/school/class2nd/page/attended.dart deleted file mode 100644 index a84f8cc9e..000000000 --- a/lib/school/class2nd/page/attended.dart +++ /dev/null @@ -1,408 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:go_router/go_router.dart'; -import 'package:sit/credentials/widgets/oa_scope.dart'; -import 'package:sit/design/adaptive/foundation.dart'; -import 'package:sit/design/animation/progress.dart'; -import 'package:sit/design/widgets/card.dart'; -import 'package:sit/design/widgets/common.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:sit/design/widgets/list_tile.dart'; -import 'package:sit/design/widgets/tags.dart'; -import 'package:sit/l10n/extension.dart'; -import 'package:sit/school/class2nd/entity/list.dart'; -import 'package:sit/school/class2nd/utils.dart'; -import 'package:sit/utils/error.dart'; - -import '../entity/attended.dart'; -import '../init.dart'; -import '../widgets/summary.dart'; -import '../i18n.dart'; - -class AttendedActivityPage extends StatefulWidget { - const AttendedActivityPage({super.key}); - - @override - State createState() => _AttendedActivityPageState(); -} - -class _AttendedActivityPageState extends State { - List? attended = () { - final applications = Class2ndInit.pointStorage.applicationList; - final scores = Class2ndInit.pointStorage.pointItemList; - if (applications == null || scores == null) return null; - return buildAttendedActivityList( - applications: applications, - scores: scores, - ); - }(); - late bool isFetching = false; - final $loadingProgress = ValueNotifier(0.0); - Class2ndActivityCat? selectedCat; - Class2ndPointType? selectedScoreType; - - @override - void initState() { - super.initState(); - refresh(active: false); - } - - @override - void dispose() { - $loadingProgress.dispose(); - super.dispose(); - } - - Future refresh({required bool active}) async { - if (!mounted) return; - setState(() => isFetching = true); - try { - $loadingProgress.value = 0; - final applicationList = await Class2ndInit.pointService.fetchActivityApplicationList(); - $loadingProgress.value = 0.5; - final scoreItemList = await Class2ndInit.pointService.fetchScoreItemList(); - $loadingProgress.value = 1.0; - Class2ndInit.pointStorage.applicationList = applicationList; - Class2ndInit.pointStorage.pointItemList = scoreItemList; - - if (!mounted) return; - setState(() { - attended = buildAttendedActivityList(applications: applicationList, scores: scoreItemList); - isFetching = false; - }); - } catch (error, stackTrace) { - debugPrintError(error, stackTrace); - if (!mounted) return; - setState(() => isFetching = false); - } finally { - $loadingProgress.value = 0; - } - } - - Class2ndPointsSummary getTargetScore() { - final admissionYear = int.tryParse(context.auth.credentials?.account.substring(0, 2) ?? "") ?? 2000; - return getTargetScoreOf(admissionYear: admissionYear); - } - - @override - Widget build(BuildContext context) { - final attended = this.attended; - final filteredActivities = attended - ?.where((activity) => selectedCat == null || activity.category == selectedCat) - .where((activity) => selectedScoreType == null || activity.scoreType == selectedScoreType) - .toList(); - return Scaffold( - body: NestedScrollView( - floatHeaderSlivers: true, - headerSliverBuilder: (context, innerBoxIsScrolled) { - return [ - SliverOverlapAbsorber( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), - sliver: SliverAppBar( - floating: true, - title: i18n.attended.title.text(), - bottom: isFetching - ? PreferredSize( - preferredSize: const Size.fromHeight(4), - child: $loadingProgress >> (ctx, value) => AnimatedProgressBar(value: value), - ) - : null, - forceElevated: innerBoxIsScrolled, - ), - ), - ]; - }, - body: RefreshIndicator.adaptive( - triggerMode: RefreshIndicatorTriggerMode.anywhere, - onRefresh: () async { - await HapticFeedback.heavyImpact(); - await refresh(active: true); - }, - child: CustomScrollView( - slivers: [ - SliverList.list(children: [ - ListTile( - title: i18n.info.category.text(), - ), - buildActivityCatChoices(), - ListTile( - title: i18n.info.scoreType.text(), - ), - buildScoreTypeChoices(), - ]), - const SliverToBoxAdapter( - child: Divider(), - ), - if (filteredActivities != null) - if (filteredActivities.isEmpty) - SliverFillRemaining( - child: LeavingBlank( - icon: Icons.inbox_outlined, - desc: i18n.noAttendedActivities, - ), - ) - else - SliverList.builder( - itemCount: filteredActivities.length, - itemBuilder: (ctx, i) { - final activity = filteredActivities[i]; - return AttendedActivityCard(activity); - }, - ), - ], - ), - ), - ), - ); - } - - Widget buildActivityCatChoices() { - return ListView( - scrollDirection: Axis.horizontal, - physics: const RangeMaintainingScrollPhysics(), - children: [ - ChoiceChip( - label: Class2ndActivityCat.allCatL10n().text(), - selected: selectedCat == null, - onSelected: (value) { - setState(() { - selectedCat = null; - }); - }, - ).padH(4), - ...(attended ?? const []).map((activity) => activity.category).toSet().map( - (cat) => ChoiceChip( - label: cat.l10nName().text(), - selected: selectedCat == cat, - onSelected: (value) { - setState(() { - selectedCat = cat; - selectedScoreType = null; - }); - }, - ).padH(4), - ), - ], - ).sized(h: 40); - } - - Widget buildScoreTypeChoices() { - return ListView( - scrollDirection: Axis.horizontal, - physics: const RangeMaintainingScrollPhysics(), - children: [ - ChoiceChip( - label: Class2ndPointType.allCatL10n().text(), - selected: selectedScoreType == null, - onSelected: (value) { - setState(() { - selectedScoreType = null; - }); - }, - ).padH(4), - ...(attended ?? const []).map((activity) => activity.category.pointType).whereNotNull().toSet().map( - (scoreType) => ChoiceChip( - label: scoreType.l10nFullName().text(), - selected: selectedScoreType == scoreType, - onSelected: (value) { - setState(() { - selectedScoreType = scoreType; - selectedCat = null; - }); - }, - ).padH(4), - ), - ], - ).sized(h: 40); - } -} - -class AttendedActivityCard extends StatelessWidget { - final Class2ndAttendedActivity attended; - - const AttendedActivityCard(this.attended, {super.key}); - - @override - Widget build(BuildContext context) { - final (:title, :tags) = separateTagsFromTitle(attended.title); - final points = attended.calcTotalPoints(); - return FilledCard( - clip: Clip.hardEdge, - child: ListTile( - isThreeLine: true, - title: title.text(), - subtitleTextStyle: context.textTheme.bodyMedium, - subtitle: [ - "${attended.category.l10nName()} #${attended.application.applicationId}".text(), - context.formatYmdhmsNum(attended.application.time).text(), - if (tags.isNotEmpty) TagsGroup(tags), - ].column(caa: CrossAxisAlignment.start), - trailing: points != null && points != 0 - ? Text( - _pointsText(points), - style: context.textTheme.titleMedium?.copyWith(color: _pointsColor(context, points)), - ) - : Text( - attended.application.status, - style: context.textTheme.titleMedium - ?.copyWith(color: attended.application.isPassed ? Colors.green : null), - ), - onTap: () async { - await context.push("/class2nd/attended-details", extra: attended); - }), - ); - } -} - -class Class2ndAttendDetailsPage extends StatefulWidget { - final Class2ndAttendedActivity activity; - - const Class2ndAttendDetailsPage( - this.activity, { - super.key, - }); - - @override - State createState() => _Class2ndAttendDetailsPageState(); -} - -class _Class2ndAttendDetailsPageState extends State { - @override - Widget build(BuildContext context) { - final activity = widget.activity; - final (:title, :tags) = separateTagsFromTitle(activity.title); - final scores = activity.scores; - return Scaffold( - body: CustomScrollView( - slivers: [ - SliverAppBar( - floating: true, - title: i18n.info.applicationOf(activity.application.applicationId).text(), - ), - SliverList.list(children: [ - DetailListTile( - title: i18n.info.name, - subtitle: title, - ), - DetailListTile( - title: i18n.info.category, - subtitle: activity.category.l10nName(), - ), - DetailListTile( - title: i18n.info.applicationTime, - subtitle: context.formatYmdhmNum(activity.application.time), - ), - DetailListTile( - title: i18n.info.status, - subtitle: activity.application.status, - ), - if (tags.isNotEmpty) - ListTile( - isThreeLine: true, - title: i18n.info.tags.text(), - subtitle: TagsGroup(tags), - visualDensity: VisualDensity.compact, - ), - if (!activity.cancelled) - ListTile( - title: i18n.viewDetails.text(), - subtitle: i18n.info.activityOf(activity.activityId).text(), - trailing: const Icon(Icons.open_in_new), - onTap: () async { - await context.push( - "/class2nd/activity-details/${activity.activityId}?title=${activity.title}", - ); - }, - ), - ]), - if (scores.isNotEmpty) - const SliverToBoxAdapter( - child: Divider(), - ), - SliverList.builder( - itemCount: scores.length, - itemBuilder: (ctx, i) { - return Class2ndScoreTile(scores[i]); - }, - ), - ], - ), - ); - } -} - -class Class2ndScoreTile extends StatelessWidget { - final Class2ndPointItem score; - - const Class2ndScoreTile( - this.score, { - super.key, - }); - - @override - Widget build(BuildContext context) { - final time = score.time; - final subtitle = time == null ? null : context.formatYmdhmNum(time).text(); - final scoreType = score.pointType; - if (score.points != 0 && score.honestyPoints != 0) { - return ListTile( - title: RichText( - text: TextSpan(children: [ - TextSpan( - text: scoreType != null - ? "${scoreType.l10nFullName()} ${_pointsText(score.points)}" - : _pointsText(score.points), - style: context.textTheme.bodyLarge?.copyWith(color: _pointsColor(context, score.points)), - ), - const TextSpan(text: "\n"), - TextSpan( - text: "${i18n.info.honestyPoints} ${_pointsText(score.honestyPoints)}", - style: context.textTheme.bodyLarge?.copyWith(color: _pointsColor(context, score.honestyPoints)), - ), - ]), - ), - subtitle: subtitle, - ); - } else if (score.points != 0) { - return ListTile( - titleTextStyle: context.textTheme.bodyLarge?.copyWith(color: _pointsColor(context, score.points)), - title: - (scoreType != null ? "${scoreType.l10nFullName()} ${_pointsText(score.points)}" : _pointsText(score.points)) - .text(), - subtitle: subtitle, - ); - } else if (score.honestyPoints != 0) { - return ListTile( - titleTextStyle: context.textTheme.bodyLarge?.copyWith(color: _pointsColor(context, score.honestyPoints)), - title: "${i18n.info.honestyPoints} ${_pointsText(score.honestyPoints)}".text(), - subtitle: subtitle, - ); - } else { - return ListTile( - title: "+0".text(), - subtitle: subtitle, - ); - } - } -} - -String _pointsText(double points) { - if (points > 0) { - return "+${points.toStringAsFixed(2)}"; - } else if (points == 0) { - return "+0"; - } else { - return points.toStringAsFixed(2); - } -} - -Color? _pointsColor(BuildContext ctx, double points) { - if (points > 0) { - return Colors.green; - } else if (points == 0) { - return null; - } else { - return ctx.$red$; - } -} diff --git a/lib/school/class2nd/page/details.dart b/lib/school/class2nd/page/details.dart deleted file mode 100644 index a09d29a52..000000000 --- a/lib/school/class2nd/page/details.dart +++ /dev/null @@ -1,330 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; -import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; -import 'package:sit/design/adaptive/dialog.dart'; -import 'package:sit/design/widgets/common.dart'; -import 'package:sit/design/widgets/list_tile.dart'; -import 'package:sit/design/widgets/tags.dart'; -import 'package:sit/l10n/extension.dart'; -import 'package:sit/settings/settings.dart'; -import 'package:sit/utils/error.dart'; -import 'package:sit/widgets/html.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:url_launcher/url_launcher_string.dart'; - -import '../entity/details.dart'; -import '../init.dart'; -import '../i18n.dart'; -import '../utils.dart'; - -String _getActivityUrl(int activityId) { - return 'http://sc.sit.edu.cn/public/activity/activityDetail.action?activityId=$activityId'; -} - -class Class2ndActivityDetailsPage extends StatefulWidget { - final int activityId; - final String? title; - final DateTime? time; - final bool enableApply; - - const Class2ndActivityDetailsPage({ - super.key, - required this.activityId, - this.title, - this.time, - this.enableApply = false, - }); - - @override - State createState() => _Class2ndActivityDetailsPageState(); -} - -class _Class2ndActivityDetailsPageState extends State { - int get activityId => widget.activityId; - late Class2ndActivityDetails? details = Class2ndInit.activityStorage.getActivityDetails(activityId); - bool isFetching = false; - - @override - void initState() { - super.initState(); - fetch(); - } - - Future fetch() async { - if (details != null) return; - setState(() { - isFetching = true; - }); - final data = await Class2ndInit.activityService.getActivityDetails(activityId); - await Class2ndInit.activityStorage.setActivityDetails(activityId, data); - setState(() { - details = data; - isFetching = false; - }); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - body: DefaultTabController( - length: 2, - child: NestedScrollView( - floatHeaderSlivers: true, - headerSliverBuilder: (context, innerBoxIsScrolled) { - return [ - SliverOverlapAbsorber( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), - sliver: SliverAppBar( - floating: true, - title: i18n.info.activityOf(activityId).text(), - actions: [ - if (widget.enableApply) - PlatformTextButton( - child: i18n.apply.btn.text(), - onPressed: () async { - await showApplyRequest(); - }, - ), - buildMoreActions(), - ], - forceElevated: innerBoxIsScrolled, - bottom: TabBar( - isScrollable: true, - tabs: [ - Tab(child: i18n.infoTab.text()), - Tab(child: i18n.descriptionTab.text()), - ], - ), - ), - ), - ]; - }, - body: TabBarView( - children: [ - ActivityDetailsInfoTabView(activityTitle: widget.title, activityTime: widget.time, details: details), - ActivityDetailsDocumentTabView(details: details), - ], - ), - ), - ), - bottomNavigationBar: isFetching - ? const PreferredSize( - preferredSize: Size.fromHeight(4), - child: LinearProgressIndicator(), - ) - : null, - ); - } - - Widget buildMoreActions() { - return PopupMenuButton( - position: PopupMenuPosition.under, - padding: EdgeInsets.zero, - itemBuilder: (ctx) => [ - PopupMenuItem( - child: ListTile( - leading: const Icon(Icons.open_in_browser), - title: "Open in browser".text(), - ), - onTap: () async { - await launchUrlString( - _getActivityUrl(activityId), - mode: LaunchMode.externalApplication, - ); - }, - ), - if (Settings.isDeveloperMode) - PopupMenuItem( - child: ListTile( - leading: const Icon(Icons.send), - title: "Forcibly apply".text(), - ), - onTap: () async { - await showForciblyApplyRequest(); - }, - ), - ], - ); - } - - Future showApplyRequest() async { - final confirm = await context.showRequest( - title: i18n.apply.applyRequest, - desc: i18n.apply.applyRequestDesc, - yes: i18n.confirm, - no: i18n.notNow, - highlight: true, - ); - if (confirm != true) return; - try { - final response = await Class2ndInit.applicationService.join(activityId); - if (!mounted) return; - await context.showTip(title: i18n.apply.replyTip, desc: response, ok: i18n.ok); - } catch (e) { - if (!mounted) return; - await context.showTip( - title: i18n.error, - desc: e.toString(), - ok: i18n.ok, - serious: true, - ); - rethrow; - } - } - - Future showForciblyApplyRequest() async { - final confirm = await context.showRequest( - title: "Forcibly apply", - desc: "Confirm to apply this activity forcibly?", - yes: i18n.confirm, - no: i18n.notNow, - highlight: true, - serious: true, - ); - if (confirm != true) return; - try { - final response = await Class2ndInit.applicationService.join(activityId, force: true); - if (!mounted) return; - await context.showTip(title: i18n.apply.replyTip, desc: response, ok: i18n.ok); - } catch (error, stackTrace) { - debugPrintError(error, stackTrace); - if (!mounted) return; - await context.showTip(title: i18n.apply.replyTip, desc: error.toString(), ok: i18n.ok); - rethrow; - } - } -} - -class ActivityDetailsInfoTabView extends StatefulWidget { - final String? activityTitle; - final DateTime? activityTime; - final Class2ndActivityDetails? details; - - const ActivityDetailsInfoTabView({ - super.key, - this.activityTitle, - this.activityTime, - this.details, - }); - - @override - State createState() => _ActivityDetailsInfoTabViewState(); -} - -class _ActivityDetailsInfoTabViewState extends State with AutomaticKeepAliveClientMixin { - @override - bool get wantKeepAlive => true; - - @override - Widget build(BuildContext context) { - super.build(context); - final details = widget.details; - final (:title, :tags) = separateTagsFromTitle(widget.activityTitle ?? details?.title ?? ""); - final time = details?.startTime ?? widget.activityTime; - return SelectionArea( - child: CustomScrollView( - slivers: [ - SliverList.list(children: [ - DetailListTile( - title: i18n.info.name, - subtitle: title, - ), - if (time != null) - DetailListTile( - title: i18n.info.startTime, - subtitle: context.formatYmdhmNum(time), - ), - if (details != null) ...[ - if (details.place != null) - DetailListTile( - title: i18n.info.location, - subtitle: details.place!, - ), - if (details.principal != null) - DetailListTile( - title: i18n.info.principal, - subtitle: details.principal!, - ), - if (details.organizer != null) - DetailListTile( - title: i18n.info.organizer, - subtitle: details.organizer!, - ), - if (details.undertaker != null) - DetailListTile( - title: i18n.info.undertaker, - subtitle: details.undertaker!, - ), - if (details.contactInfo != null) - DetailListTile( - title: i18n.info.contactInfo, - subtitle: details.contactInfo!, - ), - if (tags.isNotEmpty) - ListTile( - isThreeLine: true, - title: i18n.info.tags.text(), - subtitle: TagsGroup(tags), - ), - DetailListTile( - title: i18n.info.signInTime, - subtitle: context.formatYmdhmNum(details.signStartTime), - ), - DetailListTile( - title: i18n.info.signOutTime, - subtitle: context.formatYmdhmNum(details.signEndTime), - ), - if (details.duration != null) - DetailListTile( - title: i18n.info.duration, - subtitle: details.duration!, - ), - ], - ]), - ], - ), - ); - } -} - -class ActivityDetailsDocumentTabView extends StatefulWidget { - final Class2ndActivityDetails? details; - - const ActivityDetailsDocumentTabView({ - super.key, - this.details, - }); - - @override - State createState() => _ActivityDetailsDocumentTabViewState(); -} - -class _ActivityDetailsDocumentTabViewState extends State - with AutomaticKeepAliveClientMixin { - @override - bool get wantKeepAlive => true; - - @override - Widget build(BuildContext context) { - super.build(context); - final description = widget.details?.description; - return SelectionArea( - child: CustomScrollView( - slivers: [ - if (description == null) - SliverToBoxAdapter( - child: LeavingBlank( - icon: Icons.inbox_outlined, - desc: i18n.noDetails, - ), - ) - else - SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 8), - sliver: RestyledHtmlWidget(description, renderMode: RenderMode.sliverList), - ) - ], - ), - ); - } -} diff --git a/lib/school/class2nd/service/activity.dart b/lib/school/class2nd/service/activity.dart deleted file mode 100644 index aa60635ad..000000000 --- a/lib/school/class2nd/service/activity.dart +++ /dev/null @@ -1,143 +0,0 @@ -import 'package:beautiful_soup_dart/beautiful_soup.dart'; -import 'package:dio/dio.dart'; -import 'package:sit/init.dart'; - -import 'package:sit/school/utils.dart'; -import 'package:sit/session/class2nd.dart'; - -import '../entity/details.dart'; -import '../entity/list.dart'; -import "package:intl/intl.dart"; - -class Class2ndActivityService { - static final re = RegExp(r"(\d){7}"); - static final _spacesRx = RegExp(r'\s{2}\s+'); - static final dateFormatParser = DateFormat('yyyy-MM-dd hh:mm:ss'); - - Class2ndSession get session => Init.class2ndSession; - - const Class2ndActivityService(); - - String generateUrl(Class2ndActivityCat category, int page, [int pageSize = 20]) { - return 'http://sc.sit.edu.cn/public/activity/activityList.action?pageNo=$page&pageSize=$pageSize&categoryId=${category.id}'; - } - - /// 获取第二课堂活动列表 - Future> getActivityList(Class2ndActivityCat type, int page) async { - final url = generateUrl(type, page); - final response = await session.request( - url, - options: Options( - method: "GET", - ), - ); - return _parseActivityList(response.data); - } - - Future> query(String queryString) async { - const String url = 'http://sc.sit.edu.cn/public/activity/activityList.action'; - final response = await session.request( - url, - data: 'activityName=$queryString', - options: Options( - contentType: Headers.formUrlEncodedContentType, - method: "POST", - ), - ); - - return _parseActivityList(response.data); - } - - static List _parseActivityList(String htmlPage) { - final BeautifulSoup soup = BeautifulSoup(htmlPage); - List result = soup.findAll('.ul_7 li > a').map( - (element) { - final date = element.nextSibling!.text; - final String fullTitle = mapChinesePunctuations(element.text.substring(2)); - final String link = element.attributes['href']!; - - final String? x = re.firstMatch(link)?.group(0).toString(); - final int id = int.parse(x!); - return Class2ndActivity( - id: id, - title: fullTitle, - time: dateFormatParser.parse(date), - ); - }, - ).toList(); - return result; - } - - /// 获取第二课堂活动详情 - Future getActivityDetails(int activityId) async { - final response = await session.request( - 'http://sc.sit.edu.cn/public/activity/activityDetail.action?activityId=$activityId', - options: Options( - method: "POST", - ), - ); - final data = response.data; - return _parseActivityDetails(data); - } - - static String _cleanText(String banner) { - String result = banner.replaceAll(' ', ' ').replaceAll('
', ''); - return result.replaceAll(_spacesRx, '\n'); - } - - static Map _splitActivityProperties(String banner) { - String cleanText = _cleanText(banner); - List lines = cleanText.split('\n'); - lines.removeLast(); - final map = {}; - for (String line in lines) { - List result = line.split(':'); - map.addAll({result[0]: result[1]}); - } - return map; - } - - static DateTime _parseDateTime(String dateTime) { - var dateFormat = DateFormat('yyyy-MM-dd hh:mm:ss'); - return dateFormat.parse(dateTime); - } - - static List _parseSignTime(String value) { - List time = value.split(' --至-- '); - return [_parseDateTime(time[0]), _parseDateTime(time[1])]; - } - - static Class2ndActivityDetails _parseProperties(Bs4Element item) { - String title = item.findAll('h1').map((e) => e.innerHtml.trim()).elementAt(0); - String description = - item.findAll('div[style="padding:30px 50px; font-size:14px;"]').map((e) => e.innerHtml.trim()).elementAt(0); - String banner = - item.findAll('div[style=" color:#7a7a7a; text-align:center"]').map((e) => e.innerHtml.trim()).elementAt(0); - - final properties = _splitActivityProperties(banner); - final signTime = _parseSignTime(properties['刷卡时间段']!); - - return Class2ndActivityDetails( - id: int.parse(properties['活动编号']!), - title: mapChinesePunctuations(title), - startTime: _parseDateTime(properties['活动开始时间']!), - signStartTime: signTime[0], - signEndTime: signTime[1], - place: properties['活动地点'], - duration: properties['活动时长'], - principal: properties['负责人'], - contactInfo: properties['负责人电话'], - organizer: properties['主办方'], - undertaker: properties['承办方'], - description: description, - ); - } - - static Class2ndActivityDetails _parseActivityDetails(String htmlPage) { - final BeautifulSoup soup = BeautifulSoup(htmlPage); - final frame = soup.find('.box-1'); - final detail = _parseProperties(frame!); - - return detail; - } -} diff --git a/lib/school/class2nd/service/application.dart b/lib/school/class2nd/service/application.dart deleted file mode 100644 index d481d2cde..000000000 --- a/lib/school/class2nd/service/application.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:dio/dio.dart'; -import 'package:sit/init.dart'; - -import 'package:sit/session/class2nd.dart'; - -class Class2ndApplicationService { - static const _codeMessage = [ - '检查成功', - '您的个人信息不全,请补全您的信息!', - '您已申请过该活动,不能重复申请!', - '对不起,您今天的申请次数已达上限!', - '对不起,该活动的申请人数已达上限!', - '对不起,该活动已过期并停止申请!', - '您已申请过该时间段的活动,不能重复申请!', - '对不起,您不能申请该活动!', - '对不起,您不在该活动的范围内!', - ]; - - Class2ndSession get session => Init.class2ndSession; - - const Class2ndApplicationService(); - - /// 提交最后的活动申请 - Future _sendFinalRequest(int activityId) async { - final res = await session.request( - 'http://sc.sit.edu.cn/public/pcenter/applyActivity.action?activityId=$activityId', - options: Options( - method: "GET", - ), - ); - return res.data as String; - } - - Future _sendCheckRequest(int activityId) async { - final res = await session.request( - 'http://sc.sit.edu.cn/public/pcenter/check.action?activityId=$activityId', - options: Options( - method: "GET", - ), - ); - final code = (res.data as String).trim(); - - return _codeMessage[int.parse(code)]; - } - - /// 参加活动 - Future join( - int activityId, { - bool force = false, - }) async { - if (!force) { - final result = await _sendCheckRequest(activityId); - if (result != '检查成功') { - return result; - } - } - final result = await _sendFinalRequest(activityId); - return result.contains('申请成功') ? '申请成功' : '申请失败'; - } -} diff --git a/lib/school/class2nd/service/points.dart b/lib/school/class2nd/service/points.dart deleted file mode 100644 index 2790514cd..000000000 --- a/lib/school/class2nd/service/points.dart +++ /dev/null @@ -1,231 +0,0 @@ -import 'package:beautiful_soup_dart/beautiful_soup.dart'; -import 'package:dio/dio.dart'; -import 'package:intl/intl.dart'; -import 'package:sit/init.dart'; - -import 'package:sit/school/utils.dart'; -import 'package:sit/session/class2nd.dart'; - -import '../entity/list.dart'; -import '../entity/attended.dart'; - -class Class2ndPointsService { - static const homeUrl = 'http://sc.sit.edu.cn/public/init/index.action'; - static const scoreUrl = 'http://sc.sit.edu.cn/public/pcenter/scoreDetail.action'; - static const myEventUrl = 'http://sc.sit.edu.cn/public/pcenter/activityOrderList.action?pageSize=999'; - - Class2ndSession get session => Init.class2ndSession; - - const Class2ndPointsService(); - - /// 获取第二课堂分数 - Future fetchScoreSummary() async { - final response = await session.request( - homeUrl, - options: Options( - method: "POST", - ), - ); - final data = response.data; - return _parseScoreSummary(data); - } - - static Class2ndPointsSummary _parseScoreSummary(String htmlPage) { - final html = BeautifulSoup(htmlPage); - - final (:thematicReport, :practice, :creation, :schoolCulture, :schoolSafetyCivilization, :voluntary) = - _parseAllStatus(html); - - final honestyPoints = _parseHonestyPoints(html); - final total = _parseTotalPoints(html); - return Class2ndPointsSummary( - thematicReport: thematicReport, - practice: practice, - creation: creation, - schoolSafetyCivilization: schoolSafetyCivilization, - voluntary: voluntary, - schoolCulture: schoolCulture, - honestyPoints: honestyPoints, - totalPoints: total, - ); - } - - static ({ - double thematicReport, - double practice, - double creation, - double schoolSafetyCivilization, - double voluntary, - double schoolCulture, - }) _parseAllStatus(BeautifulSoup html) { - // 学分=1.5(主题报告)+2.0(社会实践)+1.5(创新创业创意)+1.0(校园安全文明)+0.0(公益志愿)+2.0(校园文化) - final found = html.find('#span_score')!; - final String scoreText = found.text; - final regExp = RegExp(r'([\d.]+)\(([\u4e00-\u9fa5]+)\)'); - - late final double lecture, practice, creation, safetyEdu, voluntary, campus; - - final matches = regExp.allMatches(scoreText); - for (final item in matches) { - final score = double.parse(item.group(1) ?? '0.0'); - final typeName = item.group(2)!; - final type = Class2ndPointType.parse(typeName); - - switch (type) { - case Class2ndPointType.thematicReport: - lecture = score; - break; - case Class2ndPointType.creation: - creation = score; - break; - case Class2ndPointType.schoolCulture: - campus = score; - break; - case Class2ndPointType.practice: - practice = score; - break; - case Class2ndPointType.voluntary: - voluntary = score; - break; - case Class2ndPointType.schoolSafetyCivilization: - safetyEdu = score; - break; - case null: - break; - } - } - return ( - thematicReport: lecture, - practice: practice, - creation: creation, - schoolSafetyCivilization: safetyEdu, - voluntary: voluntary, - schoolCulture: campus, - ); - } - - static final honestyPointsRe = RegExp(r'诚信积分:(\S+)'); - - static double _parseHonestyPoints(BeautifulSoup html) { - final element = html.find("div", attrs: {"onmouseover": "showSynopsis()"}); - var honestyPoints = 0.0; - if (element != null) { - final pointsRaw = honestyPointsRe.firstMatch(element.text)?.group(1); - if (pointsRaw != null) { - final points = double.tryParse(pointsRaw); - if (points != null) { - honestyPoints = points; - } - } - } - return honestyPoints; - } - - static const totalPoints = '#content-box > div.user-info > div:nth-child(3) > font'; - - static double _parseTotalPoints(BeautifulSoup html) { - final pointsRaw = html.find(totalPoints); - if (pointsRaw != null) { - final total = double.tryParse(pointsRaw.text); - if (total != null) { - return total; - } - } - return 0.0; - } - - /// 获取我的得分列表 - Future> fetchScoreItemList() async { - final response = await session.request( - scoreUrl, - options: Options( - method: "POST", - ), - ); - return _parseScoreList(response.data); - } - - static final scoreItemTimeFormat = DateFormat('yyyy-MM-dd hh:mm'); - - static List _parseScoreList(String htmlPage) { - Class2ndPointItem nodeToScoreItem(Bs4Element item) { - final title = item.find('td:nth-child(3)')!.text.trim(); - final timeRaw = item.find('td:nth-child(9) > a'); - final time = timeRaw == null ? null : scoreItemTimeFormat.parse(timeRaw.text.trim()); - final idRaw = item.find('td:nth-child(7)'); - final id = int.parse(idRaw!.innerHtml.trim()); - // 注意:“我的成绩” 页面中,成绩条目显示的是活动类型,而非加分类型, 因此使用 ActivityType. - final categoryRaw = item.find('td:nth-child(5)')!.innerHtml.trim(); - final category = Class2ndActivityCat.parse(categoryRaw); - assert(category != null, "Unknown class2nd category $categoryRaw"); - final points = double.parse(item.find('td:nth-child(11) > span')!.innerHtml.trim()); - final honestyPointsRaw = item.find('td:nth-child(13) > span')!.innerHtml.trim(); - final honestyPoints = honestyPointsRaw.startsWith("+-") - ? double.parse(honestyPointsRaw.substring(1)) - : double.parse(honestyPointsRaw); - - return Class2ndPointItem( - name: mapChinesePunctuations(title), - activityId: id, - category: category!, - time: time, - points: points, - honestyPoints: honestyPoints, - ); - } - - return BeautifulSoup(htmlPage) - .findAll('#div1 > div.table_style_4 > form > table:nth-child(7) > tbody > tr') - .map(nodeToScoreItem) - .toList(); - } - - /// 获取我的活动列表 - Future> fetchActivityApplicationList() async { - final response = await session.request( - myEventUrl, - options: Options( - method: "POST", - ), - ); - return _parseActivityApplicationList(response.data); - } - - static List _parseActivityApplicationList(String htmlPage) { - final html = BeautifulSoup(htmlPage); - return html - .findAll('#content-box > div:nth-child(23) > div.table_style_4 > form > table > tbody > tr') - .map((e) => _activityMapDetail(e)) - .where(filterDeletedActivity) - .toList(); - } - - static bool filterDeletedActivity(Class2ndActivityApplication x) => x.activityId != 0; - - static final attendedTimeFormat = DateFormat('yyyy-MM-dd hh:mm:ss'); - static final activityIdRe = RegExp(r'activityId=(\d+)'); - - static Class2ndActivityApplication _activityMapDetail(Bs4Element item) { - final applyIdText = item.find('td:nth-child(1)')!.text.trim(); - final applicationId = int.parse(applyIdText); - final activityIdText = item.find('td:nth-child(3)')!.innerHtml.trim(); - // 部分取消了的活动,活动链接不存在,这里将活动 id 记为 -1. - final activityId = int.parse(activityIdRe.firstMatch(activityIdText)?.group(1) ?? '-1'); - final title = item.find('td:nth-child(3)')!.text.trim(); - final categoryRaw = item.find('td:nth-child(5)')!.text.trim(); - final category = Class2ndActivityCat.parse(categoryRaw); - assert(category != null, "Unknown class2nd category $categoryRaw"); - final timeRaw = item.find('td:nth-child(7)')!.text.trim(); - final time = attendedTimeFormat.parse(timeRaw); - final status = item.find('td:nth-child(9)')!.text.trim(); - - return Class2ndActivityApplication( - applicationId: applicationId, - activityId: activityId, - title: mapChinesePunctuations(title), - category: category!, - time: time, - status: status, - ); - } -} diff --git a/lib/school/class2nd/storage/activity.dart b/lib/school/class2nd/storage/activity.dart deleted file mode 100644 index a341bbdb2..000000000 --- a/lib/school/class2nd/storage/activity.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:hive/hive.dart'; -import 'package:sit/storage/hive/init.dart'; - -import '../entity/details.dart'; -import '../entity/list.dart'; - -class _K { - static String activity(int id) => '/activities/$id'; - - static String activityDetails(int id) => '/activityDetails/$id'; - - static String activityIdList(Class2ndActivityCat type) => '/activityIdList/$type'; -} - -class Class2ndActivityStorage { - Box get box => HiveInit.class2nd; - - const Class2ndActivityStorage(); - - List? getActivityIdList(Class2ndActivityCat type) => box.get(_K.activityIdList(type)); - - Future setActivityIdList(Class2ndActivityCat type, List? activityIdList) => - box.put(_K.activityIdList(type), activityIdList); - - Class2ndActivity? getActivity(int id) => box.get(_K.activity(id)); - - Future setActivity(int id, Class2ndActivity? activity) => box.put(_K.activity(id), activity); - - Class2ndActivityDetails? getActivityDetails(int id) => box.get(_K.activityDetails(id)); - - Future setActivityDetails(int id, Class2ndActivityDetails? details) => box.put(_K.activityDetails(id), details); - - List? getActivities(Class2ndActivityCat type) { - final idList = getActivityIdList(type); - if (idList == null) return null; - final res = []; - for (final id in idList) { - final activity = getActivity(id); - if (activity != null) { - res.add(activity); - } - } - return res; - } - - Future? setActivities(Class2ndActivityCat type, List? activities) async { - if (activities == null) { - await setActivities(type, null); - } else { - await setActivityIdList(type, activities.map((e) => e.id).toList(growable: false)); - for (final activity in activities) { - await setActivity(activity.id, activity); - } - } - } -} diff --git a/lib/school/class2nd/storage/points.dart b/lib/school/class2nd/storage/points.dart deleted file mode 100644 index eeb59e9b2..000000000 --- a/lib/school/class2nd/storage/points.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:hive_flutter/hive_flutter.dart'; -import 'package:sit/storage/hive/init.dart'; - -import '../entity/attended.dart'; - -class _K { - static const pointsSummary = "/pointsSummary"; - static const pointItemList = "/pointItemList"; - static const applicationList = "/applicationList"; -} - -class Class2ndPointsStorage { - Box get box => HiveInit.class2nd; - - const Class2ndPointsStorage(); - - Class2ndPointsSummary? get pointsSummary => box.get(_K.pointsSummary); - - set pointsSummary(Class2ndPointsSummary? newValue) => box.put(_K.pointsSummary, newValue); - - ValueListenable listenPointsSummary() => box.listenable(keys: [_K.pointsSummary]); - - List? get pointItemList => (box.get(_K.pointItemList) as List?)?.cast(); - - set pointItemList(List? newValue) => box.put(_K.pointItemList, newValue); - - List? get applicationList => - (box.get(_K.applicationList) as List?)?.cast(); - - set applicationList(List? newValue) => box.put(_K.applicationList, newValue); -} diff --git a/lib/school/class2nd/utils.dart b/lib/school/class2nd/utils.dart deleted file mode 100644 index c6268febb..000000000 --- a/lib/school/class2nd/utils.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:sit/school/class2nd/entity/list.dart'; - -import 'entity/attended.dart'; - -final _tagParenthesesRegx = RegExp(r"\[(.*?)\]"); - -({String title, List tags}) separateTagsFromTitle(String full) { - if (full.isEmpty) return (title: "", tags: []); - final allMatched = _tagParenthesesRegx.allMatches(full); - final resultTags = []; - for (final matched in allMatched) { - final tag = matched.group(1); - if (tag != null) { - final tags = tag.split("&"); - for (final tag in tags) { - resultTags.add(tag.trim()); - } - } - } - final title = full.replaceAll(_tagParenthesesRegx, ""); - return (title: title, tags: resultTags.toSet().toList()); -} - -const commonClass2ndCategories = [ - Class2ndActivityCat.lecture, - Class2ndActivityCat.creation, - Class2ndActivityCat.thematicEdu, - Class2ndActivityCat.practice, - Class2ndActivityCat.voluntary, - Class2ndActivityCat.schoolCultureActivity, - Class2ndActivityCat.schoolCultureCompetition, -]; - -List buildAttendedActivityList({ - required List applications, - required List scores, -}) { - final attended = applications.map((application) { - final relatedScoreItems = scores.where((e) => e.activityId == application.activityId).toList(); - return Class2ndAttendedActivity( - application: application, - scores: relatedScoreItems, - ); - }).toList(); - return attended; -} diff --git a/lib/school/class2nd/widgets/activity.dart b/lib/school/class2nd/widgets/activity.dart deleted file mode 100644 index 81d235a3c..000000000 --- a/lib/school/class2nd/widgets/activity.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:sit/design/widgets/card.dart'; -import 'package:sit/design/widgets/tags.dart'; -import 'package:sit/l10n/extension.dart'; -import 'package:rettulf/rettulf.dart'; - -import '../entity/list.dart'; -import '../utils.dart'; - -class ActivityCard extends StatelessWidget { - final Class2ndActivity activity; - final VoidCallback? onTap; - - const ActivityCard( - this.activity, { - super.key, - this.onTap, - }); - - @override - Widget build(BuildContext context) { - final textTheme = context.textTheme; - final (:title, :tags) = separateTagsFromTitle(activity.title); - return FilledCard( - clip: Clip.hardEdge, - child: ListTile( - isThreeLine: true, - title: title.text(), - titleTextStyle: textTheme.titleMedium, - trailing: context.formatYmdNum(activity.time).text(style: textTheme.bodyMedium), - subtitle: TagsGroup(tags), - onTap: onTap, - ), - ); - } -} diff --git a/lib/school/class2nd/widgets/search.dart b/lib/school/class2nd/widgets/search.dart deleted file mode 100644 index 73b1371eb..000000000 --- a/lib/school/class2nd/widgets/search.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:sit/design/adaptive/foundation.dart'; -import 'package:sit/design/widgets/common.dart'; - -import '../entity/list.dart'; -import '../init.dart'; -import '../i18n.dart'; -import '../page/details.dart'; -import 'activity.dart'; - -class ActivitySearchDelegate extends SearchDelegate { - @override - List? buildActions(BuildContext context) { - return [ - IconButton(onPressed: () => query = '', icon: const Icon(Icons.clear)), - ]; - } - - @override - Widget? buildLeading(BuildContext context) { - return null; - } - - @override - Widget buildResults(BuildContext context) { - return _ActivityAsyncSearchList( - query: query, - ); - } - - @override - Widget buildSuggestions(BuildContext context) { - return Container(); - } -} - -class _ActivityAsyncSearchList extends StatefulWidget { - final String query; - - const _ActivityAsyncSearchList({ - super.key, - required this.query, - }); - - @override - State<_ActivityAsyncSearchList> createState() => _ActivityAsyncSearchListState(); -} - -class _ActivityAsyncSearchListState extends State<_ActivityAsyncSearchList> { - List? activityList; - - @override - void initState() { - super.initState(); - load(); - } - - Future load() async { - final result = await Class2ndInit.activityService.query(widget.query); - setState(() { - activityList = result; - }); - } - - @override - Widget build(BuildContext context) { - final activityList = this.activityList; - return CustomScrollView( - slivers: [ - if (activityList != null) - if (activityList.isNotEmpty) - SliverList.builder( - itemCount: activityList.length, - itemBuilder: (ctx, i) { - final activity = activityList[i]; - return ActivityCard( - activity, - onTap: () async { - await context.show$Sheet$((ctx) => Class2ndActivityDetailsPage( - activityId: activity.id, - title: activity.title, - time: activity.time, - enableApply: true, - )); - }, - ); - }, - ) - else - SliverFillRemaining( - child: LeavingBlank( - icon: Icons.inbox_outlined, - desc: i18n.noActivities, - ), - ), - ], - ); - } -} diff --git a/lib/school/class2nd/widgets/summary.dart b/lib/school/class2nd/widgets/summary.dart deleted file mode 100644 index 24210cf27..000000000 --- a/lib/school/class2nd/widgets/summary.dart +++ /dev/null @@ -1,166 +0,0 @@ -import 'package:fl_chart/fl_chart.dart'; -import 'package:flutter/material.dart'; -import 'package:rettulf/rettulf.dart'; - -import '../entity/attended.dart'; - -const _targetScores2019 = Class2ndPointsSummary( - thematicReport: 1.5, - practice: 2, - creation: 1.5, - schoolSafetyCivilization: 1, - voluntary: 1, - schoolCulture: 1, -); -const _admissionYear2targetScores = { - 2013: Class2ndPointsSummary(thematicReport: 1, schoolCulture: 1), - 2014: Class2ndPointsSummary(thematicReport: 1, practice: 1, schoolCulture: 1), - 2015: Class2ndPointsSummary(thematicReport: 1, practice: 1, creation: 1, schoolCulture: 1), - 2016: Class2ndPointsSummary(thematicReport: 1, practice: 1, creation: 1, schoolCulture: 1), - 2017: Class2ndPointsSummary( - thematicReport: 1.5, - practice: 2, - creation: 1.5, - schoolSafetyCivilization: 1, - schoolCulture: 2, - ), - 2018: Class2ndPointsSummary( - thematicReport: 1.5, - practice: 2, - creation: 1.5, - schoolSafetyCivilization: 1, - schoolCulture: 2, - ), - 2019: _targetScores2019, - 2020: _targetScores2019, - 2021: _targetScores2019, - 2022: _targetScores2019, - 2023: _targetScores2019, -}; - -Class2ndPointsSummary getTargetScoreOf({required int? admissionYear}) { - return _admissionYear2targetScores[admissionYear] ?? _targetScores2019; -} - -class Class2ndScoreSummeryCard extends StatelessWidget { - final Class2ndPointsSummary targetScore; - final Class2ndPointsSummary summary; - final double aspectRatio; - - const Class2ndScoreSummeryCard({ - super.key, - required this.targetScore, - required this.summary, - this.aspectRatio = 16 / 9, - }); - - @override - Widget build(BuildContext context) { - return AspectRatio( - aspectRatio: aspectRatio, - child: Card( - child: Class2ndScoreSummaryChart(targetScore: targetScore, summary: summary).padSymmetric(v: 4), - ), - ); - } -} - -class Class2ndScoreSummaryChart extends StatelessWidget { - final Class2ndPointsSummary targetScore; - final Class2ndPointsSummary summary; - - const Class2ndScoreSummaryChart({ - super.key, - required this.targetScore, - required this.summary, - }); - - @override - Widget build(BuildContext context) { - final scores = summary.toName2score(); - final targetScores = targetScore.toName2score(); - final barColor = context.colorScheme.primary; - final completeStyle = TextStyle(color: context.colorScheme.primary); - List values = []; - for (var i = 0; i < scores.length; i++) { - // Skip empty targets to prevent zero-division error. - if (targetScores[i].score == 0) continue; - final pct = scores[i].score / targetScores[i].score; - values.add(BarChartGroupData(x: i, barRods: [ - BarChartRodData( - toY: pct, - width: 12, - color: barColor, - ), - ])); - } - return BarChart( - BarChartData( - maxY: 1, - barGroups: values, - borderData: FlBorderData(show: false), - gridData: const FlGridData(show: false), - barTouchData: BarTouchData(enabled: false), - titlesData: FlTitlesData( - show: true, - leftTitles: const AxisTitles(), - topTitles: AxisTitles( - sideTitles: SideTitles( - showTitles: true, - getTitlesWidget: (double indexDouble, TitleMeta meta) { - final i = indexDouble.toInt(); - final isComplete = scores[i].score / targetScores[i].score >= 1; - return targetScores[i].score.toString().text(style: isComplete ? completeStyle : null); - }, - ), - ), - rightTitles: const AxisTitles(), - bottomTitles: AxisTitles( - sideTitles: SideTitles( - showTitles: true, - reservedSize: 44, - getTitlesWidget: (double indexDouble, TitleMeta meta) { - final i = indexDouble.toInt(); - final isComplete = scores[i].score / targetScores[i].score >= 1; - return Tooltip( - triggerMode: TooltipTriggerMode.tap, - message: scores[i].type.l10nFullName(), - child: [ - scores[i].type.l10nShortName().text(style: isComplete ? completeStyle : null), - scores[i].score.toString().text(style: isComplete ? completeStyle : null), - ].column(), - ); - }, - ), - ), - ), - ), - ); - } -} - -// syncfusion_flutter_charts: ^22.2.12 -// SfCartesianChart( -// primaryXAxis: CategoryAxis( -// majorGridLines: const MajorGridLines(width: 0), -// axisLine: const AxisLine(width: 0), -// ), -// primaryYAxis: NumericAxis( -// minimum: 0, -// maximum: 1, -// numberFormat: NumberFormat.percentPattern(), -// majorGridLines: const MajorGridLines(width: 0), -// axisLine: const AxisLine(width: 0), -// ), -// series: >[ -// BarSeries<({String name, double score}), String>( -// borderRadius: BorderRadius.circular(4), -// dataSource: scoreValues, -// xValueMapper: (data, _) => data.name, -// dataLabelMapper: (data, i) => "${data.score}/${targetScores[i].score}".toString(), -// yValueMapper: (data, i) => data.score / targetScores[i].score, -// color: context.colorScheme.primary, -// dataLabelSettings: const DataLabelSettings(isVisible: true), -// ) -// ], -// ); diff --git a/lib/school/entity/icon.dart b/lib/school/entity/icon.dart deleted file mode 100644 index a3ccfa6a7..000000000 --- a/lib/school/entity/icon.dart +++ /dev/null @@ -1,104 +0,0 @@ -class CourseCategory { - static const Map> _courseToCategory = { - 'art': [ - '手绘', - '速写', - '插图', - '图文', - '创作', - '摄影', - '构图', - '美术', - '水彩', - '油画', - '素描', - '艺术', - '雕塑', - '装饰', - '写生', - '技法', - '视觉', - '漆艺', - 'UI', - '广告', - '美学' - ], - 'biological': ['生物', '环境', '花卉', '药物', '微生物', '材料'], - 'history': ['文化', '历史', '设计史', '书画', '文明史'], - 'building': ['建筑', '轨道', '铁道', '桥梁', '结构力学', '房屋', '建材', '工程', '混凝土', '建设'], - 'chemical': ['化学', '传热学', '仪器', '药剂', '腐蚀', '制药', '化妆品', '酒', '香精', '聚合物', '水质', '药理'], - 'engineering': ['测量', '力学', '光谱', '检测', '有限单元'], - 'practice': ['实习', '实训', '营销', '就业', '实践', '职业'], - 'circuit': ['电工', '电磁', '电子', '信号', '数码', '数字', '嵌入式', 'EDA', '单片机'], - 'computer': [ - 'Python', - '计算机', - '程序设计', - '软件', - 'web', - '开发', - '建模', - '非线性编辑', - '微机', - '图形', - '操作系统', - '数据结构', - 'C语言', - '编译', - '人工智能' - ], - 'control': ['控制', '半导体', '泵', '电源', '系统', '故障诊断', '接触网', '维修', '液压', '气压', '汽轮机'], - 'experiment': ['特效', '会展', '实验', '活性剂', '光学'], - 'electricity': ['化工', '给水', '燃烧', '管网', '热工', '玻璃', '固废', '发电厂'], - 'music': ['音频', '音乐', '产品设计'], - 'social': ['园林'], - 'geography': ['生态', '一带一路', '大气污染', '地理'], - 'economic': ['估价', '贸易', '会计', '经济', '货币'], - 'physical': ['土壤', '国际', '物理'], - 'design': ['规划', '园艺', '线造型', '制图', '设计', 'Design', 'CAD'], - 'mechanical': ['工艺', '设备', '装备', '机械', '机电', '金属', '钢'], - 'sports': ['篮球'], - 'internship': ['香原料', '美容', '品评', '社区', '心理', '采编', '招聘', '妇女'], - 'political': ['珠宝'], - 'running': ['体育'], - 'language': ['英语', '德语', '语言', '法语', '日语', '英文', '英汉', '专业外语'], - 'ideological': ['思想道德', '毛泽东', '法治', '近现代史', '马克思', '政策', '政治'], - 'reading': ['植物', '信息论'], - 'management': ['食品', '管理', '项目', '关系', '安全', '行为', '社会'], - 'training': ['通信', '网络', '物联网', '文献检索'], - 'business': ['多媒体', '动画', '审计', '企业'], - 'statistical': ['数据分析', '数据挖掘', '数据库', '大数据', '市场', '调研', '证券', '统计'], - 'mathematics': ['计算', '复变函数', '概率论', '积分', '数学', '代数'], - 'technology': ['空调', '技术', '科学', '科技'], - 'generality': ['书籍'], - 'literature': ['文学', '编辑', '新闻', '报刊'], - 'curriculum': ['论文'], - }; - - static const _fallbackCat = "principle"; - - static String query(String curriculum, {String fallback = _fallbackCat}) { - for (var title in _courseToCategory.keys) { - for (var item in _courseToCategory[title]!) { - if (curriculum.contains(item)) { - return title; - } - } - } - return fallback; - } - - static const String _courseIconDir = 'assets/course'; - - static String iconPathOf({String? courseName, String? iconName}) { - final String icon; - if (iconName != null && _courseToCategory.containsKey(iconName)) { - icon = iconName; - } else if (courseName != null) { - icon = query(courseName); - } else { - icon = _fallbackCat; - } - return "$_courseIconDir/$icon.png"; - } -} diff --git a/lib/school/entity/school.dart b/lib/school/entity/school.dart deleted file mode 100644 index ded391b00..000000000 --- a/lib/school/entity/school.dart +++ /dev/null @@ -1,149 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:json_annotation/json_annotation.dart'; -import 'package:sit/storage/hive/type_id.dart'; - -part 'school.g.dart'; - -typedef SchoolYear = int; - -@HiveType(typeId: CacheHiveType.semester) -@JsonEnum() -enum Semester implements Comparable { - @HiveField(0) - all, - @HiveField(1) - term1, - @HiveField(2) - term2; - - String l10n() => "school.semester.$name".tr(); - - @override - int compareTo(Semester other) { - if (this == other) return 0; - if (this == term1) return -1; - return 1; - } -} - -@HiveType(typeId: CacheHiveType.semesterInfo) -class SemesterInfo implements Comparable { - /// null means all school year - @HiveField(0) - final SchoolYear? year; - @HiveField(1) - final Semester semester; - - const SemesterInfo({ - required this.year, - required this.semester, - }); - - static const all = SemesterInfo(year: null, semester: Semester.all); - - bool get exactlyOne => year != null && semester != Semester.all; - - SchoolYear get exactYear { - assert(exactlyOne); - return year ?? DateTime.now().year; - } - - @override - String toString() { - return "$year:$semester"; - } - - // TODO: l10n - String l10n() { - final year = this.year; - if (year == null) { - return "All year ${semester.l10n()}"; - } else { - return "$year ${year + 1} ${semester.l10n()}"; - } - } - - @override - bool operator ==(Object other) { - return other is SemesterInfo && - runtimeType == other.runtimeType && - year == other.year && - semester == other.semester; - } - - @override - int get hashCode => Object.hash(year, semester); - - @override - int compareTo(SemesterInfo other) { - final yearA = year; - final yearB = other.year; - if (yearA != yearB) { - if (yearA == null) return 1; - if (yearB == null) return -1; - return yearA.compareTo(yearB); - } - if (semester != other.semester) { - if (semester == Semester.all) return 1; - if (other.semester == Semester.all) return -1; - return semester.compareTo(other.semester); - } - return 0; - } -} - -String semesterToFormField(Semester semester) { - const mapping = { - Semester.all: '', - Semester.term1: '3', - Semester.term2: '12', - }; - return mapping[semester]!; -} - -@HiveType(typeId: CacheHiveType.courseCat) -enum CourseCat { - @HiveField(0) - none, - - /// 通识课 - @HiveField(1) - genEd, - - /// 公共基础课 - @HiveField(2) - publicCore, - - /// 学科专业基础课 - @HiveField(3) - specializedCore, - - /// 专业必修课 - @HiveField(4) - specializedCompulsory, - - /// 专业选修课 - @HiveField(5) - specializedElective, - - /// 综合实践 - @HiveField(6) - integratedPractice, - - /// 实践教学 - @HiveField(7) - practicalInstruction; - - static CourseCat parse(String? str) { - return switch (str) { - "通识课" => genEd, - "公共基础课" => publicCore, - "学科专业基础课" => specializedCore, - "专业必修课" => specializedCompulsory, - "专业选修课" => specializedElective, - "综合实践" => integratedPractice, - "实践教学" => practicalInstruction, - _ => none, - }; - } -} diff --git a/lib/school/entity/school.g.dart b/lib/school/entity/school.g.dart deleted file mode 100644 index f2c950f3c..000000000 --- a/lib/school/entity/school.g.dart +++ /dev/null @@ -1,149 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'school.dart'; - -// ************************************************************************** -// TypeAdapterGenerator -// ************************************************************************** - -class SemesterInfoAdapter extends TypeAdapter { - @override - final int typeId = 1; - - @override - SemesterInfo read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return SemesterInfo( - year: fields[0] as int?, - semester: fields[1] as Semester, - ); - } - - @override - void write(BinaryWriter writer, SemesterInfo obj) { - writer - ..writeByte(2) - ..writeByte(0) - ..write(obj.year) - ..writeByte(1) - ..write(obj.semester); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is SemesterInfoAdapter && runtimeType == other.runtimeType && typeId == other.typeId; -} - -class SemesterAdapter extends TypeAdapter { - @override - final int typeId = 0; - - @override - Semester read(BinaryReader reader) { - switch (reader.readByte()) { - case 0: - return Semester.all; - case 1: - return Semester.term1; - case 2: - return Semester.term2; - default: - return Semester.all; - } - } - - @override - void write(BinaryWriter writer, Semester obj) { - switch (obj) { - case Semester.all: - writer.writeByte(0); - break; - case Semester.term1: - writer.writeByte(1); - break; - case Semester.term2: - writer.writeByte(2); - break; - } - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || other is SemesterAdapter && runtimeType == other.runtimeType && typeId == other.typeId; -} - -class CourseCatAdapter extends TypeAdapter { - @override - final int typeId = 2; - - @override - CourseCat read(BinaryReader reader) { - switch (reader.readByte()) { - case 0: - return CourseCat.none; - case 1: - return CourseCat.genEd; - case 2: - return CourseCat.publicCore; - case 3: - return CourseCat.specializedCore; - case 4: - return CourseCat.specializedCompulsory; - case 5: - return CourseCat.specializedElective; - case 6: - return CourseCat.integratedPractice; - case 7: - return CourseCat.practicalInstruction; - default: - return CourseCat.none; - } - } - - @override - void write(BinaryWriter writer, CourseCat obj) { - switch (obj) { - case CourseCat.none: - writer.writeByte(0); - break; - case CourseCat.genEd: - writer.writeByte(1); - break; - case CourseCat.publicCore: - writer.writeByte(2); - break; - case CourseCat.specializedCore: - writer.writeByte(3); - break; - case CourseCat.specializedCompulsory: - writer.writeByte(4); - break; - case CourseCat.specializedElective: - writer.writeByte(5); - break; - case CourseCat.integratedPractice: - writer.writeByte(6); - break; - case CourseCat.practicalInstruction: - writer.writeByte(7); - break; - } - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || other is CourseCatAdapter && runtimeType == other.runtimeType && typeId == other.typeId; -} diff --git a/lib/school/entity/timetable.dart b/lib/school/entity/timetable.dart deleted file mode 100644 index 70ac1b8a0..000000000 --- a/lib/school/entity/timetable.dart +++ /dev/null @@ -1,132 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:sit/entity/campus.dart'; -import 'package:sit/l10n/common.dart'; -import 'package:sit/l10n/extension.dart'; - -class TimePoint { - /// 小时 - final int hour; - - /// 分 - final int minute; - - const TimePoint(this.hour, this.minute); - - const TimePoint.fromMinutes(int minutes) - : hour = minutes ~/ 60, - minute = minutes % 60; - - @override - String toString() => '$hour:${'$minute'.padLeft(2, '0')}'; - - String toStringPrefixed0({bool hour = true, bool minute = true}) { - final sb = StringBuffer(); - if (hour) { - sb.write(this.hour.toString().padLeft(2, '0')); - } else { - sb.write(this.hour.toString()); - } - sb.write(':'); - if (minute) { - sb.write(this.minute.toString().padLeft(2, '0')); - } else { - sb.write(this.minute.toString()); - } - return sb.toString(); - } - - String l10n(BuildContext context) => context.formatHmNum(DateTime(0, 1, 1, hour, minute)); - - TimeDuration difference(TimePoint b) => TimeDuration.fromMinutes(totalMinutes - b.totalMinutes); - - TimePoint operator -(TimeDuration b) => TimePoint.fromMinutes(totalMinutes - b.totalMinutes); - - TimePoint operator +(TimeDuration b) => TimePoint.fromMinutes(totalMinutes + b.totalMinutes); - - int get totalMinutes => hour * 60 + minute; -} - -extension DateTimeTimePointX on DateTime { - DateTime addTimePoint(TimePoint t) { - return add(Duration(hours: t.hour, minutes: t.minute)); - } -} - -class TimeDuration { - final int hour; - final int minute; - static const _i18n = TimeI18n(); - - int get totalMinutes => hour * 60 + minute; - - const TimeDuration(this.hour, this.minute); - - const TimeDuration.fromMinutes(int minutes) - : hour = minutes ~/ 60, - minute = minutes % 60; - - String localized() { - final h = "$hour"; - final min = "$minute".padLeft(2, '0'); - if (hour == 0) { - return _i18n.minuteFormat(min); - } else if (minute == 0) { - return _i18n.hourFormat(h); - } - return _i18n.hourMinuteFormat(h, min); - } - - Duration toDuration() => Duration(hours: hour, minutes: minute); -} - -typedef ClassTime = ({TimePoint begin, TimePoint end}); - -extension ClassTimeX on ClassTime { - TimeDuration get duration { - return end.difference(begin); - } -} - -const fengxianTimetable = [ - // 上午 - (begin: TimePoint(8, 20), end: TimePoint(9, 05)), - (begin: TimePoint(9, 10), end: TimePoint(9, 55)), - (begin: TimePoint(10, 15), end: TimePoint(11, 00)), - (begin: TimePoint(11, 05), end: TimePoint(11, 50)), - // 下午 - (begin: TimePoint(13, 00), end: TimePoint(13, 45)), - (begin: TimePoint(13, 50), end: TimePoint(14, 35)), - (begin: TimePoint(14, 55), end: TimePoint(15, 40)), - (begin: TimePoint(15, 45), end: TimePoint(16, 30)), - // 晚上 - (begin: TimePoint(18, 00), end: TimePoint(18, 45)), - (begin: TimePoint(18, 50), end: TimePoint(19, 35)), - (begin: TimePoint(19, 40), end: TimePoint(20, 25)), -]; - -const xuhuiCampusTimetable = [ - // 上午 - (begin: TimePoint(8, 00), end: TimePoint(8, 45)), - (begin: TimePoint(8, 50), end: TimePoint(9, 35)), - (begin: TimePoint(9, 55), end: TimePoint(10, 40)), - (begin: TimePoint(10, 45), end: TimePoint(11, 30)), - // 下午 - (begin: TimePoint(13, 00), end: TimePoint(13, 45)), - (begin: TimePoint(13, 50), end: TimePoint(14, 35)), - (begin: TimePoint(14, 55), end: TimePoint(15, 40)), - (begin: TimePoint(15, 45), end: TimePoint(16, 30)), - // 晚上 - (begin: TimePoint(18, 00), end: TimePoint(18, 45)), - (begin: TimePoint(18, 50), end: TimePoint(19, 35)), - (begin: TimePoint(19, 40), end: TimePoint(20, 25)), -]; - -List getTeachingBuildingTimetable(Campus campus, String place) { - if (campus == Campus.xuhui) { - return xuhuiCampusTimetable; - } - if (campus == Campus.fengxian) { - return fengxianTimetable; - } - return fengxianTimetable; -} diff --git a/lib/school/event.dart b/lib/school/event.dart deleted file mode 100644 index 89baad9fe..000000000 --- a/lib/school/event.dart +++ /dev/null @@ -1,3 +0,0 @@ -import 'package:sit/utils/async_event.dart'; - -final schoolEventBus = AsyncEventEmitter(); diff --git a/lib/school/exam_arrange/entity/exam.dart b/lib/school/exam_arrange/entity/exam.dart deleted file mode 100644 index e89981c20..000000000 --- a/lib/school/exam_arrange/entity/exam.dart +++ /dev/null @@ -1,148 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:intl/intl.dart'; -import 'package:json_annotation/json_annotation.dart'; -import 'package:sit/l10n/extension.dart'; -import 'package:sit/school/utils.dart'; - -part 'exam.g.dart'; - -String _parseCourseName(dynamic courseName) { - return mapChinesePunctuations(courseName.toString()); -} - -String _parsePlace(dynamic place) { - return mapChinesePunctuations(place.toString()); -} - -int? _parseSeatNumber(String s) => int.tryParse(s); -final _timeFormat = DateFormat('yyyy-MM-dd hh:mm'); - -ExamTime? _parseTime(String s) { - try { - final date = s.split('(')[0]; - final time = s.split('(')[1].replaceAll(')', ''); - final startRaw = '$date ${time.split('-')[0]}'; - final endRaw = '$date ${time.split('-')[1]}'; - - final startTime = _timeFormat.parse(startRaw); - final endTime = _timeFormat.parse(endRaw); - - return (start: startTime, end: endTime); - } catch (_) { - return null; - } -} - -bool? _parseRetake(dynamic status) { - if (status == null) return null; - return switch (status.toString()) { - "是" => true, - "否" => false, - _ => null, - }; -} - -typedef ExamTime = ({DateTime start, DateTime end}); - -@JsonSerializable() -class ExamEntry { - /// 课程名称 - @JsonKey() - final String courseName; - - /// 考试时间. 若无数据, 列表未空. - @JsonKey() - final ExamTime? time; - - /// 考试地点 - @JsonKey() - final String place; - - /// 考试校区 - @JsonKey() - final String campus; - - /// 考试座号 - @JsonKey() - final int? seatNumber; - - /// 是否重修 - @JsonKey() - final bool? isRetake; - - const ExamEntry({ - required this.courseName, - required this.place, - required this.campus, - required this.time, - required this.seatNumber, - required this.isRetake, - }); - - factory ExamEntry.fromJson(Map json) => _$ExamEntryFromJson(json); - - Map toJson() => _$ExamEntryToJson(this); - - factory ExamEntry.parseRemoteJson(Map json) { - return ExamEntry( - courseName: _parseCourseName(json['kcmc']), - place: _parsePlace(json['cdmc']), - campus: json['cdxqmc'] as String, - time: _parseTime(json['kssj'] as String), - seatNumber: _parseSeatNumber(json['zwh'] as String), - isRetake: _parseRetake(json['cxbj']), - ); - } - - @override - String toString() { - return { - "courseName": courseName, - "time": time, - "place": place, - "campus": campus, - "seatNumber": seatNumber, - "isRetake": isRetake, - }.toString(); - } - - static int comparator(ExamEntry a, ExamEntry b) { - final timeA = a.time; - final timeB = b.time; - if (timeA == null || timeB == null) { - if (timeA != timeB) { - return timeA == null ? 1 : -1; - } - return 0; - } - return timeA.start.isAfter(timeB.start) ? 1 : -1; - } -} - -extension ExamEntryX on ExamEntry { - String buildDate(BuildContext context) { - final time = this.time; - assert(time != null); - if (time == null) return "null"; - final (:start, :end) = time; - if (start.year == end.year && start.month == end.month && start.day == end.day) { - // at the same day - return context.formatMdWeekText(start); - } else { - return "${context.formatMdNum(start)}–${context.formatMdNum(end)}"; - } - } - - String buildTime(BuildContext context) { - final time = this.time; - assert(time != null); - if (time == null) return "null"; - final (:start, :end) = time; - if (start.year == end.year && start.month == end.month && start.day == end.day) { - // at the same day - return "${context.formatHmNum(start)}–${context.formatHmNum(end)}"; - } else { - return "${context.formatMdhmNum(start)}–${context.formatMdhmNum(end)}"; - } - } -} diff --git a/lib/school/exam_arrange/entity/exam.g.dart b/lib/school/exam_arrange/entity/exam.g.dart deleted file mode 100644 index 916bf5f16..000000000 --- a/lib/school/exam_arrange/entity/exam.g.dart +++ /dev/null @@ -1,42 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'exam.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -ExamEntry _$ExamEntryFromJson(Map json) => ExamEntry( - courseName: json['courseName'] as String, - place: json['place'] as String, - campus: json['campus'] as String, - time: _$recordConvertNullable( - json['time'], - ($jsonValue) => ( - end: DateTime.parse($jsonValue['end'] as String), - start: DateTime.parse($jsonValue['start'] as String), - ), - ), - seatNumber: json['seatNumber'] as int?, - isRetake: json['isRetake'] as bool?, - ); - -Map _$ExamEntryToJson(ExamEntry instance) => { - 'courseName': instance.courseName, - 'time': instance.time == null - ? null - : { - 'end': instance.time!.end.toIso8601String(), - 'start': instance.time!.start.toIso8601String(), - }, - 'place': instance.place, - 'campus': instance.campus, - 'seatNumber': instance.seatNumber, - 'isRetake': instance.isRetake, - }; - -$Rec? _$recordConvertNullable<$Rec>( - Object? value, - $Rec Function(Map) convert, -) => - value == null ? null : convert(value as Map); diff --git a/lib/school/exam_arrange/i18n.dart b/lib/school/exam_arrange/i18n.dart deleted file mode 100644 index 66f01de8e..000000000 --- a/lib/school/exam_arrange/i18n.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:sit/l10n/common.dart'; - -const i18n = _I18n(); - -class _I18n with CommonI18nMixin { - const _I18n(); - - static const ns = "examArrange"; - - String get title => "$ns.title".tr(); - - String get check => "$ns.check".tr(); - - String get date => "$ns.date".tr(); - - String get time => "$ns.time".tr(); - - String get retake => "$ns.retake".tr(); - - String get location => "$ns.location".tr(); - - String get noExamsTip => "$ns.noExamsTip".tr(); - - String get seatNumber => "$ns.seatNumber".tr(); - - String get addCalendarEvent => "$ns.addCalendarEvent".tr(); -} diff --git a/lib/school/exam_arrange/index.dart b/lib/school/exam_arrange/index.dart deleted file mode 100644 index 6f2cca428..000000000 --- a/lib/school/exam_arrange/index.dart +++ /dev/null @@ -1,153 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:share_plus/share_plus.dart'; -import 'package:sit/design/adaptive/multiplatform.dart'; -import 'package:sit/design/widgets/app.dart'; -import 'package:sit/school/utils.dart'; -import 'package:sit/school/event.dart'; -import 'package:sit/school/exam_arrange/entity/exam.dart'; -import 'package:sit/school/exam_arrange/init.dart'; -import 'package:sit/utils/async_event.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:universal_platform/universal_platform.dart'; - -import "i18n.dart"; -import 'widgets/exam.dart'; - -class ExamArrangeAppCard extends StatefulWidget { - const ExamArrangeAppCard({super.key}); - - @override - State createState() => _ExamArrangeAppCardState(); -} - -class _ExamArrangeAppCardState extends State { - List? examList; - late final EventSubscription $refreshEvent; - late final StreamSubscription $examList; - late final currentSemester = estimateCurrentSemester(); - - @override - void initState() { - super.initState(); - $refreshEvent = schoolEventBus.addListener(() { - refresh(); - }); - $examList = ExamArrangeInit.storage.watchExamList(() => currentSemester).listen((event) { - refresh(); - }); - refresh(); - } - - @override - void dispose() { - $refreshEvent.cancel(); - $examList.cancel(); - super.dispose(); - } - - void refresh() { - setState(() { - examList = ExamArrangeInit.storage.getExamList(currentSemester); - }); - } - - @override - Widget build(BuildContext context) { - final examList = this.examList; - return AppCard( - title: i18n.title.text(), - view: examList != null ? buildMostRecentExam(examList) : null, - leftActions: [ - FilledButton.icon( - onPressed: () { - context.push("/exam-arrange"); - }, - icon: const Icon(Icons.calendar_month), - label: i18n.check.text(), - ), - ], - ); - } - - Widget? buildMostRecentExam(List examList) { - if (examList.isEmpty) return const SizedBox(); - final now = DateTime.now(); - examList = examList.where((exam) => exam.time?.start.isAfter(now) ?? false).toList(); - examList.sort(ExamEntry.comparator); - final mostRecent = examList.firstOrNull; - if (mostRecent == null) return null; - return buildExam(mostRecent); - } - - Widget buildExam(ExamEntry exam) { - if (!isCupertino) { - return ExamCard(exam); - } - return Builder(builder: (context) { - return CupertinoContextMenu.builder( - enableHapticFeedback: true, - actions: [ - if (UniversalPlatform.isAndroid || UniversalPlatform.isIOS) - CupertinoContextMenuAction( - trailingIcon: CupertinoIcons.calendar_badge_plus, - child: i18n.addCalendarEvent.text(), - onPressed: () async { - Navigator.of(context, rootNavigator: true).pop(); - await addExamArrangeToCalendar(exam); - }, - ), - CupertinoContextMenuAction( - trailingIcon: CupertinoIcons.share, - onPressed: () async { - Navigator.of(context, rootNavigator: true).pop(); - await shareExamArrange(exam: exam, context: context); - }, - child: i18n.share.text(), - ), - ], - builder: (context, animation) { - return ExamCard(exam).scrolled(physics: const NeverScrollableScrollPhysics()); - }, - ); - }); - } -} - -Future shareExamArrange({ - required ExamEntry exam, - required BuildContext context, -}) async { - var text = "${exam.courseName}, ${exam.buildDate(context)}, ${exam.buildTime(context)}, ${exam.place}"; - if (exam.seatNumber != null) { - text += ", ${i18n.seatNumber} ${exam.seatNumber}"; - } - await Share.share( - text, - sharePositionOrigin: context.getSharePositionOrigin(), - ); -} - -class ExamCard extends StatelessWidget { - final ExamEntry exam; - - const ExamCard( - this.exam, { - super.key, - }); - - @override - Widget build(BuildContext context) { - return [ - [ - exam.courseName.text(style: context.textTheme.titleMedium), - if (exam.isRetake == true) Chip(label: i18n.retake.text(), elevation: 2), - ].row(maa: MainAxisAlignment.spaceBetween), - const Divider(), - ExamEntryDetailsTable(exam), - ].column(caa: CrossAxisAlignment.start).padSymmetric(v: 15, h: 20).inCard(); - } -} diff --git a/lib/school/exam_arrange/init.dart b/lib/school/exam_arrange/init.dart deleted file mode 100644 index efda15da6..000000000 --- a/lib/school/exam_arrange/init.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:sit/school/exam_arrange/storage/exam.dart'; - -import 'service/exam.dart'; - -class ExamArrangeInit { - static late ExamArrangeService service; - static late ExamArrangeStorage storage; - - static void init() { - service = const ExamArrangeService(); - storage = const ExamArrangeStorage(); - } -} diff --git a/lib/school/exam_arrange/page/list.dart b/lib/school/exam_arrange/page/list.dart deleted file mode 100644 index ef5700493..000000000 --- a/lib/school/exam_arrange/page/list.dart +++ /dev/null @@ -1,119 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:sit/credentials/widgets/oa_scope.dart'; -import 'package:sit/design/widgets/card.dart'; -import 'package:sit/design/widgets/common.dart'; -import 'package:sit/school/entity/school.dart'; -import 'package:sit/school/utils.dart'; -import 'package:sit/school/widgets/semester.dart'; -import 'package:sit/utils/error.dart'; - -import '../entity/exam.dart'; -import '../i18n.dart'; -import '../init.dart'; -import '../widgets/exam.dart'; - -class ExamArrangementListPage extends StatefulWidget { - const ExamArrangementListPage({super.key}); - - @override - State createState() => _ExamArrangementListPageState(); -} - -class _ExamArrangementListPageState extends State { - List? examList; - bool isFetching = false; - late SemesterInfo initial = ExamArrangeInit.storage.lastSemesterInfo ?? estimateCurrentSemester(); - late SemesterInfo selected = initial; - - @override - void initState() { - super.initState(); - refresh(initial); - } - - Future refresh(SemesterInfo info) async { - if (!mounted) return; - setState(() { - examList = ExamArrangeInit.storage.getExamList(info); - isFetching = true; - }); - try { - final examList = await ExamArrangeInit.service.getExamList(info); - ExamArrangeInit.storage.setExamList(info, examList); - if (info == selected) { - if (!mounted) return; - setState(() { - this.examList = examList; - isFetching = false; - }); - } - } catch (error, stackTrace) { - debugPrintError(error, stackTrace); - if (!mounted) return; - setState(() { - isFetching = false; - }); - } - } - - @override - Widget build(BuildContext context) { - final examList = this.examList; - final now = DateTime.now(); - return Scaffold( - body: CustomScrollView( - slivers: [ - SliverAppBar.medium( - pinned: true, - title: i18n.title.text(), - ), - SliverToBoxAdapter( - child: buildSemesterSelector(), - ), - if (examList != null) - if (examList.isEmpty) - SliverFillRemaining( - child: LeavingBlank( - icon: Icons.inbox_outlined, - desc: i18n.noExamsTip, - ), - ) - else - SliverList.builder( - itemCount: examList.length, - itemBuilder: (ctx, i) { - final exam = examList[i]; - return FilledCard( - child: ExamCardContent( - exam, - enableAddEvent: exam.time?.end.isAfter(now) ?? false, - ), - ).padH(6); - }, - ), - ], - ), - bottomNavigationBar: isFetching - ? const PreferredSize( - preferredSize: Size.fromHeight(4), - child: LinearProgressIndicator(), - ) - : null, - ); - } - - Widget buildSemesterSelector() { - return SemesterSelector( - initial: initial, - baseYear: getAdmissionYearFromStudentId(context.auth.credentials?.account), - onSelected: (newSelection) { - setState(() { - selected = newSelection; - }); - ExamArrangeInit.storage.lastSemesterInfo = newSelection; - refresh(newSelection); - }, - ); - } -} diff --git a/lib/school/exam_arrange/service/exam.dart b/lib/school/exam_arrange/service/exam.dart deleted file mode 100644 index b9f08e4c5..000000000 --- a/lib/school/exam_arrange/service/exam.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:dio/dio.dart'; -import 'package:sit/init.dart'; - -import 'package:sit/session/jwxt.dart'; - -import '../entity/exam.dart'; -import 'package:sit/school/entity/school.dart'; - -class ExamArrangeService { - static const _examRoomUrl = 'http://jwxt.sit.edu.cn/jwglxt/kwgl/kscx_cxXsksxxIndex.html'; - - JwxtSession get session => Init.jwxtSession; - - const ExamArrangeService(); - - /// 获取考场信息 - Future> getExamList(SemesterInfo info) async { - final response = await session.request( - _examRoomUrl, - para: { - 'doType': 'query', - 'gnmkdm': 'N358105', - }, - data: { - // 学年名 - 'xnm': info.year.toString(), - // 学期名 - 'xqm': semesterToFormField(info.semester), - }, - options: Options( - method: "POST", - ), - ); - final List itemsData = response.data['items']; - final list = itemsData.map((e) => ExamEntry.parseRemoteJson(e as Map)).toList(); - list.sort(ExamEntry.comparator); - return list; - } -} diff --git a/lib/school/exam_arrange/storage/exam.dart b/lib/school/exam_arrange/storage/exam.dart deleted file mode 100644 index 0a6031a11..000000000 --- a/lib/school/exam_arrange/storage/exam.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:hive/hive.dart'; -import 'package:sit/storage/hive/init.dart'; -import 'package:sit/school/entity/school.dart'; -import 'package:sit/utils/json.dart'; - -import '../entity/exam.dart'; - -class _K { - static const lastSemesterInfo = "/lastSemesterInfo"; - - static String examList(SemesterInfo info) => "/examList/$info"; -} - -class ExamArrangeStorage { - Box get box => HiveInit.examArrange; - - const ExamArrangeStorage(); - - List? getExamList(SemesterInfo info) => - decodeJsonList(box.get(_K.examList(info)), (e) => ExamEntry.fromJson(e)); - - void setExamList(SemesterInfo info, List? exams) => - box.put(_K.examList(info), encodeJsonList(exams, (e) => e.toJson())); - - SemesterInfo? get lastSemesterInfo => box.get(_K.lastSemesterInfo); - - set lastSemesterInfo(SemesterInfo? newV) => box.put(_K.lastSemesterInfo, newV); - - Stream watchExamList(SemesterInfo Function() getFilter) => - box.watch().where((event) => event.key == _K.examList(getFilter())); -} diff --git a/lib/school/exam_arrange/widgets/exam.dart b/lib/school/exam_arrange/widgets/exam.dart deleted file mode 100644 index d146b78bc..000000000 --- a/lib/school/exam_arrange/widgets/exam.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:add_2_calendar/add_2_calendar.dart'; -import 'package:universal_platform/universal_platform.dart'; - -import '../i18n.dart'; -import '../entity/exam.dart'; - -class ExamCardContent extends StatelessWidget { - final ExamEntry exam; - final bool enableAddEvent; - - const ExamCardContent( - this.exam, { - super.key, - required this.enableAddEvent, - }); - - @override - Widget build(BuildContext context) { - final time = exam.time; - return [ - [ - exam.courseName.text(style: context.textTheme.titleMedium), - if (exam.isRetake == true) Chip(label: i18n.retake.text(), elevation: 2), - ].row(maa: MainAxisAlignment.spaceBetween), - Divider(color: context.colorScheme.onSurfaceVariant), - ExamEntryDetailsTable(exam), - if (enableAddEvent && time != null && (UniversalPlatform.isAndroid || UniversalPlatform.isIOS)) ...[ - Divider(color: context.colorScheme.onSurfaceVariant), - buildAddToCalenderAction(), - ], - ].column(caa: CrossAxisAlignment.start).padSymmetric(v: 15, h: 20); - } - - Widget buildAddToCalenderAction() { - return FilledButton.icon( - icon: const Icon(Icons.calendar_month), - onPressed: () async { - await addExamArrangeToCalendar( - exam, - ); - }, - label: i18n.addCalendarEvent.text(), - ); - } -} - -class ExamEntryDetailsTable extends StatelessWidget { - final ExamEntry exam; - - const ExamEntryDetailsTable( - this.exam, { - super.key, - }); - - @override - Widget build(BuildContext context) { - final style = context.textTheme.bodyMedium; - final time = exam.time; - return Table( - children: [ - TableRow(children: [ - i18n.location.text(style: style), - exam.place.text(style: style), - ]), - if (exam.seatNumber != null) - TableRow(children: [ - i18n.seatNumber.text(style: style), - exam.seatNumber.toString().text(style: style), - ]), - if (time != null) ...[ - TableRow(children: [ - i18n.date.text(style: style), - exam.buildDate(context).text(style: style), - ]), - TableRow(children: [ - i18n.time.text(style: style), - exam.buildTime(context).text(style: style), - ]), - ], - ], - ); - } -} - -Future addExamArrangeToCalendar(ExamEntry exam) async { - final time = exam.time; - if (time == null) return; - final (:start, :end) = time; - final event = Event( - title: exam.courseName, - description: "${i18n.seatNumber} ${exam.seatNumber}", - location: "${exam.place} #${exam.seatNumber}", - startDate: start, - endDate: end, - ); - await Add2Calendar.addEvent2Cal(event); -} diff --git a/lib/school/exam_result/entity/gpa.dart b/lib/school/exam_result/entity/gpa.dart deleted file mode 100644 index 1e862413e..000000000 --- a/lib/school/exam_result/entity/gpa.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:sit/school/entity/school.dart'; -import 'package:sit/school/exam_result/entity/result.ug.dart'; - -class ExamResultGpaItem { - // for multi-selection - final int index; - - /// the first attempt of an exam. - final ExamResultUg initial; - final List resit; - final List retake; - - const ExamResultGpaItem({ - required this.index, - required this.initial, - required this.resit, - required this.retake, - }); - - /// Using the [initial.year] - SchoolYear get year => initial.year; - - /// Using the [initial.semester] - Semester get semester => initial.semester; - - /// Using the [initial.semesterInfo] - SemesterInfo get semesterInfo => initial.semesterInfo; - - CourseCat get courseCat => initial.courseCat; - - double get credit => initial.credit; - - double? get maxScore { - return [ - ...resit.map((e) => e.score), - ...retake.map((e) => e.score), - initial.score, - ].whereNotNull().maxOrNull; - } - - bool get passed { - final maxScore = this.maxScore; - if (maxScore == null) return false; - return maxScore >= 60.0; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - return other is ExamResultGpaItem && - runtimeType == other.runtimeType && - initial == other.initial && - resit.equals(other.resit) && - retake.equals(other.retake); - } - - @override - int get hashCode => Object.hash( - initial, - Object.hashAll(resit), - Object.hashAll(retake), - ); -} diff --git a/lib/school/exam_result/entity/result.pg.dart b/lib/school/exam_result/entity/result.pg.dart deleted file mode 100644 index 18774ad4a..000000000 --- a/lib/school/exam_result/entity/result.pg.dart +++ /dev/null @@ -1,159 +0,0 @@ -import 'package:sit/storage/hive/type_id.dart'; - -part 'result.pg.g.dart'; - -class ExamResultPgRaw { - /// 课程类别 - final String courseType; - - /// 课程编号 - final String courseCode; - - /// 课程名称 - final String courseName; - - /// 学分 - final String credit; - - /// 教师 - final String teacher; - - /// 成绩 - final String score; - - /// 是否及格 - /// eg. "及格" - final String passStatus; - - /// 考试性质 - /// eg. "期末考试" - final String examType; - - /// 考试方式 - /// eg. "笔试" - final String examForm; - - /// 考试时间 - final String examTime; - - /// 备注 - final String notes; - - const ExamResultPgRaw({ - required this.courseType, - required this.courseCode, - required this.courseName, - required this.credit, - required this.teacher, - required this.score, - required this.passStatus, - required this.examType, - required this.examForm, - required this.examTime, - required this.notes, - }); - - @override - String toString() { - return { - "courseClass": courseType, - "courseCode": courseCode, - "courseName": courseName, - "courseCredit": credit, - "teacher": teacher, - "score": score, - "isPassed": passStatus, - "examNature": examType, - "examForm": examForm, - "examTime": examTime, - }.toString(); - } - - ExamResultPg parse() { - return ExamResultPg( - courseType: courseType, - courseCode: courseCode, - courseName: courseName, - credit: int.parse(credit), - teacher: teacher, - score: double.parse(score), - passed: passStatus == "及格", - examType: examType, - form: examForm, - // currently, time is not given - time: null, - notes: notes, - ); - } - - bool canParse() { - return double.tryParse(score) != null; - } -} - -@HiveType(typeId: CacheHiveType.examResultPg) -class ExamResultPg { - @HiveField(0) - final String courseType; - - @HiveField(1) - final String courseCode; - - @HiveField(2) - final String courseName; - - @HiveField(3) - final int credit; - - @HiveField(4) - final String teacher; - - /// It's always int. But double is used for most compatibility. - @HiveField(5) - final double score; - - @HiveField(6) - final bool passed; - - @HiveField(7) - final String examType; - - @HiveField(8) - final String form; - - @HiveField(9) - final DateTime? time; - - @HiveField(10) - final String notes; - - const ExamResultPg({ - required this.courseType, - required this.courseCode, - required this.courseName, - required this.credit, - required this.teacher, - required this.score, - required this.passed, - required this.examType, - required this.form, - required this.time, - required this.notes, - }); - - @override - String toString() { - return { - "courseClass": courseType, - "courseCode": courseCode, - "courseName": courseName, - "courseCredit": credit, - "teacher": teacher, - "score": score, - "isPassed": passed, - "examNature": examType, - "examForm": form, - "examTime": time, - }.toString(); - } -} diff --git a/lib/school/exam_result/entity/result.pg.g.dart b/lib/school/exam_result/entity/result.pg.g.dart deleted file mode 100644 index ff0aeb76c..000000000 --- a/lib/school/exam_result/entity/result.pg.g.dart +++ /dev/null @@ -1,69 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'result.pg.dart'; - -// ************************************************************************** -// TypeAdapterGenerator -// ************************************************************************** - -class ExamResultPgAdapter extends TypeAdapter { - @override - final int typeId = 23; - - @override - ExamResultPg read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return ExamResultPg( - courseType: fields[0] as String, - courseCode: fields[1] as String, - courseName: fields[2] as String, - credit: fields[3] as int, - teacher: fields[4] as String, - score: fields[5] as double, - passed: fields[6] as bool, - examType: fields[7] as String, - form: fields[8] as String, - time: fields[9] as DateTime?, - notes: fields[10] as String, - ); - } - - @override - void write(BinaryWriter writer, ExamResultPg obj) { - writer - ..writeByte(11) - ..writeByte(0) - ..write(obj.courseType) - ..writeByte(1) - ..write(obj.courseCode) - ..writeByte(2) - ..write(obj.courseName) - ..writeByte(3) - ..write(obj.credit) - ..writeByte(4) - ..write(obj.teacher) - ..writeByte(5) - ..write(obj.score) - ..writeByte(6) - ..write(obj.passed) - ..writeByte(7) - ..write(obj.examType) - ..writeByte(8) - ..write(obj.form) - ..writeByte(9) - ..write(obj.time) - ..writeByte(10) - ..write(obj.notes); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is ExamResultPgAdapter && runtimeType == other.runtimeType && typeId == other.typeId; -} diff --git a/lib/school/exam_result/entity/result.ug.dart b/lib/school/exam_result/entity/result.ug.dart deleted file mode 100644 index ee149a9d7..000000000 --- a/lib/school/exam_result/entity/result.ug.dart +++ /dev/null @@ -1,263 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:copy_with_extension/copy_with_extension.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:json_annotation/json_annotation.dart'; -import 'package:sit/school/utils.dart'; -import 'package:sit/storage/hive/type_id.dart'; -import 'package:sit/school/entity/school.dart'; - -part 'result.ug.g.dart'; - -String _parseCourseName(dynamic courseName) { - return mapChinesePunctuations(courseName.toString()); -} - -Semester _formFieldToSemester(String s) { - Map semester = { - '': Semester.all, - '3': Semester.term1, - '12': Semester.term2, - }; - return semester[s]!; -} - -SchoolYear _formFieldToSchoolYear(String s) { - return int.parse(s.split('-')[0]); -} - -String _schoolYearToFormField(SchoolYear year) { - return '$year-${year + 1}'; -} - -final _timeFormat = DateFormat("yyyy-MM-dd hh:mm:ss"); - -DateTime? _parseTime(dynamic time) { - if (time == null) return null; - return _timeFormat.parse(time.toString()); -} - -List _parseTeachers(String? text) { - if (text == null) return const []; - if (text == "无") return const []; - return text.split(";"); -} - -@HiveType(typeId: CacheHiveType.examResultUgExamType) -enum UgExamType { - /// 正常考试 - @HiveField(0) - normal, - - /// 补考一 - @HiveField(1) - resit, - - /// 重修 - @HiveField(2) - retake; - - static UgExamType parse(String type) { - if (type == "正常考试") return normal; - if (type == "重修") return retake; - if (type.contains("补考")) return resit; - // fallback to normal - return normal; - } -} - -@JsonSerializable() -@HiveType(typeId: CacheHiveType.examResultUg) -@CopyWith(skipFields: true) -class ExamResultUg { - /// If the teacher of class hasn't been evaluated, the score is NaN. - @JsonKey(name: 'cj', fromJson: double.tryParse) - @HiveField(0) - final double? score; - - /// 课程 - @JsonKey(name: 'kcmc', fromJson: _parseCourseName) - @HiveField(1) - final String courseName; - - /// 课程代码 - @JsonKey(name: 'kch') - @HiveField(2) - final String courseCode; - - /// 班级(正方内部使用) - @JsonKey(name: 'jxb_id') - @HiveField(3) - final String innerClassId; - - /// 班级ID(数字) - @JsonKey(name: 'jxbmc', defaultValue: "") - @HiveField(4) - final String classCode; - - /// 学年 - @JsonKey(name: 'xnmmc', fromJson: _formFieldToSchoolYear, toJson: _schoolYearToFormField) - @HiveField(5) - final SchoolYear year; - - /// 学期 - @JsonKey(name: 'xqm', fromJson: _formFieldToSemester) - @HiveField(6) - final Semester semester; - - /// 学分 - @JsonKey(name: 'xf', fromJson: double.parse) - @HiveField(7) - final double credit; - - @JsonKey(name: "tjsj", fromJson: _parseTime, includeToJson: false) - @HiveField(8) - final DateTime? time; - - @JsonKey(name: "kclbmc", fromJson: CourseCat.parse) - @HiveField(9) - final CourseCat courseCat; - - @JsonKey(name: "jsxm", fromJson: _parseTeachers) - @HiveField(10) - final List teachers; - - @JsonKey(name: "ksxz", fromJson: UgExamType.parse) - @HiveField(11) - final UgExamType examType; - - @JsonKey(includeToJson: false, includeFromJson: false) - @HiveField(12) - final List items; - - const ExamResultUg({ - required this.score, - required this.courseName, - required this.courseCode, - required this.innerClassId, - required this.year, - required this.semester, - required this.credit, - required this.classCode, - required this.time, - required this.courseCat, - required this.examType, - required this.teachers, - this.items = const [], - }); - - bool get passed { - final score = this.score; - return score != null ? score >= 60.0 : false; - } - - bool get isPreparatory => courseCode.startsWith("YK"); - - SemesterInfo get semesterInfo => SemesterInfo(year: year, semester: semester); - - factory ExamResultUg.fromJson(Map json) => _$ExamResultUgFromJson(json); - - @override - String toString() { - return { - "score": "$score", - "courseName": courseName, - "courseId": courseCode, - "innerClassId": innerClassId, - "dynClassId": classCode, - "schoolYear": "$year", - "semester": "$semester", - "credit": "$credit", - "time": time, - "items": "$items", - }.toString(); - } - - static int compareByTime(ExamResultUg a, ExamResultUg b) { - final timeA = a.time; - final timeB = b.time; - if (timeA == null && timeB == null) return 0; - if (timeA == null) return -1; - if (timeB == null) return 1; - return timeA.compareTo(timeB); - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - return other is ExamResultUg && - runtimeType == other.runtimeType && - score == other.score && - courseName == other.courseName && - courseCode == other.courseCode && - innerClassId == other.innerClassId && - year == other.year && - semester == other.semester && - credit == other.credit && - classCode == other.classCode && - time == other.time && - courseCat == other.courseCat && - examType == other.examType && - teachers.equals(other.teachers) && - items.equals(other.items); - } - - @override - int get hashCode => Object.hashAll([ - score, - courseName, - courseCode, - innerClassId, - year, - semester, - credit, - classCode, - time, - courseCat, - examType, - Object.hashAll(teachers), - Object.hashAll(items), - ]); -} - -@HiveType(typeId: CacheHiveType.examResultUgItem) -class ExamResultItem { - /// 成绩名称 - @HiveField(0) - final String scoreType; - - /// 占总成绩百分比 - @HiveField(1) - final String percentage; - - /// 成绩数值 - @HiveField(3) - final double? score; - - const ExamResultItem( - this.scoreType, - this.percentage, - this.score, - ); - - @override - String toString() { - return { - "scoreType": scoreType, - "percentage": percentage, - "score": score, - }.toString(); - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - return other is ExamResultItem && - runtimeType == other.runtimeType && - scoreType == other.scoreType && - score == other.score && - percentage == other.percentage; - } - - @override - int get hashCode => Object.hash(scoreType, score, percentage); -} diff --git a/lib/school/exam_result/entity/result.ug.g.dart b/lib/school/exam_result/entity/result.ug.g.dart deleted file mode 100644 index 77109a893..000000000 --- a/lib/school/exam_result/entity/result.ug.g.dart +++ /dev/null @@ -1,331 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'result.ug.dart'; - -// ************************************************************************** -// CopyWithGenerator -// ************************************************************************** - -abstract class _$ExamResultUgCWProxy { - /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. - /// - /// Usage - /// ```dart - /// ExamResultUg(...).copyWith(id: 12, name: "My name") - /// ```` - ExamResultUg call({ - double? score, - String? courseName, - String? courseCode, - String? innerClassId, - int? year, - Semester? semester, - double? credit, - String? classCode, - DateTime? time, - CourseCat? courseCat, - UgExamType? examType, - List? teachers, - List? items, - }); -} - -/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfExamResultUg.copyWith(...)`. -class _$ExamResultUgCWProxyImpl implements _$ExamResultUgCWProxy { - const _$ExamResultUgCWProxyImpl(this._value); - - final ExamResultUg _value; - - @override - - /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. - /// - /// Usage - /// ```dart - /// ExamResultUg(...).copyWith(id: 12, name: "My name") - /// ```` - ExamResultUg call({ - Object? score = const $CopyWithPlaceholder(), - Object? courseName = const $CopyWithPlaceholder(), - Object? courseCode = const $CopyWithPlaceholder(), - Object? innerClassId = const $CopyWithPlaceholder(), - Object? year = const $CopyWithPlaceholder(), - Object? semester = const $CopyWithPlaceholder(), - Object? credit = const $CopyWithPlaceholder(), - Object? classCode = const $CopyWithPlaceholder(), - Object? time = const $CopyWithPlaceholder(), - Object? courseCat = const $CopyWithPlaceholder(), - Object? examType = const $CopyWithPlaceholder(), - Object? teachers = const $CopyWithPlaceholder(), - Object? items = const $CopyWithPlaceholder(), - }) { - return ExamResultUg( - score: score == const $CopyWithPlaceholder() - ? _value.score - // ignore: cast_nullable_to_non_nullable - : score as double?, - courseName: courseName == const $CopyWithPlaceholder() || courseName == null - ? _value.courseName - // ignore: cast_nullable_to_non_nullable - : courseName as String, - courseCode: courseCode == const $CopyWithPlaceholder() || courseCode == null - ? _value.courseCode - // ignore: cast_nullable_to_non_nullable - : courseCode as String, - innerClassId: innerClassId == const $CopyWithPlaceholder() || innerClassId == null - ? _value.innerClassId - // ignore: cast_nullable_to_non_nullable - : innerClassId as String, - year: year == const $CopyWithPlaceholder() || year == null - ? _value.year - // ignore: cast_nullable_to_non_nullable - : year as int, - semester: semester == const $CopyWithPlaceholder() || semester == null - ? _value.semester - // ignore: cast_nullable_to_non_nullable - : semester as Semester, - credit: credit == const $CopyWithPlaceholder() || credit == null - ? _value.credit - // ignore: cast_nullable_to_non_nullable - : credit as double, - classCode: classCode == const $CopyWithPlaceholder() || classCode == null - ? _value.classCode - // ignore: cast_nullable_to_non_nullable - : classCode as String, - time: time == const $CopyWithPlaceholder() - ? _value.time - // ignore: cast_nullable_to_non_nullable - : time as DateTime?, - courseCat: courseCat == const $CopyWithPlaceholder() || courseCat == null - ? _value.courseCat - // ignore: cast_nullable_to_non_nullable - : courseCat as CourseCat, - examType: examType == const $CopyWithPlaceholder() || examType == null - ? _value.examType - // ignore: cast_nullable_to_non_nullable - : examType as UgExamType, - teachers: teachers == const $CopyWithPlaceholder() || teachers == null - ? _value.teachers - // ignore: cast_nullable_to_non_nullable - : teachers as List, - items: items == const $CopyWithPlaceholder() || items == null - ? _value.items - // ignore: cast_nullable_to_non_nullable - : items as List, - ); - } -} - -extension $ExamResultUgCopyWith on ExamResultUg { - /// Returns a callable class that can be used as follows: `instanceOfExamResultUg.copyWith(...)`. - // ignore: library_private_types_in_public_api - _$ExamResultUgCWProxy get copyWith => _$ExamResultUgCWProxyImpl(this); -} - -// ************************************************************************** -// TypeAdapterGenerator -// ************************************************************************** - -class ExamResultUgAdapter extends TypeAdapter { - @override - final int typeId = 20; - - @override - ExamResultUg read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return ExamResultUg( - score: fields[0] as double?, - courseName: fields[1] as String, - courseCode: fields[2] as String, - innerClassId: fields[3] as String, - year: fields[5] as int, - semester: fields[6] as Semester, - credit: fields[7] as double, - classCode: fields[4] as String, - time: fields[8] as DateTime?, - courseCat: fields[9] as CourseCat, - examType: fields[11] as UgExamType, - teachers: (fields[10] as List).cast(), - items: (fields[12] as List).cast(), - ); - } - - @override - void write(BinaryWriter writer, ExamResultUg obj) { - writer - ..writeByte(13) - ..writeByte(0) - ..write(obj.score) - ..writeByte(1) - ..write(obj.courseName) - ..writeByte(2) - ..write(obj.courseCode) - ..writeByte(3) - ..write(obj.innerClassId) - ..writeByte(4) - ..write(obj.classCode) - ..writeByte(5) - ..write(obj.year) - ..writeByte(6) - ..write(obj.semester) - ..writeByte(7) - ..write(obj.credit) - ..writeByte(8) - ..write(obj.time) - ..writeByte(9) - ..write(obj.courseCat) - ..writeByte(10) - ..write(obj.teachers) - ..writeByte(11) - ..write(obj.examType) - ..writeByte(12) - ..write(obj.items); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is ExamResultUgAdapter && runtimeType == other.runtimeType && typeId == other.typeId; -} - -class ExamResultItemAdapter extends TypeAdapter { - @override - final int typeId = 21; - - @override - ExamResultItem read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return ExamResultItem( - fields[0] as String, - fields[1] as String, - fields[3] as double?, - ); - } - - @override - void write(BinaryWriter writer, ExamResultItem obj) { - writer - ..writeByte(3) - ..writeByte(0) - ..write(obj.scoreType) - ..writeByte(1) - ..write(obj.percentage) - ..writeByte(3) - ..write(obj.score); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is ExamResultItemAdapter && runtimeType == other.runtimeType && typeId == other.typeId; -} - -class UgExamTypeAdapter extends TypeAdapter { - @override - final int typeId = 22; - - @override - UgExamType read(BinaryReader reader) { - switch (reader.readByte()) { - case 0: - return UgExamType.normal; - case 1: - return UgExamType.resit; - case 2: - return UgExamType.retake; - default: - return UgExamType.normal; - } - } - - @override - void write(BinaryWriter writer, UgExamType obj) { - switch (obj) { - case UgExamType.normal: - writer.writeByte(0); - break; - case UgExamType.resit: - writer.writeByte(1); - break; - case UgExamType.retake: - writer.writeByte(2); - break; - } - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is UgExamTypeAdapter && runtimeType == other.runtimeType && typeId == other.typeId; -} - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -ExamResultUg _$ExamResultUgFromJson(Map json) => ExamResultUg( - score: double.tryParse(json['cj'] as String), - courseName: _parseCourseName(json['kcmc']), - courseCode: json['kch'] as String, - innerClassId: json['jxb_id'] as String, - year: _formFieldToSchoolYear(json['xnmmc'] as String), - semester: _formFieldToSemester(json['xqm'] as String), - credit: double.parse(json['xf'] as String), - classCode: json['jxbmc'] as String? ?? '', - time: _parseTime(json['tjsj']), - courseCat: CourseCat.parse(json['kclbmc'] as String?), - examType: UgExamType.parse(json['ksxz'] as String), - teachers: _parseTeachers(json['jsxm'] as String?), - ); - -Map _$ExamResultUgToJson(ExamResultUg instance) => { - 'cj': instance.score, - 'kcmc': instance.courseName, - 'kch': instance.courseCode, - 'jxb_id': instance.innerClassId, - 'jxbmc': instance.classCode, - 'xnmmc': _schoolYearToFormField(instance.year), - 'xqm': _$SemesterEnumMap[instance.semester]!, - 'xf': instance.credit, - 'kclbmc': _$CourseCatEnumMap[instance.courseCat]!, - 'jsxm': instance.teachers, - 'ksxz': _$UgExamTypeEnumMap[instance.examType]!, - }; - -const _$SemesterEnumMap = { - Semester.all: 'all', - Semester.term1: 'term1', - Semester.term2: 'term2', -}; - -const _$CourseCatEnumMap = { - CourseCat.none: 'none', - CourseCat.genEd: 'genEd', - CourseCat.publicCore: 'publicCore', - CourseCat.specializedCore: 'specializedCore', - CourseCat.specializedCompulsory: 'specializedCompulsory', - CourseCat.specializedElective: 'specializedElective', - CourseCat.integratedPractice: 'integratedPractice', - CourseCat.practicalInstruction: 'practicalInstruction', -}; - -const _$UgExamTypeEnumMap = { - UgExamType.normal: 'normal', - UgExamType.resit: 'resit', - UgExamType.retake: 'retake', -}; diff --git a/lib/school/exam_result/events.dart b/lib/school/exam_result/events.dart deleted file mode 100644 index 98d457f04..000000000 --- a/lib/school/exam_result/events.dart +++ /dev/null @@ -1,5 +0,0 @@ -import 'package:event_bus/event_bus.dart'; - -final eventBus = EventBus(); - -class LessonEvaluatedEvent {} diff --git a/lib/school/exam_result/i18n.dart b/lib/school/exam_result/i18n.dart deleted file mode 100644 index ae680defb..000000000 --- a/lib/school/exam_result/i18n.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:sit/l10n/common.dart'; - -const i18n = _I18n(); - -class _I18n with CommonI18nMixin { - const _I18n(); - - static const ns = "examResult"; - final gpa = const _Gpa(); - - String get title => "$ns.title".tr(); - - String get check => "$ns.check".tr(); - - String get teacherEval => "$ns.teacherEval".tr(); - - String get teacherEvalTitle => "$ns.teacherEvalTitle".tr(); - - String get noResultsTip => "$ns.noResultsTip".tr(); - - String get lessonNotEvaluated => "$ns.lessonNotEvaluated".tr(); - - String get compulsory => "$ns.compulsory".tr(); - - String get credit => "$ns.credit".tr(); - - String get elective => "$ns.elective".tr(); -} - -class _Gpa { - const _Gpa(); - - static const ns = "${_I18n.ns}.gpa"; - - String lessonSelected(int count) => "$ns.lessonSelected".tr(args: [ - count.toString(), - ]); - - String gpaResult(double point) => "$ns.gpaResult".tr(args: [ - point.toStringAsPrecision(2), - ]); -} diff --git a/lib/school/exam_result/index.pg.dart b/lib/school/exam_result/index.pg.dart deleted file mode 100644 index 172bf99f3..000000000 --- a/lib/school/exam_result/index.pg.dart +++ /dev/null @@ -1,80 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:sit/design/widgets/app.dart'; -import 'package:sit/school/event.dart'; -import 'package:sit/school/exam_result/init.dart'; -import 'package:sit/school/exam_result/widgets/pg.dart'; -import 'package:sit/utils/async_event.dart'; -import 'package:rettulf/rettulf.dart'; - -import 'entity/result.pg.dart'; -import "i18n.dart"; - -const _recentLength = 2; - -class ExamResultPgAppCard extends StatefulWidget { - const ExamResultPgAppCard({super.key}); - - @override - State createState() => _ExamResultPgAppCardState(); -} - -class _ExamResultPgAppCardState extends State { - List? resultList; - late final EventSubscription $refreshEvent; - final $resultList = ExamResultInit.pgStorage.listenResultList(); - @override - void initState() { - super.initState(); - $refreshEvent = schoolEventBus.addListener(() async { - refresh(); - }); - $resultList.addListener(refresh); - refresh(); - } - - @override - void dispose() { - $refreshEvent.cancel(); - $resultList.removeListener(refresh); - super.dispose(); - } - - void refresh() { - setState(() { - resultList = ExamResultInit.pgStorage.getResultList(); - }); - } - - @override - Widget build(BuildContext context) { - final resultList = this.resultList; - return AppCard( - title: i18n.title.text(), - view: resultList == null ? null : buildRecentResults(resultList), - leftActions: [ - FilledButton.icon( - onPressed: () async { - await context.push("/exam-result/pg"); - }, - icon: const Icon(Icons.fact_check), - label: i18n.check.text(), - ), - ], - ); - } - - Widget? buildRecentResults(List resultList) { - if (resultList.isEmpty) return null; - final results = resultList.sublist(0, min(_recentLength, resultList.length)); - return results - .map((result) => ExamResultPgCard( - result, - elevated: true, - )) - .toList() - .column(); - } -} diff --git a/lib/school/exam_result/index.ug.dart b/lib/school/exam_result/index.ug.dart deleted file mode 100644 index 413a0d9d2..000000000 --- a/lib/school/exam_result/index.ug.dart +++ /dev/null @@ -1,94 +0,0 @@ -import 'dart:async'; -import 'dart:math'; - -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:sit/design/widgets/app.dart'; -import 'package:sit/design/widgets/card.dart'; -import 'package:sit/school/event.dart'; -import 'package:sit/school/exam_result/init.dart'; -import 'package:sit/school/exam_result/widgets/ug.dart'; -import 'package:sit/school/utils.dart'; -import 'package:sit/utils/async_event.dart'; -import 'package:rettulf/rettulf.dart'; - -import 'entity/result.ug.dart'; -import "i18n.dart"; - -const _recentLength = 2; - -class ExamResultUgAppCard extends StatefulWidget { - const ExamResultUgAppCard({super.key}); - - @override - State createState() => _ExamResultUgAppCardState(); -} - -class _ExamResultUgAppCardState extends State { - List? resultList; - late final EventSubscription $refreshEvent; - late final StreamSubscription $resultList; - late final currentSemester = estimateCurrentSemester(); - - @override - void initState() { - super.initState(); - $refreshEvent = schoolEventBus.addListener(() async { - refresh(); - }); - $resultList = ExamResultInit.ugStorage.watchResultList(() => currentSemester).listen((event) { - refresh(); - }); - refresh(); - } - - @override - void dispose() { - $refreshEvent.cancel(); - $resultList.cancel(); - super.dispose(); - } - - void refresh() { - setState(() { - resultList = ExamResultInit.ugStorage.getResultList(currentSemester); - }); - } - - @override - Widget build(BuildContext context) { - final resultList = this.resultList; - return AppCard( - title: i18n.title.text(), - view: resultList == null ? null : buildRecentResults(resultList), - leftActions: [ - FilledButton.icon( - onPressed: () async { - await context.push("/exam-result/ug"); - }, - icon: const Icon(Icons.fact_check), - label: i18n.check.text(), - ), - OutlinedButton.icon( - onPressed: () async { - await context.push("/exam-result/ug/gpa"); - }, - icon: const Icon(Icons.assessment), - label: "GPA".text(), - ) - ], - ); - } - - Widget? buildRecentResults(List resultList) { - if (resultList.isEmpty) return null; - resultList.sort((a, b) => -ExamResultUg.compareByTime(a, b)); - final results = resultList.sublist(0, min(_recentLength, resultList.length)); - return results - .map((result) => ExamResultUgTile( - result, - ).inCard()) - .toList() - .column(); - } -} diff --git a/lib/school/exam_result/init.dart b/lib/school/exam_result/init.dart deleted file mode 100644 index 20c50673b..000000000 --- a/lib/school/exam_result/init.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'service/result.pg.dart'; -import 'service/result.ug.dart'; -import 'storage/result.pg.dart'; -import 'storage/result.ug.dart'; - -class ExamResultInit { - static late ExamResultUgService ugService; - static late ExamResultPgService pgService; - static late ExamResultUgStorage ugStorage; - static late ExamResultPgStorage pgStorage; - - static void init() { - ugService = const ExamResultUgService(); - pgService = const ExamResultPgService(); - ugStorage = const ExamResultUgStorage(); - pgStorage = const ExamResultPgStorage(); - } -} diff --git a/lib/school/exam_result/page/details.ug.dart b/lib/school/exam_result/page/details.ug.dart deleted file mode 100644 index d81ae3daa..000000000 --- a/lib/school/exam_result/page/details.ug.dart +++ /dev/null @@ -1,102 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:sit/design/widgets/list_tile.dart'; -import 'package:sit/design/widgets/navigation.dart'; -import 'package:sit/l10n/extension.dart'; -import 'package:sit/school/exam_result/entity/result.ug.dart'; -import '../i18n.dart'; - -class ExamResultUgDetailsPage extends StatefulWidget { - final ExamResultUg result; - - const ExamResultUgDetailsPage(this.result, {super.key}); - - @override - State createState() => _ExamResultDetailsPageState(); -} - -class _ExamResultDetailsPageState extends State { - @override - Widget build(BuildContext context) { - final result = widget.result; - final score = result.score; - final time = result.time; - final items = - result.items.where((e) => e.score != null && !(e.scoreType == "总评" && e.score == result.score)).toList(); - return Scaffold( - body: CustomScrollView( - slivers: [ - SliverAppBar.medium( - pinned: true, - title: result.courseName.text(), - ), - SliverList.list(children: [ - if (score != null) - DetailListTile( - leading: const Icon(Icons.score), - title: "Score", - subtitle: result.score.toString(), - ) - else - PageNavigationTile( - leading: const Icon(Icons.warning), - title: i18n.lessonNotEvaluated.text(), - subtitle: "Score is available after evaluation".text(), - path: '/teacher-eval', - ), - DetailListTile( - leading: const Icon(Icons.class_), - title: "Exam type", - subtitle: result.examType.toString(), - ), - DetailListTile( - leading: const Icon(Icons.view_timeline_outlined), - title: "Semester", - subtitle: result.semesterInfo.l10n(), - ), - if (time != null) - DetailListTile( - leading: const Icon(Icons.access_time), - title: "Time", - subtitle: context.formatYmdhmNum(time), - ), - DetailListTile( - leading: const Icon(Icons.numbers), - title: "Course code", - subtitle: result.courseCode.toString(), - ), - if (result.classCode.isNotEmpty) - DetailListTile( - leading: const Icon(Icons.group), - title: "Class code", - subtitle: result.classCode.toString(), - ), - DetailListTile( - leading: const Icon(Icons.category), - title: "Course category", - subtitle: result.courseCat.toString(), - ), - if (result.teachers.isNotEmpty) - DetailListTile( - leading: Icon(result.teachers.length > 1 ? Icons.people : Icons.person), - title: "Teachers", // plural - subtitle: result.teachers.join(", "), - ), - ]), - const SliverToBoxAdapter( - child: Divider(), - ), - SliverGrid.extent( - maxCrossAxisExtent: 240, - children: items - .map((item) => ListTile( - title: "${item.scoreType} ${item.percentage}".text(), - subtitle: item.score.toString().text(), - )) - .toList(), - ), - ], - ), - ); - } -} diff --git a/lib/school/exam_result/page/evaluation.dart b/lib/school/exam_result/page/evaluation.dart deleted file mode 100644 index 087030315..000000000 --- a/lib/school/exam_result/page/evaluation.dart +++ /dev/null @@ -1,111 +0,0 @@ -import 'package:dio/dio.dart'; -import 'package:flutter/material.dart'; -import 'package:sit/init.dart'; - -import 'package:sit/settings/settings.dart'; -import 'package:sit/utils/cookies.dart'; -import 'package:sit/widgets/webview/injectable.dart'; -import 'package:sit/widgets/webview/page.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:webview_flutter/webview_flutter.dart'; - -import '../init.dart'; -import '../i18n.dart'; - -class TeacherEvaluationPage extends StatefulWidget { - const TeacherEvaluationPage({super.key}); - - @override - State createState() => _TeacherEvaluationPageState(); -} - -final teacherEvaluationUri = Uri( - scheme: 'http', - host: 'jwxt.sit.edu.cn', - path: '/jwglxt/xspjgl/xspj_cxXspjIndex.html', - queryParameters: { - 'doType': 'details', - 'gnmkdm': 'N401605', - 'layout': 'default', - // 'su': studentId, - }, -); - -const _skipCountingDownPageJs = """ -onClickMenu.call(this, '/xspjgl/xspj_cxXspjIndex.html?doType=details', 'N401605', { "offDetails": "1" }) -"""; - -class _TeacherEvaluationPageState extends State { - final $autoScore = ValueNotifier(100); - final controller = WebViewController(); - List? cookies; - - @override - void initState() { - super.initState(); - $autoScore.addListener(setAllScores); - loadCookies(); - } - - @override - void dispose() { - $autoScore.dispose(); - $autoScore.removeListener(setAllScores); - super.dispose(); - } - - Future setAllScores() async { - await controller.runJavaScript( - "for(const e of document.getElementsByClassName('input-pjf')) e.value='${$autoScore.value}'", - ); - } - - Future loadCookies() async { - // refresh the cookies - await ExamResultInit.ugService.session.request( - teacherEvaluationUri.toString(), - options: Options( - method: "GET", - ), - ); - final cookies = await Init.cookieJar.loadAsWebViewCookie(teacherEvaluationUri); - setState(() { - this.cookies = cookies; - }); - } - - @override - Widget build(BuildContext context) { - final cookies = this.cookies; - if (cookies == null) return const SizedBox(); - return WebViewPage( - controller: controller, - initialUrl: teacherEvaluationUri.toString(), - fixedTitle: i18n.teacherEvalTitle, - initialCookies: cookies, - pageFinishedInjections: const [ - Injection( - js: _skipCountingDownPageJs, - ), - ], - bottomNavigationBar: Settings.isDeveloperMode - ? BottomAppBar( - height: 40, - child: buildAutofillScore(), - ) - : null, - ); - } - - Widget buildAutofillScore() { - return $autoScore >> - (context, value) => Slider( - min: 0, - max: 100, - divisions: 100, - label: value.toString(), - value: value.toDouble(), - onChanged: (v) => $autoScore.value = v.toInt(), - ); - } -} diff --git a/lib/school/exam_result/page/gpa.dart b/lib/school/exam_result/page/gpa.dart deleted file mode 100644 index 5dddaeb25..000000000 --- a/lib/school/exam_result/page/gpa.dart +++ /dev/null @@ -1,323 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:sit/design/animation/progress.dart'; -import 'package:sit/design/widgets/card.dart'; -import 'package:sit/design/widgets/common.dart'; -import 'package:sit/design/widgets/grouped.dart'; -import 'package:sit/design/widgets/multi_select.dart'; -import 'package:sit/school/entity/school.dart'; -import 'package:sit/school/exam_result/entity/gpa.dart'; -import 'package:sit/school/exam_result/entity/result.ug.dart'; -import 'package:sit/school/exam_result/init.dart'; -import 'package:sit/utils/error.dart'; -import 'package:sit/design/adaptive/foundation.dart'; - -import '../i18n.dart'; -import '../utils.dart'; - -class GpaCalculatorPage extends StatefulWidget { - const GpaCalculatorPage({super.key}); - - @override - State createState() => _GpaCalculatorPageState(); -} - -typedef _GpaGroups = ({ - List<({SemesterInfo semester, List items})> groups, - List list -}); - -class _GpaCalculatorPageState extends State { - late _GpaGroups? gpaItems = buildGpaItems(ExamResultInit.ugStorage.getResultList(SemesterInfo.all)); - - final $loadingProgress = ValueNotifier(0.0); - bool isFetching = false; - final $selected = ValueNotifier(const []); - final multiselect = MultiselectController(); - - @override - void initState() { - super.initState(); - fetchAll(); - } - - @override - void dispose() { - multiselect.dispose(); - $loadingProgress.dispose(); - super.dispose(); - } - - _GpaGroups? buildGpaItems(List? resultList) { - if (resultList == null) return null; - final gpaItems = extractExamResultGpaItems(resultList); - final groups = groupExamResultGpaItems(gpaItems); - return (groups: groups, list: gpaItems); - } - - Future fetchAll() async { - setState(() { - isFetching = true; - }); - try { - final results = await ExamResultInit.ugService.fetchResultList( - SemesterInfo.all, - onProgress: (p) { - $loadingProgress.value = p; - }, - ); - ExamResultInit.ugStorage.setResultList(SemesterInfo.all, results); - if (!mounted) return; - setState(() { - gpaItems = buildGpaItems(results); - isFetching = false; - }); - } catch (error, stackTrace) { - debugPrintError(error, stackTrace); - if (!mounted) return; - setState(() { - isFetching = false; - }); - } - } - - @override - Widget build(BuildContext context) { - final gpaItems = this.gpaItems; - return Scaffold( - body: MultiselectScope( - controller: multiselect, - dataSource: gpaItems?.list ?? const [], - onSelectionChanged: (indexes, items) { - $selected.value = items; - }, - child: CustomScrollView( - slivers: [ - SliverAppBar( - pinned: true, - title: buildTitle(), - actions: [ - PlatformTextButton( - onPressed: () { - multiselect.clearSelection(); - }, - child: Text(i18n.done), - ) - ], - bottom: isFetching - ? PreferredSize( - preferredSize: const Size.fromHeight(4), - child: $loadingProgress >> (ctx, value) => AnimatedProgressBar(value: value), - ) - : null, - ), - if (gpaItems != null) - if (gpaItems.groups.isEmpty) - SliverFillRemaining( - child: LeavingBlank( - icon: Icons.inbox_outlined, - desc: i18n.noResultsTip, - ), - ), - if (gpaItems != null) - ...gpaItems.groups.map((e) => ExamResultGroupBySemester( - semester: e.semester, - items: e.items, - )), - ], - ), - ), - bottomNavigationBar: PreferredSize( - preferredSize: const Size.fromHeight(40), - child: buildCourseCatChoices().sized(h: 40), - ), - ); - } - - Widget buildTitle() { - return $selected >> - (ctx, selected) => selected.isEmpty - ? i18n.title.text() - : GpaCalculationText( - items: selected, - ); - } - - Widget buildCourseCatChoices() { - return $selected >> - (ctx, selected) { - return ListView( - scrollDirection: Axis.horizontal, - physics: const RangeMaintainingScrollPhysics(), - children: [ - ActionChip( - label: "Select all".text(), - onPressed: selected.length != gpaItems?.list.length - ? () { - multiselect.selectAll(); - } - : null, - ).padH(4), - ActionChip( - label: "Invert".text(), - onPressed: () { - multiselect.invertSelection(); - }, - ).padH(4), - ActionChip( - label: "Except genEd".text(), - onPressed: selected.any((item) => item.courseCat == CourseCat.genEd) - ? () { - multiselect.setSelectedIndexes(selected - .where((item) => item.courseCat != CourseCat.genEd) - .map((item) => item.index) - .toList()); - } - : null, - ).padH(4), - ActionChip( - label: "Except failed".text(), - onPressed: selected.any((item) => !item.passed) - ? () { - multiselect.setSelectedIndexes( - selected.where((item) => item.passed).map((item) => item.index).toList()); - } - : null, - ).padH(4), - ], - ); - }; - } -} - -class ExamResultGroupBySemester extends StatefulWidget { - final SemesterInfo semester; - final List items; - - const ExamResultGroupBySemester({ - super.key, - required this.semester, - required this.items, - }); - - @override - State createState() => _ExamResultGroupBySemesterState(); -} - -class _ExamResultGroupBySemesterState extends State { - @override - Widget build(BuildContext context) { - final scope = MultiselectScope.controllerOf(context); - final selectedIndicesSet = scope.selectedIndexes.toSet(); - final indicesOfGroup = widget.items.map((item) => item.index).toSet(); - final intersection = selectedIndicesSet.intersection(indicesOfGroup); - final selectedItems = intersection.map((i) => scope[i]).toList(); - final isGroupNoneSelected = intersection.isEmpty; - final isGroupAllSelected = intersection.length == indicesOfGroup.length; - return GroupedSection( - headerBuilder: (expanded, toggleExpand, defaultTrailing) { - return ListTile( - title: widget.semester.l10n().text(), - subtitle: GpaCalculationText( - items: selectedItems, - ), - titleTextStyle: context.textTheme.titleMedium, - onTap: toggleExpand, - trailing: IconButton( - icon: Icon( - isGroupNoneSelected - ? Icons.check_box_outline_blank - : isGroupAllSelected - ? Icons.check_box_outlined - : Icons.indeterminate_check_box_outlined, - ), - onPressed: () { - for (final item in widget.items) { - if (isGroupAllSelected) { - scope.unselect(item.index); - } else { - scope.select(item.index); - } - } - }, - ), - ); - }, - itemCount: widget.items.length, - itemBuilder: (ctx, i) { - final item = widget.items[i]; - final selected = scope.isSelectedIndex(item.index); - return ExamResultGpaTile( - item, - selected: selected, - onTap: () { - scope.toggle(item.index); - }, - ).inFilledCard(clip: Clip.hardEdge); - }); - } -} - -class ExamResultGpaTile extends StatelessWidget { - final ExamResultGpaItem item; - final VoidCallback? onTap; - final bool selected; - - const ExamResultGpaTile( - this.item, { - super.key, - this.onTap, - required this.selected, - }); - - @override - Widget build(BuildContext context) { - final textTheme = context.textTheme; - final result = item.initial; - assert(item.maxScore != null); - final score = item.maxScore ?? 0.0; - return ListTile( - isThreeLine: true, - selected: selected, - leading: Icon(selected ? Icons.check_box_outlined : Icons.check_box_outline_blank).padAll(8), - titleTextStyle: textTheme.titleMedium, - title: Text(result.courseName), - subtitleTextStyle: textTheme.bodyMedium, - subtitle: [ - '${i18n.credit}: ${result.credit}'.text(), - if (result.teachers.isNotEmpty) result.teachers.join(", ").text(), - ].column(caa: CrossAxisAlignment.start, mas: MainAxisSize.min), - leadingAndTrailingTextStyle: textTheme.labelSmall?.copyWith( - fontSize: textTheme.bodyLarge?.fontSize, - color: result.passed ? null : context.$red$, - ), - trailing: score.toString().text(), - onTap: onTap, - ); - } -} - -class GpaCalculationText extends StatelessWidget { - final List items; - - const GpaCalculationText({ - super.key, - required this.items, - }); - - @override - Widget build(BuildContext context) { - if (items.isEmpty) { - return i18n.gpa.lessonSelected(items.length).text(); - } - final validItems = items.map((item) { - final maxScore = item.maxScore; - if (maxScore == null) return null; - return (score: maxScore, credit: item.credit); - }).whereNotNull(); - final gpa = calcGPA(validItems); - return "${i18n.gpa.lessonSelected(items.length)} ${i18n.gpa.gpaResult(gpa)}".text(); - } -} diff --git a/lib/school/exam_result/page/result.pg.dart b/lib/school/exam_result/page/result.pg.dart deleted file mode 100644 index 925bc9e54..000000000 --- a/lib/school/exam_result/page/result.pg.dart +++ /dev/null @@ -1,93 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:sit/design/widgets/common.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:sit/utils/error.dart'; - -import '../entity/result.pg.dart'; -import '../init.dart'; -import '../widgets/pg.dart'; -import '../i18n.dart'; - -class ExamResultPgPage extends StatefulWidget { - const ExamResultPgPage({super.key}); - - @override - State createState() => _ExamResultPgPageState(); -} - -class _ExamResultPgPageState extends State { - List? resultList; - bool isFetching = false; - bool isSelecting = false; - - @override - void initState() { - super.initState(); - refresh(); - } - - @override - void dispose() { - super.dispose(); - } - - Future refresh() async { - if (!mounted) return; - setState(() { - resultList = ExamResultInit.pgStorage.getResultList(); - isFetching = true; - }); - try { - final resultList = await ExamResultInit.pgService.fetchResultList(); - await ExamResultInit.pgStorage.setResultList(resultList); - if (!mounted) return; - setState(() { - this.resultList = resultList; - isFetching = false; - }); - } catch (error, stackTrace) { - debugPrintError(error, stackTrace); - if (!mounted) return; - setState(() { - isFetching = false; - }); - } - } - - @override - Widget build(BuildContext context) { - final resultList = this.resultList; - return Scaffold( - body: CustomScrollView( - slivers: [ - SliverAppBar.medium( - pinned: true, - title: i18n.title.text(), - bottom: isFetching - ? const PreferredSize( - preferredSize: Size.fromHeight(4), - child: LinearProgressIndicator(), - ) - : null, - ), - if (resultList != null) - if (resultList.isEmpty) - SliverFillRemaining( - child: LeavingBlank( - icon: Icons.inbox_outlined, - desc: i18n.noResultsTip, - ), - ) - else - SliverList.builder( - itemCount: resultList.length, - itemBuilder: (item, i) => ExamResultPgCard( - resultList[i], - elevated: false, - ), - ), - ], - ), - ); - } -} diff --git a/lib/school/exam_result/page/result.ug.dart b/lib/school/exam_result/page/result.ug.dart deleted file mode 100644 index 935482b71..000000000 --- a/lib/school/exam_result/page/result.ug.dart +++ /dev/null @@ -1,144 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; -import 'package:go_router/go_router.dart'; -import 'package:sit/credentials/widgets/oa_scope.dart'; -import 'package:sit/design/animation/progress.dart'; -import 'package:sit/design/widgets/card.dart'; -import 'package:sit/design/widgets/common.dart'; -import 'package:sit/school/utils.dart'; -import 'package:sit/school/widgets/semester.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:sit/school/entity/school.dart'; -import 'package:sit/utils/error.dart'; -import 'package:sit/utils/guard_launch.dart'; -import 'package:universal_platform/universal_platform.dart'; - -import '../entity/result.ug.dart'; -import '../init.dart'; -import '../widgets/ug.dart'; -import '../i18n.dart'; -import 'evaluation.dart'; - -class ExamResultUgPage extends StatefulWidget { - const ExamResultUgPage({super.key}); - - @override - State createState() => _ExamResultUgPageState(); -} - -class _ExamResultUgPageState extends State { - late List? resultList = ExamResultInit.ugStorage.getResultList(initial); - bool isFetching = false; - final $loadingProgress = ValueNotifier(0.0); - late SemesterInfo initial = ExamResultInit.ugStorage.lastSemesterInfo ?? estimateCurrentSemester(); - late SemesterInfo selected = initial; - - @override - void initState() { - super.initState(); - refresh(initial); - } - - @override - void dispose() { - $loadingProgress.dispose(); - super.dispose(); - } - - Future refresh(SemesterInfo info) async { - if (!mounted) return; - setState(() { - isFetching = true; - }); - try { - final resultList = await ExamResultInit.ugService.fetchResultList( - info, - onProgress: (p) { - $loadingProgress.value = p; - }, - ); - await ExamResultInit.ugStorage.setResultList(info, resultList); - // Prevents the former query replace new query. - if (info == selected) { - if (!mounted) return; - setState(() { - this.resultList = resultList; - isFetching = false; - }); - } - } catch (error, stackTrace) { - debugPrintError(error, stackTrace); - if (!mounted) return; - setState(() { - isFetching = false; - }); - } finally { - $loadingProgress.value = 0; - } - } - - @override - Widget build(BuildContext context) { - final resultList = this.resultList; - return Scaffold( - body: CustomScrollView( - slivers: [ - SliverAppBar.medium( - pinned: true, - title: i18n.title.text(), - actions: [ - PlatformTextButton( - child: i18n.teacherEval.text(), - onPressed: () async { - if (UniversalPlatform.isDesktop) { - await guardLaunchUrl(context, teacherEvaluationUri); - } else { - await context.push("/teacher-eval"); - } - }, - ) - ], - ), - SliverToBoxAdapter( - child: buildSemesterSelector(), - ), - if (resultList != null) - if (resultList.isEmpty) - SliverFillRemaining( - child: LeavingBlank( - icon: Icons.inbox_outlined, - desc: i18n.noResultsTip, - ), - ) - else - SliverList.builder( - itemCount: resultList.length, - itemBuilder: (item, i) => ExamResultUgTile( - resultList[i], - ).inFilledCard(), - ), - ], - ), - bottomNavigationBar: isFetching - ? PreferredSize( - preferredSize: const Size.fromHeight(4), - child: $loadingProgress >> (ctx, value) => AnimatedProgressBar(value: value), - ) - : null, - ); - } - - Widget buildSemesterSelector() { - return SemesterSelector( - initial: initial, - baseYear: getAdmissionYearFromStudentId(context.auth.credentials?.account), - onSelected: (newSelection) { - setState(() { - selected = newSelection; - }); - ExamResultInit.ugStorage.lastSemesterInfo = newSelection; - refresh(newSelection); - }, - ); - } -} diff --git a/lib/school/exam_result/service/result.pg.dart b/lib/school/exam_result/service/result.pg.dart deleted file mode 100644 index 2cb00de5b..000000000 --- a/lib/school/exam_result/service/result.pg.dart +++ /dev/null @@ -1,78 +0,0 @@ -import 'package:dio/dio.dart'; -import 'package:html/parser.dart'; -import 'package:sit/init.dart'; -import 'package:sit/school/utils.dart'; -import 'package:sit/session/gms.dart'; - -import '../entity/result.pg.dart'; - -class ExamResultPgService { - static const _postgraduateScoresUrl = "http://gms.sit.edu.cn/epstar/app/template.jsp"; - - GmsSession get gmsSession => Init.gmsSession; - - const ExamResultPgService(); - - Future> fetchResultRawList() async { - final res = await gmsSession.request( - _postgraduateScoresUrl, - options: Options( - method: "GET", - ), - para: { - "mainobj": "YJSXT/PYGL/CJGLST/V_PYGL_CJGL_KSCJHZB", - "tfile": "KSCJHZB_CJCX_CD/KSCJHZB_XSCX_CD_BD", - }, - ); - final resultRawList = _parse(res.data); - return resultRawList; - } - - Future> fetchResultList() async { - final rawList = await fetchResultRawList(); - return rawList.where((raw) => raw.canParse()).map((raw) => raw.parse()).toList(); - } - - static List _parse(String html) { - List all = []; - - final htmlDocument = parse(html); - final table = htmlDocument - .querySelectorAll('table.t_table')[1] - .querySelector("tbody")! - .querySelectorAll("tr")[1] - .querySelector("td"); - final tbody = table!.querySelector("tbody"); - final trList = tbody!.querySelectorAll("tr"); - for (var tr in trList) { - if (tr.className == "tr_fld_v") { - final tdList = tr.querySelectorAll("td"); - var courseClass = mapChinesePunctuations(tdList[0].text.trim()); - var courseCode = tdList[1].text.trim(); - var courseName = mapChinesePunctuations(tdList[2].text.trim()); - var courseCredit = tdList[3].text.trim(); - var teacher = tdList[4].text.trim(); - var score = tdList[5].text.trim(); - var isPassed = tdList[6].text.trim(); - var examNature = tdList[7].text.trim(); - var examForm = tdList[8].text.trim(); - var examTime = tdList[9].text.trim(); - var notes = tdList[9].text.trim(); - final scoresRaw = ExamResultPgRaw( - courseType: courseClass, - courseCode: courseCode, - courseName: courseName, - credit: courseCredit, - teacher: teacher, - score: score, - passStatus: isPassed, - examType: examNature, - examForm: examForm, - examTime: examTime, - notes: notes); - all.add(scoresRaw); - } - } - return all; - } -} diff --git a/lib/school/exam_result/service/result.ug.dart b/lib/school/exam_result/service/result.ug.dart deleted file mode 100644 index 46da9487a..000000000 --- a/lib/school/exam_result/service/result.ug.dart +++ /dev/null @@ -1,120 +0,0 @@ -import 'package:beautiful_soup_dart/beautiful_soup.dart'; -import 'package:dio/dio.dart'; -import 'package:sit/design/animation/progress.dart'; -import 'package:sit/init.dart'; - -import 'package:sit/school/entity/school.dart'; -import 'package:sit/session/jwxt.dart'; - -import '../entity/result.ug.dart'; - -class ExamResultUgService { - static const _scoreUrl = 'http://jwxt.sit.edu.cn/jwglxt/cjcx/cjcx_cxDgXscj.html'; - static const _scoreDetailUrl = 'http://jwxt.sit.edu.cn/jwglxt/cjcx/cjcx_cxCjxqGjh.html'; - - /* Why there child is 1,3,5 not 1,2,3? - The example likes follow: - - 【 平时 】 - 40%  - 77.5  - - When you use 1,2,3 to choose the , you will get [] by 2, - it's because /n is chosen by 2 in dart,so here use 1,3,5 to choose - */ - static const _scoreDetailPageSelector = 'div.table-responsive > #subtab > tbody > tr'; - static const _scoreFormSelector = 'td:nth-child(1)'; - static const _scorePercentageSelector = 'td:nth-child(3)'; - static const _scoreValueSelector = 'td:nth-child(5)'; - - JwxtSession get session => Init.jwxtSession; - - const ExamResultUgService(); - - /// 获取成绩 - Future> fetchResultList( - SemesterInfo info, { - void Function(double progress)? onProgress, - }) async { - final year = info.year; - final progress = ProgressWatcher(callback: onProgress); - final response = await session.request( - _scoreUrl, - options: Options( - method: "POST", - ), - para: { - 'gnmkdm': 'N305005', - 'doType': 'query', - }, - data: { - // 学年名 - 'xnm': year == null ? "" : year.toString(), - // 学期名 - 'xqm': semesterToFormField(info.semester), - // 获取成绩最大数量 - 'queryModel.showCount': 5000, - }, - ); - progress.value = 0.2; - final resultList = _parseScoreListPage(response.data); - final newResultList = []; - for (final result in resultList) { - final resultItems = await getResultItems(SemesterInfo(year: result.year, semester: result.semester), - classId: result.innerClassId); - newResultList.add(result.copyWith(items: resultItems)); - progress.value += 0.8 / resultList.length; - } - progress.value = 1; - return newResultList; - } - - /// 获取成绩详情 - Future> getResultItems( - SemesterInfo info, { - required String classId, - }) async { - final response = await session.request( - _scoreDetailUrl, - options: Options( - method: "POST", - ), - para: {'gnmkdm': 'N305005'}, - data: FormData.fromMap({ - // 班级 - 'jxb_id': classId, - // 学年名 - 'xnm': info.year.toString(), - // 学期名 - 'xqm': semesterToFormField(info.semester) - }), - ); - final html = response.data as String; - return _parseDetailPage(html); - } - - static List _parseScoreListPage(Map jsonPage) { - final List? scoreList = jsonPage['items']; - if (scoreList == null) return const []; - return scoreList.map((e) => ExamResultUg.fromJson(e as Map)).toList(); - } - - static ExamResultItem _mapToDetailItem(Bs4Element item) { - f1(s) => s.replaceAll(' ', '').replaceAll(' ', ''); - f2(s) => s.replaceAll('【', '').replaceAll('】', ''); - f(s) => f1(f2(s)); - - String type = item.find(_scoreFormSelector)!.innerHtml.trim(); - String percentage = item.find(_scorePercentageSelector)!.innerHtml.trim(); - String value = item.find(_scoreValueSelector)!.innerHtml; - - return ExamResultItem(f(type), f(percentage), double.tryParse(f(value))); - } - - static List _parseDetailPage(String htmlPage) { - final BeautifulSoup soup = BeautifulSoup(htmlPage); - final elements = soup.findAll(_scoreDetailPageSelector); - - return elements.map(_mapToDetailItem).toList(); - } -} diff --git a/lib/school/exam_result/storage/result.pg.dart b/lib/school/exam_result/storage/result.pg.dart deleted file mode 100644 index 5c0620811..000000000 --- a/lib/school/exam_result/storage/result.pg.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:hive_flutter/hive_flutter.dart'; -import 'package:sit/school/exam_result/entity/result.pg.dart'; -import 'package:sit/storage/hive/init.dart'; - -class _K { - static const ns = "/pg"; - - static const resultList = "$ns/resultList"; -} - -class ExamResultPgStorage { - Box get box => HiveInit.examResult; - - const ExamResultPgStorage(); - - List? getResultList() => (box.get(_K.resultList) as List?)?.cast(); - - Future setResultList(List? newV) => box.put(_K.resultList, newV); - - ValueListenable listenResultList() => box.listenable(keys: [_K.resultList]); -} diff --git a/lib/school/exam_result/storage/result.ug.dart b/lib/school/exam_result/storage/result.ug.dart deleted file mode 100644 index 9988514f1..000000000 --- a/lib/school/exam_result/storage/result.ug.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:hive_flutter/hive_flutter.dart'; -import 'package:sit/storage/hive/init.dart'; -import 'package:sit/school/entity/school.dart'; - -import '../entity/result.ug.dart'; - -class _K { - static const ns = "/ug"; - static const lastSemesterInfo = "$ns/lastSemesterInfo"; - - static String resultList(SemesterInfo info) => "$ns/resultList/$info"; -} - -class ExamResultUgStorage { - Box get box => HiveInit.examResult; - - const ExamResultUgStorage(); - - List? getResultList(SemesterInfo info) => (box.get(_K.resultList(info)) as List?)?.cast(); - - Future setResultList(SemesterInfo info, List? results) => box.put(_K.resultList(info), results); - - ValueListenable listenResultList(SemesterInfo info) => box.listenable(keys: [_K.resultList(info)]); - - SemesterInfo? get lastSemesterInfo => box.get(_K.lastSemesterInfo); - - set lastSemesterInfo(SemesterInfo? newV) => box.put(_K.lastSemesterInfo, newV); - - Stream watchResultList(SemesterInfo Function() getFilter) => - box.watch().where((event) => event.key == _K.resultList(getFilter())); -} diff --git a/lib/school/exam_result/utils.dart b/lib/school/exam_result/utils.dart deleted file mode 100644 index 29a4ec03a..000000000 --- a/lib/school/exam_result/utils.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:sit/school/entity/school.dart'; - -import 'entity/gpa.dart'; -import 'entity/result.ug.dart'; - -double calcGPA(Iterable<({double score, double credit})> resultList) { - double totalCredits = 0.0; - double sum = 0.0; - - for (final s in resultList) { - final score = s.score; - assert(score >= 0, "Exam score should be >= 0"); - totalCredits += s.credit; - sum += s.credit * score; - } - final res = sum / totalCredits / 10.0 - 5.0; - return res.isNaN ? 0 : res; -} - -List filterGpaAvailableResult(List list) { - return list.where((result) => result.score != null && !result.isPreparatory).toList(); -} - -List<({SemesterInfo semester, List results})> groupExamResultList(List list) { - final semester2Result = list.groupListsBy((result) => result.semesterInfo); - final groups = semester2Result.entries.map((entry) => (semester: entry.key, results: entry.value)).toList(); - groups.sortBy((group) => group.semester); - return groups; -} - -List extractExamResultGpaItems(List list) { - final groupByExamType = list.groupListsBy((result) => result.examType); - final normal = groupByExamType[UgExamType.normal] ?? []; - final resit = groupByExamType[UgExamType.resit] ?? []; - final retake = groupByExamType[UgExamType.retake] ?? []; - - final res = []; - var index = 0; - for (final exam in normal) { - final relatedResit = resit.where((e) => e.courseCode == exam.courseCode).toList(); - final relatedRetake = retake.where((e) => e.courseCode == exam.courseCode).toList(); - res.add(ExamResultGpaItem( - index: index, - initial: exam, - resit: relatedResit, - retake: relatedRetake, - )); - index++; - } - return res; -} - -List<({SemesterInfo semester, List items})> groupExamResultGpaItems(List list) { - final semester2Result = list.groupListsBy((result) => result.semesterInfo); - final groups = semester2Result.entries.map((entry) => (semester: entry.key, items: entry.value)).toList(); - groups.sortBy((group) => group.semester); - return groups; -} diff --git a/lib/school/exam_result/widgets/pg.dart b/lib/school/exam_result/widgets/pg.dart deleted file mode 100644 index 88a2868cc..000000000 --- a/lib/school/exam_result/widgets/pg.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:sit/design/adaptive/foundation.dart'; -import 'package:sit/design/widgets/card.dart'; -import 'package:sit/school/widgets/course.dart'; -import 'package:rettulf/rettulf.dart'; - -import '../entity/result.pg.dart'; -import '../i18n.dart'; - -class ExamResultPgCard extends StatelessWidget { - final bool elevated; - final ExamResultPg result; - - const ExamResultPgCard( - this.result, { - super.key, - required this.elevated, - }); - - @override - Widget build(BuildContext context) { - final textTheme = context.textTheme; - return ListTile( - isThreeLine: true, - leading: CourseIcon(courseName: result.courseName), - titleTextStyle: textTheme.titleMedium, - title: Text(result.courseName), - subtitleTextStyle: textTheme.bodyMedium, - subtitle: [ - '${result.courseType} ${result.teacher}'.text(), - '${result.examType} | ${i18n.credit}: ${result.credit}'.text(), - ].column(caa: CrossAxisAlignment.start), - leadingAndTrailingTextStyle: textTheme.labelSmall?.copyWith( - fontSize: textTheme.bodyLarge?.fontSize, - color: result.passed ? null : context.$red$, - ), - trailing: result.score.toString().text(), - ).inAnyCard(clip: Clip.hardEdge, type: elevated ? CardType.plain : CardType.filled); - } -} diff --git a/lib/school/exam_result/widgets/ug.dart b/lib/school/exam_result/widgets/ug.dart deleted file mode 100644 index 7d84f7c0c..000000000 --- a/lib/school/exam_result/widgets/ug.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:sit/design/adaptive/foundation.dart'; -import 'package:sit/school/exam_result/page/details.ug.dart'; -import 'package:sit/school/widgets/course.dart'; -import 'package:rettulf/rettulf.dart'; - -import '../i18n.dart'; -import '../entity/result.ug.dart'; - -class ExamResultUgTile extends StatelessWidget { - final ExamResultUg result; - - const ExamResultUgTile( - this.result, { - super.key, - }); - - @override - Widget build(BuildContext context) { - final textTheme = context.textTheme; - final score = result.score; - return ListTile( - isThreeLine: true, - leading: CourseIcon(courseName: result.courseName), - titleTextStyle: textTheme.titleMedium, - title: Text(result.courseName), - subtitleTextStyle: textTheme.bodyMedium, - subtitle: [ - '${result.examType}'.text(), - if (result.teachers.isNotEmpty) result.teachers.join(", ").text(), - ].column(caa: CrossAxisAlignment.start, mas: MainAxisSize.min), - leadingAndTrailingTextStyle: textTheme.labelSmall?.copyWith( - fontSize: textTheme.bodyLarge?.fontSize, - color: result.passed ? null : context.$red$, - ), - trailing: score != null ? score.toString().text() : i18n.lessonNotEvaluated.text(), - onTap: () async { - context.show$Sheet$((ctx) => ExamResultUgDetailsPage(result)); - }, - ); - } -} diff --git a/lib/school/i18n.dart b/lib/school/i18n.dart deleted file mode 100644 index 177cdc344..000000000 --- a/lib/school/i18n.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:sit/l10n/common.dart'; - -const i18n = _I18n(); - -class _I18n with CommonI18nMixin { - const _I18n(); - - static const ns = "school"; - - String get title => "$ns.title".tr(); - - String get navigation => "$ns.navigation".tr(); - - String get schoolYear => "$ns.schoolYear".tr(); - - String get semester => "$ns.semester.title".tr(); -} - -class CourseI18n { - const CourseI18n(); - - static const ns = "${_I18n.ns}.course"; - - String get classCode => "$ns.classCode".tr(); - - String get courseCode => "$ns.courseCode".tr(); - - String get teacher => "$ns.teacher".tr(); - - String get credit => "$ns.credit".tr(); -} diff --git a/lib/school/index.dart b/lib/school/index.dart deleted file mode 100644 index 1cac9623f..000000000 --- a/lib/school/index.dart +++ /dev/null @@ -1,107 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:sit/credentials/entity/login_status.dart'; -import 'package:sit/credentials/entity/user_type.dart'; -import 'package:sit/credentials/widgets/oa_scope.dart'; -import 'package:sit/school/class2nd/index.dart'; -import 'package:sit/school/event.dart'; -import 'package:sit/school/exam_arrange/index.dart'; -import 'package:sit/school/exam_result/index.pg.dart'; -import 'package:sit/school/exam_result/index.ug.dart'; -import 'package:sit/school/library/index.dart'; -import 'package:sit/school/oa_announce/index.dart'; -import 'package:sit/school/yellow_pages/index.dart'; -import 'package:sit/school/ywb/index.dart'; -import 'package:rettulf/rettulf.dart'; -import 'i18n.dart'; - -class SchoolPage extends StatefulWidget { - const SchoolPage({super.key}); - - @override - State createState() => _SchoolPageState(); -} - -class _SchoolPageState extends State { - LoginStatus? loginStatus; - OaUserType? userType; - - @override - void didChangeDependencies() { - final auth = context.auth; - final newLoginStatus = auth.loginStatus; - final newUserType = auth.userType; - if (loginStatus != newLoginStatus || userType != newUserType) { - setState(() { - loginStatus = newLoginStatus; - userType = newUserType; - }); - } - super.didChangeDependencies(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - resizeToAvoidBottomInset: false, - body: NestedScrollView( - floatHeaderSlivers: true, - headerSliverBuilder: (context, innerBoxIsScrolled) { - return [ - SliverOverlapAbsorber( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), - sliver: SliverAppBar( - title: i18n.navigation.text(), - forceElevated: innerBoxIsScrolled, - ), - ), - ]; - }, - body: RefreshIndicator.adaptive( - triggerMode: RefreshIndicatorTriggerMode.anywhere, - onRefresh: () async { - debugPrint("School page refreshed"); - await HapticFeedback.heavyImpact(); - await schoolEventBus.notifyListeners(); - }, - child: CustomScrollView( - slivers: [ - if (loginStatus != LoginStatus.never) ...[ - if (userType?.capability.enableClass2nd == true) - const SliverToBoxAdapter( - child: Class2ndAppCard(), - ), - if (userType?.capability.enableExamArrange == true) - const SliverToBoxAdapter( - child: ExamArrangeAppCard(), - ), - if (userType == OaUserType.undergraduate) - const SliverToBoxAdapter( - child: ExamResultUgAppCard(), - ) - else if (userType == OaUserType.postgraduate) - const SliverToBoxAdapter( - child: ExamResultPgAppCard(), - ), - ], - if (loginStatus != LoginStatus.never) - const SliverToBoxAdapter( - child: OaAnnounceAppCard(), - ), - if (loginStatus != LoginStatus.never) - const SliverToBoxAdapter( - child: YwbAppCard(), - ), - const SliverToBoxAdapter( - child: LibraryAppCard(), - ), - const SliverToBoxAdapter( - child: YellowPagesAppCard(), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/school/init.dart b/lib/school/init.dart deleted file mode 100644 index 685b0504b..000000000 --- a/lib/school/init.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:sit/design/adaptive/editor.dart'; - -import 'entity/school.dart'; - -class SchoolInit { - static void init() { - EditorEx.registerEnumEditor(Semester.values); - } -} diff --git a/lib/school/library/aggregated.dart b/lib/school/library/aggregated.dart deleted file mode 100644 index bda78c824..000000000 --- a/lib/school/library/aggregated.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'entity/collection_preview.dart'; -import 'entity/image.dart'; -import 'entity/book.dart'; -import 'init.dart'; - -class LibraryAggregated { - /// The result isbn the same as [isbnList] - static Future> fetchBookImages({ - required List isbnList, - }) async { - final result = {}; - final searchRequired = []; - for (final isbn in isbnList) { - final image = LibraryInit.imageStorage.getImage(isbn); - if (image == null) { - searchRequired.add(isbn); - } else { - result[isbn] = image; - } - } - if (searchRequired.isNotEmpty) { - final isbn2Image = await LibraryInit.bookImageSearch.searchByIsbnList(searchRequired); - for (final isbn in searchRequired) { - final image = isbn2Image[isbn.replaceAll('-', '')]; - if (image != null) { - result[isbn] = image; - await LibraryInit.imageStorage.setImage(isbn, image); - } - } - } - return result; - } - - static Future fetchBookImage({required String isbn}) async { - final result = await fetchBookImages(isbnList: [isbn]); - return result.entries.firstOrNull?.value; - } - - static BookImage? getCachedBookImageByIsbn(String isbn) { - return LibraryInit.imageStorage.getImage(isbn); - } - - /// [Book.bookId] to [BookCollectionItem] - static Future>> fetchBooksCollectionPreviewList({ - required List bookIdList, - }) async { - final bookId2Preview = await LibraryInit.collectionPreviewService.getCollectionPreviews(bookIdList); - return bookId2Preview; - } - - static Future> fetchBookCollectionPreviewList({required String bookId}) async { - final bookId2Previews = await fetchBooksCollectionPreviewList(bookIdList: [bookId]); - return bookId2Previews.values.first; - } -} diff --git a/lib/school/library/api.dart b/lib/school/library/api.dart deleted file mode 100644 index b42192ed9..000000000 --- a/lib/school/library/api.dart +++ /dev/null @@ -1,21 +0,0 @@ -class LibraryApi { - static const opacUrl = 'http://210.35.66.106/opac'; - static const forgotLoginPasswordUrl = "$opacUrl/reader/retrievePassword"; - - static const searchUrl = '$opacUrl/search'; - static const hotSearchUrl = '$opacUrl/hotsearch'; - static const apiUrl = '$opacUrl/api'; - static const bookUrl = '$opacUrl/book'; - - static const loanUrl = '$opacUrl/loan'; - static const currentLoanListUrl = '$loanUrl/currentLoanList'; - static const historyLoanListUrl = '$loanUrl/historyLoanList'; - static const renewList = '$loanUrl/renewList'; - static const doRenewUrl = '$loanUrl/doRenew'; - - static const bookCollectionUrl = '$apiUrl/holding'; - static const bookCollectionPreviewsUrl = '$bookUrl/holdingPreviews'; - static const virtualBookshelfUrl = '$apiUrl/virtualBookshelf'; - - static const bookImageInfoUrl = 'https://book-resource.dataesb.com/websearch/metares'; -} diff --git a/lib/school/library/entity/book.dart b/lib/school/library/entity/book.dart deleted file mode 100644 index 4b5872960..000000000 --- a/lib/school/library/entity/book.dart +++ /dev/null @@ -1,94 +0,0 @@ -import 'package:sit/storage/hive/type_id.dart'; - -part "book.g.dart"; - -@HiveType(typeId: CacheHiveType.libraryBook) -class Book { - @HiveField(0) - final String bookId; - @HiveField(1) - final String isbn; - @HiveField(2) - final String title; - @HiveField(3) - final String author; - @HiveField(4) - final String publisher; - @HiveField(5) - final String publishDate; - @HiveField(6) - final String callNumber; - - const Book({ - required this.bookId, - required this.isbn, - required this.title, - required this.author, - required this.publisher, - required this.publishDate, - required this.callNumber, - }); - - @override - String toString() { - return { - "bookId": bookId, - "isbn": isbn, - "title": title, - "author": author, - "publisher": publisher, - "publishDate": publishDate, - "callNumber": callNumber, - }.toString(); - } -} - -@HiveType(typeId: CacheHiveType.libraryBookDetails) -class BookDetails { - @HiveField(0) - final Map details; - - const BookDetails({ - required this.details, - }); - - @override - String toString() { - return details.toString(); - } -} - -class BookSearchResult { - final int resultCount; - final double useTime; - final int currentPage; - final int totalPage; - final List books; - - const BookSearchResult({ - required this.resultCount, - required this.useTime, - required this.currentPage, - required this.totalPage, - required this.books, - }); - - const BookSearchResult.empty({ - this.resultCount = 0, - required this.useTime, - this.currentPage = 0, - this.totalPage = 0, - this.books = const [], - }); - - @override - String toString() { - return { - "resultCount": resultCount, - "useTime": useTime, - "currentPage": currentPage, - "totalPage": totalPage, - "books": books, - }.toString(); - } -} diff --git a/lib/school/library/entity/book.g.dart b/lib/school/library/entity/book.g.dart deleted file mode 100644 index 180381d68..000000000 --- a/lib/school/library/entity/book.g.dart +++ /dev/null @@ -1,88 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'book.dart'; - -// ************************************************************************** -// TypeAdapterGenerator -// ************************************************************************** - -class BookAdapter extends TypeAdapter { - @override - final int typeId = 105; - - @override - Book read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return Book( - bookId: fields[0] as String, - isbn: fields[1] as String, - title: fields[2] as String, - author: fields[3] as String, - publisher: fields[4] as String, - publishDate: fields[5] as String, - callNumber: fields[6] as String, - ); - } - - @override - void write(BinaryWriter writer, Book obj) { - writer - ..writeByte(7) - ..writeByte(0) - ..write(obj.bookId) - ..writeByte(1) - ..write(obj.isbn) - ..writeByte(2) - ..write(obj.title) - ..writeByte(3) - ..write(obj.author) - ..writeByte(4) - ..write(obj.publisher) - ..writeByte(5) - ..write(obj.publishDate) - ..writeByte(6) - ..write(obj.callNumber); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || other is BookAdapter && runtimeType == other.runtimeType && typeId == other.typeId; -} - -class BookDetailsAdapter extends TypeAdapter { - @override - final int typeId = 107; - - @override - BookDetails read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return BookDetails( - details: (fields[0] as Map).cast(), - ); - } - - @override - void write(BinaryWriter writer, BookDetails obj) { - writer - ..writeByte(1) - ..writeByte(0) - ..write(obj.details); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is BookDetailsAdapter && runtimeType == other.runtimeType && typeId == other.typeId; -} diff --git a/lib/school/library/entity/borrow.dart b/lib/school/library/entity/borrow.dart deleted file mode 100644 index 477d6f768..000000000 --- a/lib/school/library/entity/borrow.dart +++ /dev/null @@ -1,137 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:sit/storage/hive/type_id.dart'; - -part "borrow.g.dart"; - -@HiveType(typeId: CacheHiveType.libraryBorrowedBook) -class BorrowedBookItem { - @HiveField(0) - final String bookId; - - @HiveField(1) - final DateTime borrowDate; - - @HiveField(2) - final DateTime expireDate; - - @HiveField(3) - final String barcode; - - @HiveField(4) - final String isbn; - - @HiveField(5) - final String callNumber; - - @HiveField(6) - final String title; - - @HiveField(7) - final String location; - - @HiveField(8) - final String author; - @HiveField(9) - final String type; - - const BorrowedBookItem({ - required this.bookId, - required this.barcode, - required this.isbn, - required this.author, - required this.title, - required this.callNumber, - required this.location, - required this.type, - required this.borrowDate, - required this.expireDate, - }); - - @override - String toString() { - return { - "bookId": bookId, - "barcode": barcode, - "isbn": isbn, - "author": author, - "title": title, - "callNumber": callNumber, - "location": location, - "type": type, - "borrowDate": borrowDate, - "expireDate": expireDate, - }.toString(); - } -} - -@HiveType(typeId: CacheHiveType.libraryBorrowingHistoryOp) -enum BookBorrowingHistoryOperation { - @HiveField(0) - borrowing, - @HiveField(1) - returning; - - String l10n() => "library.history.operation.$name".tr(); -} - -@HiveType(typeId: CacheHiveType.libraryBorrowingHistory) -class BookBorrowingHistoryItem { - @HiveField(0) - final String bookId; - - @HiveField(1) - final DateTime processDate; - - @HiveField(2) - final BookBorrowingHistoryOperation operation; - - @HiveField(3) - final String barcode; - - @HiveField(4) - final String title; - - @HiveField(5) - final String isbn; - - @HiveField(6) - final String callNumber; - - @HiveField(7) - final String author; - - @HiveField(8) - final String location; - - @HiveField(9) - final String type; - - const BookBorrowingHistoryItem({ - required this.bookId, - required this.operation, - required this.barcode, - required this.title, - required this.isbn, - required this.callNumber, - required this.location, - required this.type, - required this.author, - required this.processDate, - }); - - @override - String toString() { - return { - "bookId": bookId, - "operateType": operation, - "barcode": barcode, - "title": title, - "isbn": isbn, - "callNo": callNumber, - "location": location, - "type": type, - "author": author, - "processDate": processDate, - }.toString(); - } -} diff --git a/lib/school/library/entity/borrow.g.dart b/lib/school/library/entity/borrow.g.dart deleted file mode 100644 index f3b429ed0..000000000 --- a/lib/school/library/entity/borrow.g.dart +++ /dev/null @@ -1,162 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'borrow.dart'; - -// ************************************************************************** -// TypeAdapterGenerator -// ************************************************************************** - -class BorrowedBookItemAdapter extends TypeAdapter { - @override - final int typeId = 108; - - @override - BorrowedBookItem read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return BorrowedBookItem( - bookId: fields[0] as String, - barcode: fields[3] as String, - isbn: fields[4] as String, - author: fields[8] as String, - title: fields[6] as String, - callNumber: fields[5] as String, - location: fields[7] as String, - type: fields[9] as String, - borrowDate: fields[1] as DateTime, - expireDate: fields[2] as DateTime, - ); - } - - @override - void write(BinaryWriter writer, BorrowedBookItem obj) { - writer - ..writeByte(10) - ..writeByte(0) - ..write(obj.bookId) - ..writeByte(1) - ..write(obj.borrowDate) - ..writeByte(2) - ..write(obj.expireDate) - ..writeByte(3) - ..write(obj.barcode) - ..writeByte(4) - ..write(obj.isbn) - ..writeByte(5) - ..write(obj.callNumber) - ..writeByte(6) - ..write(obj.title) - ..writeByte(7) - ..write(obj.location) - ..writeByte(8) - ..write(obj.author) - ..writeByte(9) - ..write(obj.type); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is BorrowedBookItemAdapter && runtimeType == other.runtimeType && typeId == other.typeId; -} - -class BookBorrowingHistoryItemAdapter extends TypeAdapter { - @override - final int typeId = 109; - - @override - BookBorrowingHistoryItem read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return BookBorrowingHistoryItem( - bookId: fields[0] as String, - operation: fields[2] as BookBorrowingHistoryOperation, - barcode: fields[3] as String, - title: fields[4] as String, - isbn: fields[5] as String, - callNumber: fields[6] as String, - location: fields[8] as String, - type: fields[9] as String, - author: fields[7] as String, - processDate: fields[1] as DateTime, - ); - } - - @override - void write(BinaryWriter writer, BookBorrowingHistoryItem obj) { - writer - ..writeByte(10) - ..writeByte(0) - ..write(obj.bookId) - ..writeByte(1) - ..write(obj.processDate) - ..writeByte(2) - ..write(obj.operation) - ..writeByte(3) - ..write(obj.barcode) - ..writeByte(4) - ..write(obj.title) - ..writeByte(5) - ..write(obj.isbn) - ..writeByte(6) - ..write(obj.callNumber) - ..writeByte(7) - ..write(obj.author) - ..writeByte(8) - ..write(obj.location) - ..writeByte(9) - ..write(obj.type); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is BookBorrowingHistoryItemAdapter && runtimeType == other.runtimeType && typeId == other.typeId; -} - -class BookBorrowingHistoryOperationAdapter extends TypeAdapter { - @override - final int typeId = 110; - - @override - BookBorrowingHistoryOperation read(BinaryReader reader) { - switch (reader.readByte()) { - case 0: - return BookBorrowingHistoryOperation.borrowing; - case 1: - return BookBorrowingHistoryOperation.returning; - default: - return BookBorrowingHistoryOperation.borrowing; - } - } - - @override - void write(BinaryWriter writer, BookBorrowingHistoryOperation obj) { - switch (obj) { - case BookBorrowingHistoryOperation.borrowing: - writer.writeByte(0); - break; - case BookBorrowingHistoryOperation.returning: - writer.writeByte(1); - break; - } - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is BookBorrowingHistoryOperationAdapter && runtimeType == other.runtimeType && typeId == other.typeId; -} diff --git a/lib/school/library/entity/collection.dart b/lib/school/library/entity/collection.dart deleted file mode 100644 index 057a65f94..000000000 --- a/lib/school/library/entity/collection.dart +++ /dev/null @@ -1,288 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; - -part 'collection.g.dart'; - -class BookCollection { - /// 图书记录号(同一本书可能有多本,该参数用于标识同一本书的不同本) - final int bookRecordId; - - /// 图书编号(用于标识哪本书) - final int bookId; - - /// 馆藏状态类型名称 - final String stateTypeName; - - /// 条码号 - final String barcode; - - /// 索书号 - final String callNumber; - - /// 文献所属馆 - final String originLibrary; - - /// 所属馆位置 - final String originLocation; - - /// 文献所在馆 - final String currentLibrary; - - /// 所在馆位置 - final String currentLocation; - - /// 流通类型名称 - final String circulateTypeName; - - /// 流通类型描述 - final String circulateTypeDescription; - - /// 注册日期 - final DateTime registerDate; - - /// 入馆日期 - final DateTime inDate; - - /// 单价 - final double singlePrice; - - /// 总价 - final double totalPrice; - - const BookCollection({ - required this.bookRecordId, - required this.bookId, - required this.stateTypeName, - required this.barcode, - required this.callNumber, - required this.originLibrary, - required this.originLocation, - required this.currentLibrary, - required this.currentLocation, - required this.circulateTypeName, - required this.circulateTypeDescription, - required this.registerDate, - required this.inDate, - required this.singlePrice, - required this.totalPrice, - }); - - @override - String toString() { - return { - "bookRecordId": bookRecordId, - "bookId": bookId, - "stateTypeName": stateTypeName, - "barcode": barcode, - "callNumber": callNumber, - "originLibrary": originLibrary, - "originLocation": originLocation, - "currentLibrary": currentLibrary, - "currentLocation": currentLocation, - "circulateTypeName": circulateTypeName, - "circulateTypeDescription": circulateTypeDescription, - "registerDate": registerDate, - "inDate": inDate, - "singlePrice": singlePrice, - "totalPrice": totalPrice, - }.toString(); - } -} - -/// 图书的流通类型 -@JsonSerializable() -class BookCirculateType { - // 流通类型代码 - @JsonKey(name: 'cirtype') - final String circulateType; - - // 图书馆代码(SITLIB等) - @JsonKey(name: 'libcode') - final String libraryCode; - - // 流通类型名 - final String name; - - // 流通类型描述 - @JsonKey(name: 'descripe') - final String description; - - // 不知道是啥 - final int loanNumSign; - - // 不知道是啥 - final int isPreviService; - - const BookCirculateType( - this.circulateType, this.libraryCode, this.name, this.description, this.loanNumSign, this.isPreviService); - - factory BookCirculateType.fromJson(Map json) => _$BookCirculateTypeFromJson(json); - - Map toJson() => _$BookCirculateTypeToJson(this); -} - -@JsonSerializable() -class BookCollectionState { - final int stateType; - final String stateName; - - const BookCollectionState(this.stateType, this.stateName); - - factory BookCollectionState.fromJson(Map json) => _$BookCollectionStateFromJson(json); - - Map toJson() => _$BookCollectionStateToJson(this); -} - -@JsonSerializable() -class BookCollectionItem { - // 图书记录号(同一本书可能有多本,该参数用于标识同一本书的不同本) - @JsonKey(name: 'recno') - final int bookRecordId; - - // 图书编号(用于标识哪本书) - @JsonKey(name: 'bookrecno') - final int bookId; - - // 馆藏状态类型号 - @JsonKey(name: 'state') - final int stateType; - - // 条码号 - final String barcode; - - // 索书号 - @JsonKey(name: 'callno') - final String callNumber; - - // 文献所属馆 - @JsonKey(name: 'orglib') - final String originLibraryCode; - @JsonKey(name: 'orglocal') - final String originLocationCode; - - // 文献所在馆 - @JsonKey(name: 'curlib') - final String currentLibraryCode; - @JsonKey(name: 'curlocal') - final String currentLocationCode; - - // 流通类型 - @JsonKey(name: 'cirtype') - final String circulateType; - - // 注册日期 - @JsonKey(name: 'regdate') - final String registerDate; - - // String? register_time; - - // 入馆日期 - @JsonKey(name: 'indate') - final String inDate; - - // 单价 - final double singlePrice; - - // 总价 - final double totalPrice; - - const BookCollectionItem({ - required this.bookRecordId, - required this.bookId, - required this.stateType, - required this.barcode, - required this.callNumber, - required this.originLibraryCode, - required this.originLocationCode, - required this.currentLibraryCode, - required this.currentLocationCode, - required this.circulateType, - required this.registerDate, - required this.inDate, - required this.singlePrice, - required this.totalPrice, - }); - -// double totalLoanNum; - factory BookCollectionItem.fromJson(Map json) => _$BookCollectionItemFromJson(json); - - Map toJson() => _$BookCollectionItemToJson(this); -} - -@JsonSerializable() -class BookCollectionInfo { - // 馆藏信息列表 - final List holdingList; - - // "libcodeMap": { - // "SITLIB": "上应大", - // "999": "中心馆" - // }, - - // 图书馆代码字典 - @JsonKey(name: 'libcodeMap') - final Map libraryCodeMap; - - // "localMap": { - // "110": "徐汇社科阅览室", - // "111": "徐汇综合阅览室", - // "001": "奉贤借阅", - // "002": "社科历史地理", - // "003": "奉贤外文", - - @JsonKey(name: 'localMap') - final Map locationMap; - - // "pBCtypeMap": { - // "SIT_US01": { - // "cirtype": "SIT_US01", - // "libcode": "SITLIB", - // "name": "西文图书", - // "descripe": "全局西文图书", - // "loanNumSign": 0, - // "isPreviService": 1 - // }, - // "SIT_US02": { - // "cirtype": "SIT_US02", - @JsonKey(name: 'pBCtypeMap') - final Map circulateTypeMap; - - // "holdStateMap": { - // "32": { - // "stateType": 32, - // "stateName": "已签收" - // }, - // "0": { - // "stateType": 0, - // "stateName": "流通还回上架中" - // }, - // 馆藏状态 - final Map holdStateMap; - - // 不知道是啥 - // "libcodeDeferDateMap": { - // "SITLIB": 7, - // "999": 7 - // } - final Map libcodeDeferDateMap; - - // 不知道是啥 - // "barcodeLocationUrlMap": { - // "SITLIB": "http://210.35.66.106:8088/TSDW/GotoFlash.aspx?szBarCode=", - // "999": "http://210.35.66.106:8088" - // }, - final Map barcodeLocationUrlMap; - - const BookCollectionInfo({ - required this.holdingList, - required this.libraryCodeMap, - required this.locationMap, - required this.circulateTypeMap, - required this.holdStateMap, - required this.libcodeDeferDateMap, - required this.barcodeLocationUrlMap, - }); - - factory BookCollectionInfo.fromJson(Map json) => _$BookCollectionInfoFromJson(json); - - Map toJson() => _$BookCollectionInfoToJson(this); -} diff --git a/lib/school/library/entity/collection.g.dart b/lib/school/library/entity/collection.g.dart deleted file mode 100644 index c1946c501..000000000 --- a/lib/school/library/entity/collection.g.dart +++ /dev/null @@ -1,95 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'collection.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -BookCirculateType _$BookCirculateTypeFromJson(Map json) => BookCirculateType( - json['cirtype'] as String, - json['libcode'] as String, - json['name'] as String, - json['descripe'] as String, - json['loanNumSign'] as int, - json['isPreviService'] as int, - ); - -Map _$BookCirculateTypeToJson(BookCirculateType instance) => { - 'cirtype': instance.circulateType, - 'libcode': instance.libraryCode, - 'name': instance.name, - 'descripe': instance.description, - 'loanNumSign': instance.loanNumSign, - 'isPreviService': instance.isPreviService, - }; - -BookCollectionState _$BookCollectionStateFromJson(Map json) => BookCollectionState( - json['stateType'] as int, - json['stateName'] as String, - ); - -Map _$BookCollectionStateToJson(BookCollectionState instance) => { - 'stateType': instance.stateType, - 'stateName': instance.stateName, - }; - -BookCollectionItem _$BookCollectionItemFromJson(Map json) => BookCollectionItem( - bookRecordId: json['recno'] as int, - bookId: json['bookrecno'] as int, - stateType: json['state'] as int, - barcode: json['barcode'] as String, - callNumber: json['callno'] as String, - originLibraryCode: json['orglib'] as String, - originLocationCode: json['orglocal'] as String, - currentLibraryCode: json['curlib'] as String, - currentLocationCode: json['curlocal'] as String, - circulateType: json['cirtype'] as String, - registerDate: json['regdate'] as String, - inDate: json['indate'] as String, - singlePrice: (json['singlePrice'] as num).toDouble(), - totalPrice: (json['totalPrice'] as num).toDouble(), - ); - -Map _$BookCollectionItemToJson(BookCollectionItem instance) => { - 'recno': instance.bookRecordId, - 'bookrecno': instance.bookId, - 'state': instance.stateType, - 'barcode': instance.barcode, - 'callno': instance.callNumber, - 'orglib': instance.originLibraryCode, - 'orglocal': instance.originLocationCode, - 'curlib': instance.currentLibraryCode, - 'curlocal': instance.currentLocationCode, - 'cirtype': instance.circulateType, - 'regdate': instance.registerDate, - 'indate': instance.inDate, - 'singlePrice': instance.singlePrice, - 'totalPrice': instance.totalPrice, - }; - -BookCollectionInfo _$BookCollectionInfoFromJson(Map json) => BookCollectionInfo( - holdingList: (json['holdingList'] as List) - .map((e) => BookCollectionItem.fromJson(e as Map)) - .toList(), - libraryCodeMap: Map.from(json['libcodeMap'] as Map), - locationMap: Map.from(json['localMap'] as Map), - circulateTypeMap: (json['pBCtypeMap'] as Map).map( - (k, e) => MapEntry(k, BookCirculateType.fromJson(e as Map)), - ), - holdStateMap: (json['holdStateMap'] as Map).map( - (k, e) => MapEntry(k, BookCollectionState.fromJson(e as Map)), - ), - libcodeDeferDateMap: Map.from(json['libcodeDeferDateMap'] as Map), - barcodeLocationUrlMap: Map.from(json['barcodeLocationUrlMap'] as Map), - ); - -Map _$BookCollectionInfoToJson(BookCollectionInfo instance) => { - 'holdingList': instance.holdingList, - 'libcodeMap': instance.libraryCodeMap, - 'localMap': instance.locationMap, - 'pBCtypeMap': instance.circulateTypeMap, - 'holdStateMap': instance.holdStateMap, - 'libcodeDeferDateMap': instance.libcodeDeferDateMap, - 'barcodeLocationUrlMap': instance.barcodeLocationUrlMap, - }; diff --git a/lib/school/library/entity/collection_preview.dart b/lib/school/library/entity/collection_preview.dart deleted file mode 100644 index 888b0780a..000000000 --- a/lib/school/library/entity/collection_preview.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; - -part 'collection_preview.g.dart'; - -@JsonSerializable() -class BookCollectionItem { - @JsonKey(name: 'bookrecno') - final int bookId; - @JsonKey(name: 'barcode') - final String barcode; - @JsonKey(name: 'callno') - final String callNumber; - - // 文献所在馆 - @JsonKey(name: 'curlibName') - final String currentLibrary; - - // 所在馆位置 - @JsonKey(name: 'curlocalName') - final String currentLocation; - - // 总共有多少 - @JsonKey(name: 'copycount') - final int copyCount; - - // 可借数量 - @JsonKey(name: 'loanableCount') - final int loanableCount; - - const BookCollectionItem({ - required this.bookId, - required this.barcode, - required this.callNumber, - required this.currentLibrary, - required this.currentLocation, - required this.copyCount, - required this.loanableCount, - }); - - factory BookCollectionItem.fromJson(Map json) => _$BookCollectionItemFromJson(json); - - Map toJson() => _$BookCollectionItemToJson(this); - - @override - String toString() { - return { - "bookId": bookId, - "barcode": barcode, - "callNo": callNumber, - "currentLibrary": currentLibrary, - "currentLocation": currentLocation, - "copyCount": copyCount, - "loanableCount": loanableCount, - }.toString(); - } -} diff --git a/lib/school/library/entity/collection_preview.g.dart b/lib/school/library/entity/collection_preview.g.dart deleted file mode 100644 index a316704fe..000000000 --- a/lib/school/library/entity/collection_preview.g.dart +++ /dev/null @@ -1,27 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'collection_preview.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -BookCollectionItem _$BookCollectionItemFromJson(Map json) => BookCollectionItem( - bookId: json['bookrecno'] as int, - barcode: json['barcode'] as String, - callNumber: json['callno'] as String, - currentLibrary: json['curlibName'] as String, - currentLocation: json['curlocalName'] as String, - copyCount: json['copycount'] as int, - loanableCount: json['loanableCount'] as int, - ); - -Map _$BookCollectionItemToJson(BookCollectionItem instance) => { - 'bookrecno': instance.bookId, - 'barcode': instance.barcode, - 'callno': instance.callNumber, - 'curlibName': instance.currentLibrary, - 'curlocalName': instance.currentLocation, - 'copycount': instance.copyCount, - 'loanableCount': instance.loanableCount, - }; diff --git a/lib/school/library/entity/image.dart b/lib/school/library/entity/image.dart deleted file mode 100644 index cb20487a4..000000000 --- a/lib/school/library/entity/image.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; -import 'package:sit/storage/hive/type_id.dart'; - -part 'image.g.dart'; - -@JsonSerializable() -@HiveType(typeId: CacheHiveType.libraryBookImage) -class BookImage { - @HiveField(0) - final String isbn; - @JsonKey(name: 'coverlink') - @HiveField(1) - final String coverUrl; - @JsonKey(name: 'resourceLink') - @HiveField(2) - final String resourceUrl; - @HiveField(3) - final int status; - - const BookImage({ - required this.isbn, - required this.coverUrl, - required this.resourceUrl, - required this.status, - }); - - factory BookImage.fromJson(Map json) => _$BookImageFromJson(json); - - Map toJson() => _$BookImageToJson(this); -} diff --git a/lib/school/library/entity/image.g.dart b/lib/school/library/entity/image.g.dart deleted file mode 100644 index cf65ada80..000000000 --- a/lib/school/library/entity/image.g.dart +++ /dev/null @@ -1,65 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'image.dart'; - -// ************************************************************************** -// TypeAdapterGenerator -// ************************************************************************** - -class BookImageAdapter extends TypeAdapter { - @override - final int typeId = 106; - - @override - BookImage read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return BookImage( - isbn: fields[0] as String, - coverUrl: fields[1] as String, - resourceUrl: fields[2] as String, - status: fields[3] as int, - ); - } - - @override - void write(BinaryWriter writer, BookImage obj) { - writer - ..writeByte(4) - ..writeByte(0) - ..write(obj.isbn) - ..writeByte(1) - ..write(obj.coverUrl) - ..writeByte(2) - ..write(obj.resourceUrl) - ..writeByte(3) - ..write(obj.status); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || other is BookImageAdapter && runtimeType == other.runtimeType && typeId == other.typeId; -} - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -BookImage _$BookImageFromJson(Map json) => BookImage( - isbn: json['isbn'] as String, - coverUrl: json['coverlink'] as String, - resourceUrl: json['resourceLink'] as String, - status: json['status'] as int, - ); - -Map _$BookImageToJson(BookImage instance) => { - 'isbn': instance.isbn, - 'coverlink': instance.coverUrl, - 'resourceLink': instance.resourceUrl, - 'status': instance.status, - }; diff --git a/lib/school/library/entity/search.dart b/lib/school/library/entity/search.dart deleted file mode 100644 index e87967e03..000000000 --- a/lib/school/library/entity/search.dart +++ /dev/null @@ -1,145 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:json_annotation/json_annotation.dart'; - -part 'search.g.dart'; - -@JsonEnum() -enum SearchMethod { - any(""), - title("title"), - primaryTitle("title200a"), - isbn("isbn"), - author("author"), - subject("subject"), - $class("class"), - bookId("ctrlno"), - orderNumber("orderno"), - publisher("publisher"), - callNumber("callno"); - - final String internalQueryParameter; - - const SearchMethod(this.internalQueryParameter); - - String l10nName() => "library.searchMethod.$name".tr(); -} - -@JsonEnum() -enum SortMethod { - // 匹配度 - matchScore("score"), - // 出版日期 - publishDate("pubdate_sort"), - // 主题词 - subject("subject_sort"), - // 标题名 - title("title_sort"), - // 作者 - author("author_sort"), - // 索书号 - callNo("callno_sort"), - // 标题名拼音 - pinyin("pinyin_sort"), - // 借阅次数 - loanCount("loannum_sort"), - // 续借次数 - renewCount("renew_sort"), - // 题名权重 - titleWeight("title200Weight"), - // 正题名权重 - primaryTitleWeight("title200aWeight"), - // 卷册号 - volume("title200h"); - - final String internalQueryParameter; - - const SortMethod(this.internalQueryParameter); - - String l10nName() => "library.sortMethod.$name".tr(); -} - -@JsonEnum() -enum SortOrder { - asc("asc"), - desc("desc"); - - final String internalQueryParameter; - - const SortOrder(this.internalQueryParameter); -} - -@JsonSerializable() -class SearchHistoryItem { - @JsonKey() - final String keyword; - @JsonKey() - final SearchMethod searchMethod; - @JsonKey() - final DateTime time; - - SearchHistoryItem({ - required this.keyword, - required this.searchMethod, - required this.time, - }); - - @override - String toString() { - return { - "keyword": keyword, - "searchMethod": searchMethod, - "time": time, - }.toString(); - } - - factory SearchHistoryItem.fromJson(Map json) => _$SearchHistoryItemFromJson(json); - - Map toJson() => _$SearchHistoryItemToJson(this); -} - -@JsonSerializable() -class LibraryTrendsItem { - @JsonKey() - final String keyword; - @JsonKey() - final int count; - - const LibraryTrendsItem({ - required this.keyword, - required this.count, - }); - - @override - String toString() { - return "$keyword($count)"; - } - - factory LibraryTrendsItem.fromJson(Map json) => _$LibraryTrendsItemFromJson(json); - - Map toJson() => _$LibraryTrendsItemToJson(this); -} - -@JsonSerializable() -class LibraryTrends { - @JsonKey() - final List recent30days; - @JsonKey() - final List total; - - const LibraryTrends({ - required this.recent30days, - required this.total, - }); - - @override - String toString() { - return { - "recent30days": recent30days, - "total": total, - }.toString(); - } - - factory LibraryTrends.fromJson(Map json) => _$LibraryTrendsFromJson(json); - - Map toJson() => _$LibraryTrendsToJson(this); -} diff --git a/lib/school/library/entity/search.g.dart b/lib/school/library/entity/search.g.dart deleted file mode 100644 index b53c89b2f..000000000 --- a/lib/school/library/entity/search.g.dart +++ /dev/null @@ -1,56 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'search.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -SearchHistoryItem _$SearchHistoryItemFromJson(Map json) => SearchHistoryItem( - keyword: json['keyword'] as String, - searchMethod: $enumDecode(_$SearchMethodEnumMap, json['searchMethod']), - time: DateTime.parse(json['time'] as String), - ); - -Map _$SearchHistoryItemToJson(SearchHistoryItem instance) => { - 'keyword': instance.keyword, - 'searchMethod': _$SearchMethodEnumMap[instance.searchMethod]!, - 'time': instance.time.toIso8601String(), - }; - -const _$SearchMethodEnumMap = { - SearchMethod.any: 'any', - SearchMethod.title: 'title', - SearchMethod.primaryTitle: 'primaryTitle', - SearchMethod.isbn: 'isbn', - SearchMethod.author: 'author', - SearchMethod.subject: 'subject', - SearchMethod.$class: r'$class', - SearchMethod.bookId: 'bookId', - SearchMethod.orderNumber: 'orderNumber', - SearchMethod.publisher: 'publisher', - SearchMethod.callNumber: 'callNumber', -}; - -LibraryTrendsItem _$LibraryTrendsItemFromJson(Map json) => LibraryTrendsItem( - keyword: json['keyword'] as String, - count: json['count'] as int, - ); - -Map _$LibraryTrendsItemToJson(LibraryTrendsItem instance) => { - 'keyword': instance.keyword, - 'count': instance.count, - }; - -LibraryTrends _$LibraryTrendsFromJson(Map json) => LibraryTrends( - recent30days: (json['recent30days'] as List) - .map((e) => LibraryTrendsItem.fromJson(e as Map)) - .toList(), - total: - (json['total'] as List).map((e) => LibraryTrendsItem.fromJson(e as Map)).toList(), - ); - -Map _$LibraryTrendsToJson(LibraryTrends instance) => { - 'recent30days': instance.recent30days, - 'total': instance.total, - }; diff --git a/lib/school/library/i18n.dart b/lib/school/library/i18n.dart deleted file mode 100644 index 6371da952..000000000 --- a/lib/school/library/i18n.dart +++ /dev/null @@ -1,118 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:sit/credentials/i18n.dart'; -import 'package:sit/l10n/common.dart'; - -const i18n = _I18n(); - -class _I18n with CommonI18nMixin { - const _I18n(); - - static const ns = "library"; - final login = const _Login(); - final info = const _Info(); - final action = const _Action(); - final searching = const _Search(); - final borrowing = const _Borrowing(); - final history = const _History(); - - String get title => "$ns.title".tr(); - - String get hotPost => "$ns.hotPost".tr(); - - String get readerId => "$ns.readerId".tr(); - - String get noBooks => "$ns.noBooks".tr(); - - String get collectionStatus => "$ns.collectionStatus".tr(); -} - -class _Action { - const _Action(); - - static const ns = "${_I18n.ns}.action"; - - String get login => "$ns.login".tr(); - - String get searchBooks => "$ns.searchBooks".tr(); - - String get borrowing => "$ns.borrowing".tr(); -} - -class _Login { - const _Login(); - - final credentials = const CredentialsI18n(); - - static const ns = "${_I18n.ns}.login"; - - String get title => "$ns.title".tr(); - - String get readerIdHint => "$ns.readerIdHint".tr(); - - String get passwordHint => "$ns.passwordHint".tr(); - - String get failedWarn => "$ns.failedWarn.title".tr(); - - String get failedWarnDesc => "$ns.failedWarn.desc".tr(); -} - -class _Info { - const _Info(); - - static const ns = "${_I18n.ns}.info"; - - String get title => "$ns.title".tr(); - - String get author => "$ns.author".tr(); - - String get isbn => "$ns.isbn".tr(); - - String get publisher => "$ns.publisher".tr(); - - String get publishDate => "$ns.publishDate".tr(); - - String get callNumber => "$ns.callNumber".tr(); - - String get bookId => "$ns.bookId".tr(); - - String get barcode => "$ns.barcode".tr(); - - String availableCollection(String available, String collection) => "$ns.availableCollection".tr( - namedArgs: { - "available": available, - "collection": collection, - }, - ); -} - -class _Search { - const _Search(); - - static const ns = "${_I18n.ns}.search"; - - String get searchHistory => "$ns.searchHistory".tr(); - - String get trending => "$ns.trending".tr(); - - String get mostPopular => "$ns.mostPopular".tr(); -} - -class _Borrowing { - const _Borrowing(); - - static const ns = "${_I18n.ns}.borrowing"; - - String get title => "$ns.title".tr(); - - String get history => "$ns.history".tr(); - - String get renew => "$ns.renew".tr(); -} - -class _History { - const _History(); - - static const ns = "${_I18n.ns}.history"; - - String get title => "$ns.title".tr(); -} diff --git a/lib/school/library/index.dart b/lib/school/library/index.dart deleted file mode 100644 index c7409f9a9..000000000 --- a/lib/school/library/index.dart +++ /dev/null @@ -1,104 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:sit/credentials/init.dart'; -import 'package:sit/design/adaptive/multiplatform.dart'; -import 'package:sit/design/widgets/app.dart'; -import 'package:sit/school/library/page/borrowing.dart'; - -import './i18n.dart'; -import 'entity/borrow.dart'; -import 'init.dart'; -import 'page/search.dart'; -import 'utils.dart'; - -class LibraryAppCard extends StatefulWidget { - const LibraryAppCard({super.key}); - - @override - State createState() => _LibraryAppCardState(); -} - -class _LibraryAppCardState extends State { - final $credentials = CredentialsInit.storage.listenLibraryChange(); - final $borrowedBooks = LibraryInit.borrowStorage.listenBorrowedBooks(); - - @override - void initState() { - super.initState(); - $credentials.addListener(refresh); - $borrowedBooks.addListener(refresh); - } - - @override - void dispose() { - $credentials.removeListener(refresh); - $borrowedBooks.removeListener(refresh); - super.dispose(); - } - - void refresh() { - setState(() {}); - } - - @override - Widget build(BuildContext context) { - final credentials = CredentialsInit.storage.libraryCredentials; - final borrowedBooks = LibraryInit.borrowStorage.getBorrowedBooks(); - return AppCard( - title: i18n.title.text(), - view: borrowedBooks == null ? null : buildBorrowedBook(borrowedBooks), - leftActions: [ - FilledButton.icon( - onPressed: () async { - await showSearch(context: context, delegate: LibrarySearchDelegate()); - }, - icon: const Icon(Icons.search), - label: i18n.action.searchBooks.text(), - ), - if (credentials == null) - OutlinedButton( - onPressed: () async { - await context.push("/library/login"); - }, - child: i18n.action.login.text(), - ) - else - OutlinedButton.icon( - onPressed: () async { - await context.push("/library/borrowing"); - }, - icon: const Icon(Icons.person), - label: i18n.action.borrowing.text(), - ) - ], - ); - } - - Widget? buildBorrowedBook(List books) { - if (books.isEmpty) return null; - final book = books.first; - final card = BorrowedBookCard( - book, - elevated: true, - ); - if (!isCupertino) return card; - return Builder( - builder: (ctx) => CupertinoContextMenu.builder( - enableHapticFeedback: true, - actions: [ - CupertinoContextMenuAction( - trailingIcon: CupertinoIcons.refresh, - onPressed: () async { - Navigator.of(context, rootNavigator: true).pop(); - await renewBorrowedBook(ctx, book.barcode); - }, - child: i18n.borrowing.renew.text(), - ), - ], - builder: (ctx, animation) => card, - ), - ); - } -} diff --git a/lib/school/library/init.dart b/lib/school/library/init.dart deleted file mode 100644 index 333f2c167..000000000 --- a/lib/school/library/init.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'package:sit/school/library/service/auth.dart'; -import 'package:sit/school/library/storage/book.dart'; -import 'package:sit/school/library/storage/borrow.dart'; - -import 'service/details.dart'; -import 'service/book.dart'; -import 'service/borrow.dart'; -import 'service/collection.dart'; -import 'service/collection_preview.dart'; -import 'service/trends.dart'; -import 'service/image_search.dart'; -import 'storage/browse.dart'; -import 'storage/image.dart'; -import 'storage/search.dart'; - -class LibraryInit { - static late LibraryAuthService auth; - static late BookDetailsService bookDetailsService; - static late LibraryCollectionInfoService collectionInfoService; - static late BookSearchService bookSearch; - static late BookImageSearchService bookImageSearch; - static late LibraryCollectionPreviewService collectionPreviewService; - static late LibraryTrendsService hotSearchService; - static late LibraryBorrowService borrowService; - - static late LibrarySearchStorage searchStorage; - static late LibraryBookStorage bookStorage; - static late LibraryBorrowStorage borrowStorage; - static late LibraryImageStorage imageStorage; - - static late LibraryBrowseStorage browseStorage; - - static void init() { - auth = const LibraryAuthService(); - - bookSearch = const BookSearchService(); - bookDetailsService = const BookDetailsService(); - collectionInfoService = const LibraryCollectionInfoService(); - bookImageSearch = const BookImageSearchService(); - collectionPreviewService = const LibraryCollectionPreviewService(); - hotSearchService = const LibraryTrendsService(); - - borrowService = const LibraryBorrowService(); - - searchStorage = const LibrarySearchStorage(); - bookStorage = const LibraryBookStorage(); - borrowStorage = const LibraryBorrowStorage(); - imageStorage = const LibraryImageStorage(); - - browseStorage = const LibraryBrowseStorage(); - } -} diff --git a/lib/school/library/page/borrowing.dart b/lib/school/library/page/borrowing.dart deleted file mode 100644 index 1ae2c4291..000000000 --- a/lib/school/library/page/borrowing.dart +++ /dev/null @@ -1,134 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; -import 'package:go_router/go_router.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:sit/design/adaptive/foundation.dart'; -import 'package:sit/design/widgets/card.dart'; -import 'package:sit/l10n/extension.dart'; -import 'package:sit/school/library/init.dart'; -import 'package:sit/school/library/utils.dart'; -import 'package:sit/utils/error.dart'; - -import '../entity/borrow.dart'; -import '../i18n.dart'; -import '../widgets/book.dart'; -import 'details.dart'; -import 'details.model.dart'; - -class LibraryBorrowingPage extends StatefulWidget { - const LibraryBorrowingPage({super.key}); - - @override - State createState() => _LibraryBorrowingPageState(); -} - -class _LibraryBorrowingPageState extends State { - bool isFetching = false; - List? borrowed = LibraryInit.borrowStorage.getBorrowedBooks(); - - @override - void initState() { - super.initState(); - fetch(); - } - - Future fetch() async { - if (!mounted) return; - setState(() { - isFetching = true; - }); - try { - final borrowed = await LibraryInit.borrowService.getMyBorrowBookList(); - await LibraryInit.borrowStorage.setBorrowedBooks(borrowed); - if (!mounted) return; - setState(() { - this.borrowed = borrowed; - isFetching = false; - }); - } catch (error, stackTrace) { - debugPrintError(error, stackTrace); - if (!mounted) return; - setState(() { - isFetching = false; - }); - } - } - - @override - Widget build(BuildContext context) { - final borrowed = this.borrowed; - return Scaffold( - body: CustomScrollView( - slivers: [ - SliverAppBar( - title: i18n.borrowing.title.text(), - actions: [ - PlatformTextButton( - child: i18n.borrowing.history.text(), - onPressed: () async { - await context.push("/library/borrowing-history"); - }, - ), - ], - bottom: isFetching - ? const PreferredSize( - preferredSize: Size.fromHeight(4), - child: LinearProgressIndicator(), - ) - : null, - ), - if (borrowed != null) - SliverList.builder( - itemCount: borrowed.length, - itemBuilder: (ctx, i) { - return BorrowedBookCard( - borrowed[i], - elevated: false, - ); - }, - ), - ], - ), - ); - } -} - -class BorrowedBookCard extends StatelessWidget { - final BorrowedBookItem book; - final bool elevated; - - const BorrowedBookCard( - this.book, { - super.key, - required this.elevated, - }); - - @override - Widget build(BuildContext context) { - return ListTile( - leading: AsyncBookImage(isbn: book.isbn), - title: book.title.text(), - subtitle: [ - book.author.text(), - "${i18n.info.barcode} ${book.barcode}".text(), - "${i18n.info.isbn} ${book.isbn}".text(), - "${context.formatYmdText(book.borrowDate)}–${context.formatYmdText(book.expireDate)}".text(), - ].column(mas: MainAxisSize.min, caa: CrossAxisAlignment.start), - onTap: () async { - await context.show$Sheet$( - (ctx) => BookDetailsPage( - book: BookModel.fromBorrowed(book), - actions: [ - PlatformTextButton( - onPressed: () async { - await renewBorrowedBook(context, book.barcode); - }, - child: i18n.borrowing.renew.text(), - ) - ], - ), - ); - }, - ).inAnyCard(clip: Clip.hardEdge, type: elevated ? CardType.plain : CardType.filled); - } -} diff --git a/lib/school/library/page/details.dart b/lib/school/library/page/details.dart deleted file mode 100644 index 2ee1654c4..000000000 --- a/lib/school/library/page/details.dart +++ /dev/null @@ -1,262 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:sit/design/widgets/list_tile.dart'; -import 'package:sit/school/library/aggregated.dart'; -import 'package:sit/school/library/page/details.model.dart'; -import 'package:sit/school/library/widgets/book.dart'; -import 'package:sit/utils/error.dart'; - -import '../entity/book.dart'; -import '../entity/collection_preview.dart'; -import '../entity/search.dart'; -import '../init.dart'; -import '../i18n.dart'; -import 'search_result.dart'; - -class BookDetailsPage extends StatefulWidget { - final BookModel book; - final BookSearchCallback? onSearchTap; - final List? actions; - - const BookDetailsPage({ - super.key, - required this.book, - this.onSearchTap, - this.actions, - }); - - @override - State createState() => _BookDetailsPageState(); -} - -class _BookDetailsPageState extends State { - late BookDetails? details = LibraryInit.bookStorage.getBookDetails(widget.book.bookId); - bool isFetching = false; - final $hasImage = ValueNotifier(true); - - @override - void initState() { - super.initState(); - fetchDetails(); - } - - Future fetchDetails() async { - if (details != null) return; - if (!context.mounted) return; - setState(() { - isFetching = true; - }); - final bookId = widget.book.bookId; - try { - final details = await LibraryInit.bookDetailsService.query(bookId); - await LibraryInit.bookStorage.setBookDetails(bookId, details); - if (!context.mounted) return; - setState(() { - this.details = details; - isFetching = false; - }); - } catch (error, stackTrace) { - debugPrintError(error, stackTrace); - return; - } - if (!context.mounted) return; - setState(() { - isFetching = false; - }); - } - - @override - Widget build(BuildContext context) { - final details = this.details; - final book = widget.book; - final onSearchTap = widget.onSearchTap; - final publisher = book.publisher; - final publishDate = book.publishDate; - return SelectionArea( - child: Scaffold( - body: CustomScrollView( - slivers: [ - $hasImage >> - (ctx, hasImage) => TweenAnimationBuilder( - duration: Durations.long4, - curve: Curves.fastEaseInToSlowEaseOut, - tween: Tween( - begin: 0.0, - end: hasImage ? 300.0 : 0.0, - ), - builder: (context, value, _) { - return SliverAppBar( - expandedHeight: value, - pinned: true, - stretch: true, - flexibleSpace: FlexibleSpaceBar( - background: AsyncBookImage( - isbn: book.isbn, - onHasImageChanged: (value) { - $hasImage.value = value; - }, - ), - ), - actions: widget.actions, - bottom: isFetching - ? const PreferredSize( - preferredSize: Size.fromHeight(4), - child: LinearProgressIndicator(), - ) - : null, - ); - }, - ), - SliverList.list(children: [ - DetailListTile( - title: i18n.info.title, - subtitle: book.title, - trailing: onSearchTap == null - ? null - : IconButton( - icon: const Icon(Icons.youtube_searched_for), - onPressed: () { - onSearchTap.call(SearchMethod.title, book.title); - }, - ), - ), - DetailListTile( - title: i18n.info.author, - subtitle: book.author, - trailing: onSearchTap == null - ? null - : IconButton( - icon: const Icon(Icons.youtube_searched_for), - onPressed: () { - onSearchTap.call(SearchMethod.author, book.author); - }, - ), - ), - DetailListTile( - title: i18n.info.isbn, - subtitle: book.isbn, - ), - DetailListTile( - title: i18n.info.callNumber, - subtitle: book.callNumber, - ), - if (publisher != null) - DetailListTile( - title: i18n.info.publisher, - subtitle: publisher, - trailing: onSearchTap == null - ? null - : IconButton( - icon: const Icon(Icons.youtube_searched_for), - onPressed: () { - onSearchTap.call(SearchMethod.publisher, publisher); - }, - ), - ), - if (publishDate != null) - DetailListTile( - title: i18n.info.publishDate, - subtitle: book.publishDate, - ), - ]), - SliverList.list(children: [ - const Divider(), - BookCollectionPreviewList(book: book), - ]), - if (details != null) - SliverList.list(children: [ - const Divider(), - buildBookDetails(details).padAll(10), - ]), - ], - ), - ), - ); - } - - Widget buildBookDetails(BookDetails info) { - return Table( - columnWidths: const { - 0: FlexColumnWidth(2), - 1: FlexColumnWidth(5), - }, - // border: TableBorder.all(color: Colors.red), - children: info.details.entries - .map( - (e) => TableRow( - children: [ - e.key.text(), - e.value.text(), - ], - ), - ) - .toList(), - ); - } -} - -class BookCollectionPreviewList extends StatefulWidget { - final BookModel book; - - const BookCollectionPreviewList({ - super.key, - required this.book, - }); - - @override - State createState() => _BookCollectionPreviewListState(); -} - -class _BookCollectionPreviewListState extends State { - List? holding; - bool isFetching = false; - - @override - void initState() { - super.initState(); - fetchCollectionPreview(); - } - - Future fetchCollectionPreview() async { - if (!context.mounted) return; - setState(() { - isFetching = true; - }); - final bookId = widget.book.bookId; - try { - final holding = await LibraryAggregated.fetchBookCollectionPreviewList(bookId: bookId); - if (!context.mounted) return; - setState(() { - this.holding = holding; - isFetching = false; - }); - } catch (error, stackTrace) { - debugPrintError(error, stackTrace); - return; - } - if (!context.mounted) return; - setState(() { - isFetching = false; - }); - } - - @override - Widget build(BuildContext context) { - final holding = this.holding; - return [ - ListTile( - leading: const Icon(Icons.book), - title: i18n.collectionStatus.text(), - ), - if (isFetching) - const CircularProgressIndicator.adaptive() - else if (holding != null) - ...holding.map((item) { - return ListTile( - title: item.currentLocation.text(), - subtitle: i18n.info.availableCollection("${item.loanableCount}", "${item.copyCount}").text(), - ); - }) - ].column(); - } -} diff --git a/lib/school/library/page/details.model.dart b/lib/school/library/page/details.model.dart deleted file mode 100644 index ac449e244..000000000 --- a/lib/school/library/page/details.model.dart +++ /dev/null @@ -1,67 +0,0 @@ -import '../entity/book.dart'; -import '../entity/borrow.dart'; - -class BookModel { - final String bookId; - final String isbn; - final String title; - final String author; - final String callNumber; - final String? publisher; - final String? publishDate; - - const BookModel({ - required this.bookId, - required this.isbn, - required this.title, - required this.author, - required this.callNumber, - this.publisher, - this.publishDate, - }); - - @override - String toString() { - return { - "bookId": bookId, - "isbn": isbn, - "title": title, - "author": author, - "publisher": publisher, - "publishDate": publishDate, - "callNumber": callNumber, - }.toString(); - } - - factory BookModel.fromBook(Book book) { - return BookModel( - bookId: book.bookId, - isbn: book.isbn, - title: book.title, - author: book.author, - callNumber: book.callNumber, - publisher: book.publisher, - publishDate: book.publishDate, - ); - } - - factory BookModel.fromBorrowed(BorrowedBookItem book) { - return BookModel( - bookId: book.bookId, - isbn: book.isbn, - title: book.title, - author: book.author, - callNumber: book.callNumber, - ); - } - - factory BookModel.fromBorrowHistory(BookBorrowingHistoryItem book) { - return BookModel( - bookId: book.bookId, - isbn: book.isbn, - title: book.title, - author: book.author, - callNumber: book.callNumber, - ); - } -} diff --git a/lib/school/library/page/history.dart b/lib/school/library/page/history.dart deleted file mode 100644 index 51374b50c..000000000 --- a/lib/school/library/page/history.dart +++ /dev/null @@ -1,114 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:sit/design/adaptive/foundation.dart'; -import 'package:sit/design/widgets/card.dart'; -import 'package:sit/l10n/extension.dart'; -import 'package:sit/school/library/init.dart'; -import 'package:sit/utils/error.dart'; - -import '../entity/borrow.dart'; -import '../i18n.dart'; -import '../widgets/book.dart'; -import 'details.dart'; -import 'details.model.dart'; - -class LibraryMyBorrowingHistoryPage extends StatefulWidget { - const LibraryMyBorrowingHistoryPage({super.key}); - - @override - State createState() => _LibraryMyBorrowingHistoryPageState(); -} - -class _LibraryMyBorrowingHistoryPageState extends State { - bool isFetching = false; - List? history = LibraryInit.borrowStorage.getBorrowHistory(); - - @override - void initState() { - super.initState(); - fetch(); - } - - Future fetch() async { - if (!mounted) return; - setState(() { - isFetching = true; - }); - try { - final history = await LibraryInit.borrowService.getHistoryBorrowBookList(); - await LibraryInit.borrowStorage.setBorrowHistory(history); - if (!mounted) return; - setState(() { - this.history = history; - isFetching = false; - }); - } catch (error, stackTrace) { - debugPrintError(error, stackTrace); - if (!mounted) return; - setState(() { - isFetching = false; - }); - } - } - - @override - Widget build(BuildContext context) { - final history = this.history; - return Scaffold( - body: CustomScrollView( - slivers: [ - SliverAppBar( - title: i18n.history.title.text(), - bottom: isFetching - ? const PreferredSize( - preferredSize: Size.fromHeight(4), - child: LinearProgressIndicator(), - ) - : null, - ), - if (history != null) - SliverList.builder( - itemCount: history.length, - itemBuilder: (ctx, i) { - return BookBorrowHistoryCard(history[i]); - }, - ), - ], - ), - ); - } -} - -class BookBorrowHistoryCard extends StatelessWidget { - final BookBorrowingHistoryItem book; - - const BookBorrowHistoryCard( - this.book, { - super.key, - }); - - @override - Widget build(BuildContext context) { - return FilledCard( - clip: Clip.hardEdge, - child: ListTile( - leading: AsyncBookImage(isbn: book.isbn), - title: book.title.text(), - subtitle: [ - book.author.text(), - "${i18n.info.barcode} ${book.barcode}".text(), - "${i18n.info.isbn} ${book.isbn}".text(), - context.formatYmdText(book.processDate).text(), - ].column(mas: MainAxisSize.min, caa: CrossAxisAlignment.start), - trailing: book.operation.l10n().text(), - onTap: () async { - await context.show$Sheet$( - (ctx) => BookDetailsPage( - book: BookModel.fromBorrowHistory(book), - ), - ); - }, - ), - ); - } -} diff --git a/lib/school/library/page/login.dart b/lib/school/library/page/login.dart deleted file mode 100644 index 74d6a835e..000000000 --- a/lib/school/library/page/login.dart +++ /dev/null @@ -1,163 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:go_router/go_router.dart'; -import 'package:sit/credentials/entity/credential.dart'; -import 'package:sit/credentials/init.dart'; -import 'package:sit/login/utils.dart'; -import 'package:sit/login/widgets/forgot_pwd.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:sit/school/library/api.dart'; -import 'package:sit/utils/error.dart'; -import '../init.dart'; -import '../i18n.dart'; - -class LibraryLoginPage extends StatefulWidget { - const LibraryLoginPage({super.key}); - - @override - State createState() => _LibraryLoginPageState(); -} - -class _LibraryLoginPageState extends State { - final initialAccount = CredentialsInit.storage.oaCredentials?.account; - late final $readerId = TextEditingController(text: initialAccount); - final $password = TextEditingController(); - final _formKey = GlobalKey(); - bool isPasswordClear = false; - bool isLoggingIn = false; - - @override - void dispose() { - $readerId.dispose(); - $password.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: () { - // dismiss the keyboard when tap out of TextField. - FocusScope.of(context).requestFocus(FocusNode()); - }, - child: Scaffold( - appBar: AppBar( - title: i18n.login.title.text(), - bottom: isLoggingIn - ? const PreferredSize( - preferredSize: Size.fromHeight(4), - child: LinearProgressIndicator(), - ) - : null, - ), - body: buildBody(), - bottomNavigationBar: const ForgotPasswordButton(url: LibraryApi.forgotLoginPasswordUrl), - ), - ); - } - - Widget buildBody() { - return [ - buildForm(), - SizedBox(height: 10.h), - buildLoginButton(), - ].column(mas: MainAxisSize.min).scrolled(physics: const NeverScrollableScrollPhysics()).padH(25.h).center(); - } - - Widget buildForm() { - return Form( - autovalidateMode: AutovalidateMode.always, - key: _formKey, - child: Column( - children: [ - TextFormField( - controller: $readerId, - textInputAction: TextInputAction.next, - autofocus: true, - readOnly: !kDebugMode && initialAccount != null, - autocorrect: false, - enableSuggestions: false, - decoration: InputDecoration( - labelText: i18n.readerId, - hintText: i18n.login.readerIdHint, - icon: const Icon(Icons.chrome_reader_mode), - ), - ), - TextFormField( - controller: $password, - autofocus: true, - keyboardType: isPasswordClear ? TextInputType.visiblePassword : null, - textInputAction: TextInputAction.send, - contextMenuBuilder: (ctx, state) { - return AdaptiveTextSelectionToolbar.editableText( - editableTextState: state, - ); - }, - autocorrect: false, - enableSuggestions: false, - obscureText: !isPasswordClear, - onFieldSubmitted: (inputted) async { - if (!isLoggingIn) { - await onLogin(); - } - }, - decoration: InputDecoration( - labelText: i18n.login.credentials.password, - hintText: i18n.login.passwordHint, - icon: const Icon(Icons.lock), - suffixIcon: IconButton( - icon: Icon(isPasswordClear ? Icons.visibility : Icons.visibility_off), - onPressed: () { - setState(() { - isPasswordClear = !isPasswordClear; - }); - }, - ), - ), - ), - ], - ), - ); - } - - Widget buildLoginButton() { - return $readerId >> - (ctx, account) => FilledButton.icon( - // Online - onPressed: !isLoggingIn && account.text.isNotEmpty - ? () async { - // un-focus the text field. - FocusScope.of(context).requestFocus(FocusNode()); - await onLogin(); - } - : null, - icon: const Icon(Icons.login), - label: i18n.login.credentials.login.text().padAll(5), - ); - } - - Future onLogin() async { - final credential = Credentials( - account: $readerId.text, - password: $password.text, - ); - try { - if (!mounted) return; - setState(() => isLoggingIn = true); - await LibraryInit.auth.login(credential); - CredentialsInit.storage.libraryCredentials = credential; - if (!mounted) return; - setState(() => isLoggingIn = false); - context.replace("/library/borrowing"); - } catch (error, stackTrace) { - debugPrintError(error, stackTrace); - if (!mounted) return; - setState(() => isLoggingIn = false); - if (error is Exception) { - handleLoginException(context: context, error: error, stackTrace: stackTrace); - } - return; - } - } -} diff --git a/lib/school/library/page/search.dart b/lib/school/library/page/search.dart deleted file mode 100644 index 01746a3f3..000000000 --- a/lib/school/library/page/search.dart +++ /dev/null @@ -1,289 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/material.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:sit/school/library/storage/search.dart'; -import 'package:sit/school/library/widgets/search.dart'; - -import '../entity/search.dart'; -import '../init.dart'; -import '../i18n.dart'; -import 'search_result.dart'; - -class LibrarySearchDelegate extends SearchDelegate { - final $searchMethod = ValueNotifier(SearchMethod.any); - - @override - void dispose() { - $searchMethod.dispose(); - super.dispose(); - } - - void searchByGiving( - BuildContext context, { - required String keyword, - SearchMethod? searchMethod, - }) async { - if (searchMethod != null) { - $searchMethod.value = searchMethod; - } - query = keyword.trim(); - - showSuggestions(context); - await Future.delayed(const Duration(milliseconds: 500)); - if (!context.mounted) return; - showResults(context); - } - - @override - List? buildActions(BuildContext context) { - return [ - IconButton( - icon: const Icon(Icons.clear), - onPressed: () { - query = ''; - showSuggestions(context); - }, - ), - ]; - } - - @override - Widget? buildLeading(BuildContext context) { - return IconButton( - icon: AnimatedIcon( - icon: AnimatedIcons.menu_arrow, - progress: transitionAnimation, - ), - onPressed: () { - if (query.isEmpty) { - close(context, ''); - } else { - query = ''; - showSuggestions(context); - } - }, - ); - } - - @override - void showResults(BuildContext context) { - super.showResults(context); - addHistory(query, $searchMethod.value); - } - - Future addHistory(String keyword, SearchMethod searchMethod) async { - keyword = keyword.trim(); - if (keyword.isEmpty) return; - await LibraryInit.searchStorage.addSearchHistory(SearchHistoryItem( - keyword: keyword, - searchMethod: searchMethod, - time: DateTime.now(), - )); - } - - @override - Widget buildResults(BuildContext context) { - return BookSearchResultWidget( - query: query, - onSearchTap: (method, keyword) { - searchByGiving( - context, - keyword: keyword, - searchMethod: method, - ); - }, - $searchMethod: $searchMethod, - ); - } - - @override - Widget buildSuggestions(BuildContext context) { - return CustomScrollView( - slivers: [ - SliverToBoxAdapter( - child: ($searchMethod >> - (ctx, _) => SearchMethodSwitcher( - selected: $searchMethod.value, - onSelect: (newSelection) { - $searchMethod.value = newSelection; - }, - )) - .sized(h: 40), - ), - SliverToBoxAdapter( - child: LibrarySearchHistoryGroup( - onTap: (method, title) => searchByGiving(context, keyword: title, searchMethod: method), - ), - ), - SliverToBoxAdapter( - child: LibraryTrendsGroup(onTap: (title) => searchByGiving(context, keyword: title)), - ) - ], - ); - } -} - -class LibraryTrendsGroup extends StatefulWidget { - final void Function(String keyword)? onTap; - - const LibraryTrendsGroup({ - super.key, - this.onTap, - }); - - @override - State createState() => _LibraryTrendsGroupState(); -} - -class _LibraryTrendsGroupState extends State { - LibraryTrends? trends = LibraryInit.searchStorage.getTrends(); - bool recentOrTotal = true; - - @override - void initState() { - super.initState(); - fetchHotSearch(); - } - - Future fetchHotSearch() async { - final trends = await LibraryInit.hotSearchService.getTrends(); - await LibraryInit.searchStorage.setTrends(trends); - if (!context.mounted) return; - setState(() { - this.trends = trends; - }); - } - - @override - Widget build(BuildContext context) { - final onTap = widget.onTap; - final trends = this.trends; - return SuggestionItemView( - tileLeading: Icon(recentOrTotal ? Icons.local_fire_department : Icons.people), - title: recentOrTotal ? i18n.searching.trending.text() : i18n.searching.mostPopular.text(), - tileTrailing: IconButton( - icon: const Icon(Icons.swap_horiz), - onPressed: () { - setState(() { - recentOrTotal = !recentOrTotal; - }); - }, - ), - items: recentOrTotal ? trends?.recent30days : trends?.total, - itemBuilder: (ctx, item) { - return ActionChip( - label: item.keyword.text(), - onPressed: () { - onTap?.call(item.keyword); - }, - ); - }, - ); - } -} - -class LibrarySearchHistoryGroup extends StatefulWidget { - final void Function(SearchMethod method, String keyword)? onTap; - - const LibrarySearchHistoryGroup({ - super.key, - this.onTap, - }); - - @override - State createState() => _LibrarySearchHistoryGroupState(); -} - -class _LibrarySearchHistoryGroupState extends State { - List? history = LibraryInit.searchStorage.getSearchHistory(); - final $history = LibraryInit.searchStorage.listenSearchHistory(); - - @override - void initState() { - super.initState(); - $history.addListener(onChange); - } - - @override - void dispose() { - $history.removeListener(onChange); - super.dispose(); - } - - void onChange() { - setState(() { - history = LibraryInit.searchStorage.getSearchHistory(); - }); - } - - @override - Widget build(BuildContext context) { - final onTap = widget.onTap; - final history = this.history; - return SuggestionItemView( - tileLeading: const Icon(Icons.history), - title: i18n.searching.searchHistory.text(), - items: history, - tileTrailing: IconButton( - icon: const Icon(Icons.delete), - onPressed: history?.isNotEmpty == true - ? () { - LibraryInit.searchStorage.setSearchHistory(null); - } - : null, - ), - itemBuilder: (ctx, item) { - return ActionChip( - label: item.keyword.text(), - onPressed: () { - onTap?.call(item.searchMethod, item.keyword); - }, - ); - }, - ); - } -} - -class SuggestionItemView extends StatelessWidget { - final Widget title; - final List? items; - final Widget Function(BuildContext context, T item) itemBuilder; - final int limitLength; - final Widget? tileTrailing; - final Widget? tileLeading; - - const SuggestionItemView({ - super.key, - required this.title, - required this.items, - required this.itemBuilder, - this.limitLength = 20, - this.tileTrailing, - this.tileLeading, - }); - - @override - Widget build(BuildContext context) { - final items = this.items; - return [ - ListTile( - leading: tileLeading, - title: title, - trailing: tileTrailing, - ), - AnimatedSize( - alignment: Alignment.topCenter, - duration: Durations.long2, - curve: Curves.fastEaseInToSlowEaseOut, - child: items != null - ? items - .sublist(0, min(items.length, limitLength)) - .map((e) => itemBuilder(context, e)) - .toList(growable: false) - .wrap(spacing: 4) - : const SizedBox(), - ).padH(8), - ].column(caa: CrossAxisAlignment.start, mas: MainAxisSize.min); - } -} diff --git a/lib/school/library/page/search_result.dart b/lib/school/library/page/search_result.dart deleted file mode 100644 index 32a9923ec..000000000 --- a/lib/school/library/page/search_result.dart +++ /dev/null @@ -1,254 +0,0 @@ -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:sit/design/adaptive/foundation.dart'; -import 'package:sit/design/widgets/card.dart'; -import 'package:sit/design/widgets/common.dart'; -import 'package:sit/school/library/page/details.model.dart'; -import 'package:sit/school/library/widgets/book.dart'; -import 'package:sit/school/library/widgets/search.dart'; -import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; -import 'package:sit/utils/error.dart'; - -import '../entity/book.dart'; -import '../entity/search.dart'; -import '../init.dart'; -import '../i18n.dart'; -import 'details.dart'; - -typedef BookSearchCallback = void Function(SearchMethod method, String keyword); - -class BookSearchResultWidget extends StatefulWidget { - final String query; - - final ValueNotifier $searchMethod; - - final BookSearchCallback? onSearchTap; - - const BookSearchResultWidget({ - required this.query, - required this.$searchMethod, - super.key, - this.onSearchTap, - }); - - @override - State createState() => _BookSearchResultWidgetState(); -} - -class _BookSearchResultWidgetState extends State with AutomaticKeepAliveClientMixin { - @override - bool get wantKeepAlive => true; - final scrollController = ScrollController(); - static const sizePerPage = 30; - BookSearchResult? lastResult; - List? books; - bool isFetching = false; - - @override - void initState() { - super.initState(); - // Fetch the first page - fetchNextPage(); - scrollController.addListener(() { - if (scrollController.position.pixels == scrollController.position.maxScrollExtent) { - fetchNextPage(); - } - }); - } - - @override - void dispose() { - scrollController.dispose(); - super.dispose(); - } - - Future fetchNextPage() async { - if (isFetching) return; - final lastResult = this.lastResult; - if (lastResult != null && lastResult.currentPage > lastResult.totalPage) return; - setState(() { - isFetching = true; - }); - try { - final searchResult = await LibraryInit.bookSearch.search( - keyword: widget.query, - rows: sizePerPage, - page: lastResult != null ? lastResult.currentPage + 1 : 0, - searchMethod: widget.$searchMethod.value, - ); - if (!mounted) return; - setState(() { - final books = this.books ??= []; - books.addAll(searchResult.books); - this.lastResult = searchResult; - isFetching = false; - }); - } catch (error, stackTrace) { - debugPrintError(error, stackTrace); - if (!mounted) return; - setState(() { - isFetching = false; - }); - } - } - - @override - Widget build(BuildContext context) { - super.build(context); - final books = this.books; - return Scaffold( - body: CustomScrollView( - slivers: [ - SliverToBoxAdapter( - child: buildSearchMethodSwitcher().sized(h: 40), - ), - if (books != null) - if (books.isEmpty) - SliverFillRemaining( - child: LeavingBlank( - icon: Icons.inbox_outlined, - desc: i18n.noBooks, - ), - ) - else - buildGrid(books) - ], - ), - bottomNavigationBar: isFetching - ? const PreferredSize( - preferredSize: Size.fromHeight(4), - child: LinearProgressIndicator(), - ) - : null, - ); - } - - Widget buildGrid(List books) { - final onSearchTap = widget.onSearchTap; - return SliverFillRemaining( - child: MasonryGridView.extent( - controller: scrollController, - maxCrossAxisExtent: 280, - itemCount: books.length, - itemBuilder: (context, i) { - final book = books[i]; - return BookTile( - book: book, - onSearchTap: onSearchTap, - onTap: () async { - await context.show$Sheet$( - (ctx) => BookDetailsPage( - book: BookModel.fromBook(book), - onSearchTap: onSearchTap == null - ? null - : (method, keyword) { - // pop the sheet - ctx.pop(); - onSearchTap(method, keyword); - }, - ), - ); - }, - ); - }, - ), - ); - } - - Widget buildSearchMethodSwitcher() { - return widget.$searchMethod >> - (ctx, _) => SearchMethodSwitcher( - selected: widget.$searchMethod.value, - onSelect: (newSelection) { - widget.$searchMethod.value = newSelection; - setState(() { - books = null; - lastResult = null; - }); - fetchNextPage(); - }, - ); - } -} - -class BookTile extends StatelessWidget { - final Book book; - final void Function()? onTap; - final BookSearchCallback? onSearchTap; - - const BookTile({ - super.key, - this.onSearchTap, - this.onTap, - required this.book, - }); - - @override - Widget build(BuildContext context) { - final book = this.book; - return FilledCard( - clip: Clip.hardEdge, - child: InkWell( - onTap: onTap, - child: [ - AsyncBookImage(isbn: book.isbn), - [ - book.title.text(style: context.textTheme.titleMedium), - buildAuthor(context), - "${i18n.info.callNumber} ${book.callNumber}".text(), - buildPublisher(context) - ].column(mas: MainAxisSize.min).padSymmetric(v: 2, h: 4), - ].column(mas: MainAxisSize.min), - ), - ); - } - - Widget buildAuthor(BuildContext context) { - return RichText( - text: TextSpan( - children: [ - buildSearchableField( - method: SearchMethod.author, - text: book.author, - ), - ], - style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceVariant), - ), - ); - } - - Widget buildPublisher(BuildContext context) { - return RichText( - text: TextSpan( - children: [ - buildSearchableField( - method: SearchMethod.publisher, - text: book.publisher, - ), - const TextSpan(text: " "), - TextSpan(text: book.publishDate), - ], - style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceVariant), - ), - ); - } - - TextSpan buildSearchableField({ - required SearchMethod method, - required String text, - }) { - final onSearchTap = this.onSearchTap; - return TextSpan( - text: text, - style: const TextStyle(color: Colors.blue), - recognizer: onSearchTap == null - ? null - : (TapGestureRecognizer() - ..onTap = () async { - onSearchTap(method, text); - }), - ); - } -} diff --git a/lib/school/library/service/auth.dart b/lib/school/library/service/auth.dart deleted file mode 100644 index 5dac7ad82..000000000 --- a/lib/school/library/service/auth.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'dart:convert'; - -import 'package:crypto/crypto.dart'; -import 'package:dio/dio.dart'; -import 'package:encrypt/encrypt.dart'; -import 'package:sit/credentials/entity/credential.dart'; -import 'package:sit/credentials/error.dart'; -import 'package:sit/init.dart'; -import 'package:sit/utils/dio.dart'; - -const _opacUrl = 'http://210.35.66.106/opac'; -const _pemUrl = '$_opacUrl/certificate/pem'; -const _loginUrl = '$_opacUrl/reader/doLogin'; - -class LibraryAuthService { - Dio get dio => Init.dio; - - const LibraryAuthService(); - - Future login(Credentials credentials) async { - final response = await _login(credentials.account, credentials.password); - final content = response.data.toString(); - if (content.contains('用户名或密码错误')) { - throw CredentialsException( - type: CredentialsErrorType.accountPassword, - message: content, - ); - } - // TODO: Handle other exceptions - return response; - } - - Future _login(String username, String password) async { - final rdPasswd = await _encryptPassword(password); - final response = await dio.post( - _loginUrl, - data: { - 'vToken': '', - 'rdLoginId': username, - 'p': '', - 'rdPasswd': rdPasswd, - 'returnUrl': '', - 'password': '', - }, - options: disableRedirectFormEncodedOptions(), - ); - return processRedirect(dio, response); - } - - Future _encryptPassword(String password) async { - String hashedPwd = md5.convert(const Utf8Encoder().convert(password)).toString(); - final pk = await _getRSAPublicKey(); - final encrypter = Encrypter(RSA(publicKey: pk)); - final String encryptedPwd = encrypter.encrypt(hashedPwd).base64; - return encryptedPwd; - } - - Future _getRSAPublicKey() async { - final pemResponse = await dio.get( - _pemUrl, - queryParameters: { - "checkCode": "1opac", - }, - ); - String publicKeyStr = pemResponse.data; - final pemFileContent = '-----BEGIN PUBLIC KEY-----\n$publicKeyStr\n-----END PUBLIC KEY-----'; - - final parser = RSAKeyParser(); - return parser.parse(pemFileContent); - } -} diff --git a/lib/school/library/service/book.dart b/lib/school/library/service/book.dart deleted file mode 100644 index 146c212b6..000000000 --- a/lib/school/library/service/book.dart +++ /dev/null @@ -1,97 +0,0 @@ -import 'package:beautiful_soup_dart/beautiful_soup.dart'; -import 'package:dio/dio.dart'; -import 'package:sit/init.dart'; - -import 'package:sit/session/library.dart'; - -import '../entity/book.dart'; -import '../api.dart'; -import '../entity/search.dart'; - -class BookSearchService { - LibrarySession get session => Init.librarySession; - - const BookSearchService(); - - Future search({ - String keyword = '', - int rows = 10, - int page = 1, - SearchMethod searchMethod = SearchMethod.any, - SortMethod sortMethod = SortMethod.matchScore, - SortOrder sortOrder = SortOrder.desc, - }) async { - final response = await session.request( - LibraryApi.searchUrl, - para: { - 'q': keyword, - 'searchType': 'standard', - 'isFacet': 'true', - 'view': 'standard', - 'searchWay': searchMethod.internalQueryParameter, - 'rows': rows.toString(), - 'sortWay': sortMethod.internalQueryParameter, - 'sortOrder': sortOrder.internalQueryParameter, - 'hasholding': '1', - 'searchWay0': 'marc', - 'logical0': 'AND', - 'page': page.toString(), - }, - options: Options( - method: "GET", - ), - ); - - final soup = BeautifulSoup(response.data); - - final currentPage = soup.find('b', selector: '.meneame > b')?.text.trim() ?? '$page'; - final resultNumAndTime = soup - .find( - 'div', - selector: '#search_meta > div:nth-child(1)', - )! - .text; - final resultCount = - int.parse(RegExp(r'检索到: (\S*) 条结果').allMatches(resultNumAndTime).first.group(1)!.replaceAll(',', '')); - final useTime = double.parse(RegExp(r'检索时间: (\S*) 秒').allMatches(resultNumAndTime).first.group(1)!); - final totalPages = soup.find('div', class_: 'meneame')?.find('span', class_: 'disabled')?.text.trim(); - final booksRaw = soup.find('table', class_: 'resultTable')?.findAll('tr'); - if (totalPages == null || booksRaw == null) { - return BookSearchResult.empty(useTime: useTime); - } - final books = booksRaw.map((e) => _parseBook(e)).toList(); - return BookSearchResult( - resultCount: resultCount, - useTime: useTime, - currentPage: int.parse(currentPage), - totalPage: int.parse(totalPages.substring(1, totalPages.length - 1).trim().replaceAll(',', '')), - books: books, - ); - } - - static Book _parseBook(Bs4Element e) { - // 获得图书信息 - String getBookInfo(String name, String selector) { - return e.find(name, selector: selector)!.text.trim(); - } - - final bookCoverImage = e.find('img', class_: 'bookcover_img')!; - final author = getBookInfo('a', '.author-link'); - final bookId = bookCoverImage.attributes['bookrecno']!; - final isbn = bookCoverImage.attributes['isbn']!; - final callNo = getBookInfo('span', '.callnosSpan'); - final publishDate = getBookInfo('div', 'div').split('出版日期:')[1].split('\n')[0].trim(); - - final publisher = getBookInfo('a', '.publisher-link'); - final title = getBookInfo('a', '.title-link'); - return Book( - bookId: bookId, - isbn: isbn, - title: title, - author: author, - publisher: publisher, - publishDate: publishDate, - callNumber: callNo, - ); - } -} diff --git a/lib/school/library/service/borrow.dart b/lib/school/library/service/borrow.dart deleted file mode 100644 index 1d198260d..000000000 --- a/lib/school/library/service/borrow.dart +++ /dev/null @@ -1,131 +0,0 @@ -import 'package:beautiful_soup_dart/beautiful_soup.dart'; -import 'package:dio/dio.dart'; -import 'package:intl/intl.dart'; -import 'package:sit/init.dart'; - -import 'package:sit/session/library.dart'; - -import '../entity/borrow.dart'; -import '../api.dart'; - -final _historyDateFormat = DateFormat('yyyy-MM-dd'); - -class LibraryBorrowService { - LibrarySession get session => Init.librarySession; - - const LibraryBorrowService(); - - Future> getHistoryBorrowBookList() async { - final response = await session.request( - LibraryApi.historyLoanListUrl, - para: { - 'page': 1.toString(), - 'rows': 99999.toString(), - }, - options: Options( - method: "GET", - ), - ); - final html = BeautifulSoup(response.data); - final table = html.find('table', id: 'contentTable'); - if (table == null) { - return const []; - } - return table.findAll('tr').where((e) => e.id != 'contentHeader').map((e) { - final columns = e.findAll('td'); - final columnsText = columns.map((e) => e.text.trim()).toList(); - return BookBorrowingHistoryItem( - bookId: columns[0].find('input')!.attributes['value']!, - operation: _parseOpType(columnsText[0]), - barcode: columnsText[1], - title: columnsText[2], - isbn: columnsText[3], - author: columnsText[4], - callNumber: columnsText[5], - location: columnsText[6], - type: columnsText[7], - processDate: _historyDateFormat.parse(columnsText[8]), - ); - }).toList(); - } - - BookBorrowingHistoryOperation _parseOpType(String text) { - if (text == "还书") { - return BookBorrowingHistoryOperation.returning; - } else if (text == "借书") { - return BookBorrowingHistoryOperation.borrowing; - } - return BookBorrowingHistoryOperation.borrowing; - } - - Future> getMyBorrowBookList() async { - final response = await session.request( - LibraryApi.currentLoanListUrl, - para: { - 'page': 1.toString(), - 'rows': 99999.toString(), - }, - options: Options( - method: "GET", - ), - ); - final html = BeautifulSoup(response.data); - final table = html.find('table', id: 'contentTable'); - if (table == null) { - return const []; - } - return table.findAll('tr').where((e) => e.id != 'contentHeader').map((e) { - final columns = e.findAll('td'); - final columnsText = columns.map((e) => e.text.trim()).toList(); - final dataFormat = DateFormat('yyyy-MM-dd'); - return BorrowedBookItem( - bookId: columns[0].find('input')!.attributes['value']!, - barcode: columnsText[0], - title: columnsText[1], - isbn: columnsText[2], - author: columnsText[3], - callNumber: columnsText[4], - location: columnsText[5], - type: columnsText[6], - borrowDate: dataFormat.parse(columnsText[7]), - expireDate: dataFormat.parse(columnsText[8]), - ); - }).toList(); - } - - Future renewBook({ - required List barcodeList, - bool renewAll = false, - }) async { - await session.request( - LibraryApi.renewList, - options: Options( - method: "GET", - ), - ); - final listRes = await session.request( - LibraryApi.renewList, - options: Options( - method: "GET", - ), - ); - final listHtml = BeautifulSoup(listRes.data); - final pdsToken = listHtml.find('input', attrs: {'name': 'pdsToken'})!.attributes['value'] ?? ''; - final renewRes = await session.request( - LibraryApi.doRenewUrl, - data: FormData.fromMap({ - 'pdsToken': pdsToken, - 'barcodeList': barcodeList.join(','), - 'furl': '/opac/loan/renewList', - 'renewAll': renewAll ? 'all' : '', - }), - options: Options( - method: "POST", - contentType: Headers.formUrlEncodedContentType, - ), - ); - final renewHtml = BeautifulSoup(renewRes.data); - final result = renewHtml.find('div', id: 'content')!.text; - return result.replaceAll(RegExp(r"\s+"), ""); - } -} diff --git a/lib/school/library/service/collection.dart b/lib/school/library/service/collection.dart deleted file mode 100644 index 3835f4ee7..000000000 --- a/lib/school/library/service/collection.dart +++ /dev/null @@ -1,87 +0,0 @@ -import 'package:beautiful_soup_dart/beautiful_soup.dart'; -import 'package:dio/dio.dart'; -import 'package:sit/init.dart'; - -import 'package:sit/session/library.dart'; - -import '../entity/collection.dart'; -import '../api.dart'; - -class LibraryCollectionInfoService { - LibrarySession get session => Init.librarySession; - - const LibraryCollectionInfoService(); - - Future> queryByBookId(String bookId) async { - final response = await session.request( - '${LibraryApi.bookCollectionUrl}/$bookId', - options: Options( - method: "GET", - ), - ); - - final raw = BookCollectionInfo.fromJson(response.data); - final result = raw.holdingList.map((rawHoldingItem) { - final bookRecordId = rawHoldingItem.bookRecordId; - final bookId = rawHoldingItem.bookId; - final stateTypeName = raw.holdStateMap[rawHoldingItem.stateType.toString()]!.stateName; - final barcode = rawHoldingItem.barcode; - final callNo = rawHoldingItem.callNumber; - final originLibrary = raw.libraryCodeMap[rawHoldingItem.originLibraryCode]!; - final originLocation = raw.locationMap[rawHoldingItem.originLocationCode]!; - final currentLibrary = raw.libraryCodeMap[rawHoldingItem.currentLibraryCode]!; - final currentLocation = raw.locationMap[rawHoldingItem.currentLocationCode]!; - final circulateTypeName = raw.circulateTypeMap[rawHoldingItem.circulateType]!.name; - final circulateTypeDescription = raw.circulateTypeMap[rawHoldingItem.circulateType]!.description; - final registerDate = DateTime.parse(rawHoldingItem.registerDate); - final inDate = DateTime.parse(rawHoldingItem.inDate); - final singlePrice = rawHoldingItem.singlePrice; - final totalPrice = rawHoldingItem.totalPrice; - return BookCollection( - bookRecordId: bookRecordId, - bookId: bookId, - stateTypeName: stateTypeName, - barcode: barcode, - callNumber: callNo, - originLibrary: originLibrary, - originLocation: originLocation, - currentLibrary: currentLibrary, - currentLocation: currentLocation, - circulateTypeName: circulateTypeName, - circulateTypeDescription: circulateTypeDescription, - registerDate: registerDate, - inDate: inDate, - singlePrice: singlePrice, - totalPrice: totalPrice, - ); - }).toList(); - return result; - } - - /// 搜索附近的书的id号 - Future> searchNearBookIdList(String bookId) async { - final response = await session.request( - LibraryApi.virtualBookshelfUrl, - para: { - 'bookrecno': bookId, - - // 1 表示不出现同一本书的重复书籍 - 'holding': '1', - }, - options: Options( - method: "GET", - ), - ); - final soup = BeautifulSoup(response.data); - return soup - .findAll( - 'a', - attrs: {'target': '_blank'}, - ) - .map( - (e) => e.attributes['href']!, - ) - .map((e) => e.split('book/')[1]) - .toList(); - } -} diff --git a/lib/school/library/service/collection_preview.dart b/lib/school/library/service/collection_preview.dart deleted file mode 100644 index c4ca6a849..000000000 --- a/lib/school/library/service/collection_preview.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:dio/dio.dart'; -import 'package:sit/init.dart'; - -import 'package:sit/session/library.dart'; - -import '../entity/collection_preview.dart'; -import '../api.dart'; - -class LibraryCollectionPreviewService { - LibrarySession get session => Init.librarySession; - - const LibraryCollectionPreviewService(); - - Future>> getCollectionPreviews( - List bookIdList, - ) async { - final response = await session.request( - LibraryApi.bookCollectionPreviewsUrl, - para: { - 'bookrecnos': bookIdList.join(','), - 'curLibcodes': '', - 'return_fmt': 'json', - }, - options: Options( - method: "GET", - ), - ); - final json = response.data; - final previewsRaw = json['previews'] as Map?; - if (previewsRaw == null) return >{}; - final previews = previewsRaw.map((k, e) => - MapEntry(k, (e as List).map((e) => BookCollectionItem.fromJson(e as Map)).toList())); - return previews; - } -} diff --git a/lib/school/library/service/details.dart b/lib/school/library/service/details.dart deleted file mode 100644 index 2c8033865..000000000 --- a/lib/school/library/service/details.dart +++ /dev/null @@ -1,81 +0,0 @@ -import 'dart:collection'; - -import 'package:beautiful_soup_dart/beautiful_soup.dart'; -import 'package:dio/dio.dart'; -import 'package:sit/init.dart'; - -import 'package:sit/session/library.dart'; - -import '../api.dart'; -import '../entity/book.dart'; - -class BookDetailsService { - LibrarySession get session => Init.librarySession; - - const BookDetailsService(); - - Future query(String bookId) async { - final response = await session.request( - '${LibraryApi.bookUrl}/$bookId', - options: Options( - method: "GET", - ), - ); - final html = BeautifulSoup(response.data); - final detailItems = html - .find('table', id: 'bookInfoTable')! - .findAll('tr') - .map( - (e) => e - .findAll('td') - .map( - (e) => e.text.replaceAll(RegExp(r'\s*'), ''), - ) - .toList(), - ) - .where( - (element) { - if (element.isEmpty) { - return false; - } - String e1 = element[0]; - - // 过滤包含这些关键字的条目 - for (final keyword in ['分享', '相关', '随书']) { - if (e1.contains(keyword)) return false; - } - - return true; - }, - ).toList(); - - final rawDetails = LinkedHashMap.fromEntries( - detailItems.sublist(1).map( - (e) => MapEntry( - e[0].substring(0, e[0].length - 1), - e[1], - ), - ), - ); - return parseBookDetails(rawDetails); - } - - BookDetails parseBookDetails(Map details) { - final isbnAndPrice = details['ISBN']!.split('价格:'); - details["ISBN"] = isbnAndPrice[0]; - final price = isbnAndPrice.elementAtOrNull(1); - if (price != null) { - details["价格"] = price; - } - - final classAndEdition = details['中图分类法']!.split('版次:'); - details["中图分类法"] = classAndEdition[0]; - if (classAndEdition.length > 1) { - details["版次"] = classAndEdition[1]; - } - - return BookDetails( - details: details, - ); - } -} diff --git a/lib/school/library/service/image_search.dart b/lib/school/library/service/image_search.dart deleted file mode 100644 index 6b09e4082..000000000 --- a/lib/school/library/service/image_search.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'dart:convert'; - -import 'package:dio/dio.dart'; -import 'package:sit/init.dart'; - -import 'package:sit/session/library.dart'; - -import '../entity/image.dart'; -import '../api.dart'; - -/// 本类提供了一系列,通过查询图书图片的方法,返回结果类型为字典,以ISBN为键 -class BookImageSearchService { - LibrarySession get session => Init.librarySession; - - Dio get dio => Init.dio; - - const BookImageSearchService(); - - /// The result isbn doesn't have hyphen `-` - Future> searchByIsbnList(List isbnList) async { - final response = await dio.request( - LibraryApi.bookImageInfoUrl, - queryParameters: { - 'glc': 'U1SH021060', - 'cmdACT': 'getImages', - 'type': '0', - 'isbns': isbnList.join(','), - }, - options: Options( - responseType: ResponseType.plain, - method: "GET", - ), - ); - var resStr = (response.data as String).trim(); - resStr = resStr.substring(1, resStr.length - 1); - final result = {}; - final resultRaw = jsonDecode(resStr)['result'] as List; - final images = resultRaw.map((e) => BookImage.fromJson(e)); - for (final image in images) { - result[image.isbn] = image; - } - return result; - } -} diff --git a/lib/school/library/service/trends.dart b/lib/school/library/service/trends.dart deleted file mode 100644 index 858ccf12c..000000000 --- a/lib/school/library/service/trends.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:beautiful_soup_dart/beautiful_soup.dart'; -import 'package:dio/dio.dart'; -import 'package:sit/init.dart'; - -import 'package:sit/session/library.dart'; - -import '../api.dart'; -import '../entity/search.dart'; - -class LibraryTrendsService { - LibrarySession get session => Init.librarySession; - - const LibraryTrendsService(); - - LibraryTrendsItem _parse(String rawText) { - final texts = rawText.split('(').map((e) => e.trim()).toList(); - final title = texts.sublist(0, texts.length - 1).join('('); - final numWithRight = texts[texts.length - 1]; - final numText = numWithRight.substring(0, numWithRight.length - 1); - return LibraryTrendsItem( - keyword: title, - count: int.parse(numText), - ); - } - - Future getTrends() async { - final response = await session.request( - LibraryApi.hotSearchUrl, - options: Options( - method: "GET", - ), - ); - final soup = BeautifulSoup(response.data); - final fieldsets = soup.findAll('fieldset'); - - List getHotSearchItems(Bs4Element fieldset) { - return fieldset.findAll('a').map((e) => _parse(e.text)).toList(); - } - - return LibraryTrends( - recent30days: getHotSearchItems(fieldsets[0]), - total: getHotSearchItems(fieldsets[1]), - ); - } -} diff --git a/lib/school/library/storage/book.dart b/lib/school/library/storage/book.dart deleted file mode 100644 index c572845ae..000000000 --- a/lib/school/library/storage/book.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:hive/hive.dart'; -import 'package:sit/storage/hive/init.dart'; - -import '../entity/book.dart'; - -class _K { - static const ns = "/library/books"; - - static String info(String bookId) => "$ns/$bookId"; - - static String details(String bookId) => "$ns/$bookId"; -} - -class LibraryBookStorage { - Box get box => HiveInit.library; - - const LibraryBookStorage(); - - Book? getBook(String bookId) => box.get(_K.info(bookId)); - - Future setBook(String bookId, Book? value) => box.put(_K.info(bookId), value); - - BookDetails? getBookDetails(String bookId) => box.get(_K.details(bookId)); - - Future setBookDetails(String bookId, BookDetails? value) => box.put(_K.details(bookId), value); -} diff --git a/lib/school/library/storage/borrow.dart b/lib/school/library/storage/borrow.dart deleted file mode 100644 index b00399dc9..000000000 --- a/lib/school/library/storage/borrow.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:hive_flutter/hive_flutter.dart'; -import 'package:sit/storage/hive/init.dart'; - -import '../entity/borrow.dart'; - -class _K { - static const ns = "/library/borrow"; - static const borrowed = "$ns/borrowed"; - static const borrowHistory = "$ns/borrowHistory"; -} - -class LibraryBorrowStorage { - Box get box => HiveInit.library; - - const LibraryBorrowStorage(); - - List? getBorrowedBooks() => (box.get(_K.borrowed) as List?)?.cast(); - - Future setBorrowedBooks(List? value) => box.put(_K.borrowed, value); - - Listenable listenBorrowedBooks() => box.listenable(keys: [_K.borrowed]); - - List? getBorrowHistory() => - (box.get(_K.borrowHistory) as List?)?.cast(); - - Future setBorrowHistory(List? value) => box.put(_K.borrowHistory, value); -} diff --git a/lib/school/library/storage/browse.dart b/lib/school/library/storage/browse.dart deleted file mode 100644 index a9e4e76b5..000000000 --- a/lib/school/library/storage/browse.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:hive/hive.dart'; -import 'package:sit/storage/hive/init.dart'; - -class _K { - static const ns = "/library/browse"; - static const browseHistory = "$ns/browseHistory"; - static const favorite = "$ns/favorite"; -} - -class LibraryBrowseStorage { - Box get box => HiveInit.library; - - const LibraryBrowseStorage(); - - /// a list of book ID - List? getBrowseHistory() => (box.get(_K.browseHistory) as List?)?.cast(); - - /// a list of book ID - Future setBrowseHistory(String bookId, List? value) => box.put(_K.browseHistory, value); - - /// a list of book ID - List? getFavorite() => (box.get(_K.favorite) as List?)?.cast(); - - /// a list of book ID - Future setFavorite(String bookId, List? value) => box.put(_K.favorite, value); -} diff --git a/lib/school/library/storage/image.dart b/lib/school/library/storage/image.dart deleted file mode 100644 index a71cc1232..000000000 --- a/lib/school/library/storage/image.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:hive/hive.dart'; -import 'package:sit/storage/hive/init.dart'; - -import '../entity/image.dart'; - -class _K { - static const ns = "/library/images"; - - static String image(String isbn) => "$ns/$isbn"; -} - -class LibraryImageStorage { - Box get box => HiveInit.library; - - const LibraryImageStorage(); - - BookImage? getImage(String isbn) => box.get(_K.image(isbn)); - - Future setImage(String isbn, BookImage? value) => box.put(_K.image(isbn), value); -} diff --git a/lib/school/library/storage/search.dart b/lib/school/library/storage/search.dart deleted file mode 100644 index 0937817e1..000000000 --- a/lib/school/library/storage/search.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:hive_flutter/hive_flutter.dart'; -import 'package:sit/storage/hive/init.dart'; -import 'package:sit/utils/collection.dart'; -import 'package:sit/utils/json.dart'; -import '../entity/search.dart'; - -class _K { - static const ns = "/search"; - static const searchHistory = "$ns/searchHistory"; - static const trends = "$ns/trends"; -} - -class LibrarySearchStorage { - Box get box => HiveInit.library; - - const LibrarySearchStorage(); - - LibraryTrends? getTrends() => decodeJsonObject(box.get(_K.trends), (obj) => LibraryTrends.fromJson(obj)); - - Future setTrends(LibraryTrends value) => box.put(_K.trends, encodeJsonObject(value)); - - List? getSearchHistory() => - decodeJsonList(box.get(_K.searchHistory), (obj) => SearchHistoryItem.fromJson(obj)); - - Future setSearchHistory(List? value) => box.put(_K.searchHistory, encodeJsonList(value)); - - ValueListenable listenSearchHistory() => box.listenable(keys: [_K.searchHistory]); -} - -extension LibrarySearchStorageX on LibrarySearchStorage { - Future addSearchHistory(SearchHistoryItem item) async { - final all = getSearchHistory() ?? []; - all.add(item); - all.sort((a, b) => b.time.compareTo(a.time)); - all.distinctBy((item) => item.keyword); - await setSearchHistory(all); - } -} diff --git a/lib/school/library/utils.dart b/lib/school/library/utils.dart deleted file mode 100644 index 1b50359ca..000000000 --- a/lib/school/library/utils.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:sit/design/adaptive/dialog.dart'; -import 'package:sit/utils/error.dart'; - -import 'init.dart'; -import 'i18n.dart'; - -Future renewBorrowedBook(BuildContext context, String barcode) async { - try { - final result = await LibraryInit.borrowService.renewBook(barcodeList: [barcode]); - if (!context.mounted) return; - await context.showTip(title: i18n.borrowing.renew, ok: i18n.ok, desc: result); - } catch (error, stackTrace) { - debugPrintError(error, stackTrace); - } -} diff --git a/lib/school/library/widgets/book.dart b/lib/school/library/widgets/book.dart deleted file mode 100644 index 1b229af44..000000000 --- a/lib/school/library/widgets/book.dart +++ /dev/null @@ -1,68 +0,0 @@ -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:flutter/material.dart'; -import 'package:sit/utils/error.dart'; - -import '../aggregated.dart'; -import '../entity/image.dart'; - -class AsyncBookImage extends StatefulWidget { - final String isbn; - final ValueChanged? onHasImageChanged; - - const AsyncBookImage({ - super.key, - required this.isbn, - this.onHasImageChanged, - }); - - @override - State createState() => _AsyncBookImageState(); -} - -class _AsyncBookImageState extends State { - late BookImage? image = LibraryAggregated.getCachedBookImageByIsbn(widget.isbn); - - @override - void initState() { - super.initState(); - fetch(); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - widget.onHasImageChanged?.call(image != null); - }); - } - - Future fetch() async { - try { - final image = await LibraryAggregated.fetchBookImage(isbn: widget.isbn); - if (!mounted) return; - setState(() { - this.image = image; - }); - } catch (error, stackTrace) { - debugPrintError(error, stackTrace); - } - } - - @override - Widget build(BuildContext context) { - return AnimatedSize( - duration: Durations.long2, - curve: Curves.fastEaseInToSlowEaseOut, - child: buildContext(), - ); - } - - Widget buildContext() { - final image = this.image; - if (image == null) return const SizedBox(); - return CachedNetworkImage( - fit: BoxFit.cover, - imageUrl: image.resourceUrl, - placeholder: (context, url) => const SizedBox(), - errorWidget: (context, url, error) => const SizedBox(), - errorListener: (error) { - widget.onHasImageChanged?.call(false); - }, - ); - } -} diff --git a/lib/school/library/widgets/search.dart b/lib/school/library/widgets/search.dart deleted file mode 100644 index 9dec8fae8..000000000 --- a/lib/school/library/widgets/search.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:rettulf/rettulf.dart'; - -import '../entity/search.dart'; - -const _searchMethods = [ - SearchMethod.any, - SearchMethod.title, - SearchMethod.author, - SearchMethod.publisher, - SearchMethod.subject, - SearchMethod.isbn, -]; - -class SearchMethodSwitcher extends StatelessWidget { - final List all; - final SearchMethod selected; - final ValueChanged? onSelect; - - const SearchMethodSwitcher({ - super.key, - this.all = _searchMethods, - required this.selected, - this.onSelect, - }); - - @override - Widget build(BuildContext context) { - return ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: all.length, - itemBuilder: (ctx, i) { - final method = all[i]; - return ChoiceChip( - label: method.l10nName().text(), - selected: selected == method, - onSelected: (value) { - if (value) { - onSelect?.call(method); - } - }, - ).padH(4); - }, - ); - } -} diff --git a/lib/school/oa_announce/entity/announce.dart b/lib/school/oa_announce/entity/announce.dart deleted file mode 100644 index 6cebeac9b..000000000 --- a/lib/school/oa_announce/entity/announce.dart +++ /dev/null @@ -1,192 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:sit/credentials/entity/user_type.dart'; -import 'package:sit/storage/hive/type_id.dart'; - -part 'announce.g.dart'; - -/// 通知分类 -enum OaAnnounceCat { - // ug, pg - studentAffairs('学生事务', 'pe2362'), - // ug - learning('学习课堂', 'pe2364'), - // ug, pg - collegeNotification('二级学院通知', 'pe2368'), - // ug - culture('校园文化', 'pe2366'), - // ug, pg - announcement('公告信息', 'pe2367'), - // ug, pg - life('生活服务', 'pe2365'), - // ug - download('文件下载专区', 'pe2382'), - // pg - training('培养信息', 'pe3442'), - // pg - academicReport('学术报告', 'pe3422'); - - /// 分类名 - final String catName; - - /// 分类代号(OA上命名为pen,以pe打头) - final String internalId; - - String l10nName() => "oaAnnounce.oaAnnounceCat.$name".tr(); - - static String allCatL10n() => "oaAnnounce.oaAnnounceCat.all".tr(); - - const OaAnnounceCat(this.catName, this.internalId); - - static const common = [ - OaAnnounceCat.studentAffairs, - OaAnnounceCat.announcement, - OaAnnounceCat.collegeNotification, - OaAnnounceCat.life, - ]; - static const undergraduate = [ - OaAnnounceCat.learning, - OaAnnounceCat.studentAffairs, - OaAnnounceCat.announcement, - OaAnnounceCat.culture, - OaAnnounceCat.download, - OaAnnounceCat.collegeNotification, - OaAnnounceCat.life, - OaAnnounceCat.academicReport, - ]; - static const postgraduate = [ - OaAnnounceCat.studentAffairs, - OaAnnounceCat.announcement, - OaAnnounceCat.training, - OaAnnounceCat.collegeNotification, - OaAnnounceCat.life, - ]; - - static List resolve(OaUserType? userType) { - return switch (userType) { - OaUserType.undergraduate => undergraduate, - OaUserType.postgraduate => postgraduate, - _ => common, - }; - } -} - -/// 某篇通知的记录信息,根据该信息可寻找到对应文章 -@HiveType(typeId: CacheHiveType.oaAnnounceRecord) -class OaAnnounceRecord { - /// 标题 - @HiveField(0) - final String title; - - /// 文章id - @HiveField(1) - final String uuid; - - /// 目录id - @HiveField(2) - final String catalogId; - - /// 发布时间 - @HiveField(3) - final DateTime dateTime; - - /// 发布部门 - @HiveField(4) - final List departments; - - const OaAnnounceRecord({ - required this.title, - required this.uuid, - required this.catalogId, - required this.dateTime, - required this.departments, - }); - - @override - String toString() { - return { - "title": title, - "uuid": uuid, - "bulletinCatalogueId": catalogId, - "dateTime": dateTime, - "departments": departments, - }.toString(); - } -} - -@HiveType(typeId: CacheHiveType.oaAnnounceDetails) -class OaAnnounceDetails { - /// 标题 - @HiveField(0) - final String title; - - /// 发布时间 - @HiveField(1) - final DateTime dateTime; - - /// 发布部门 - @HiveField(2) - final String department; - - /// 发布者 - @HiveField(3) - final String author; - - /// 阅读人数 - @HiveField(4) - final int readNumber; - - /// 内容(html格式) - @HiveField(5) - final String content; - - /// 附件 - @HiveField(6) - final List attachments; - - const OaAnnounceDetails({ - required this.title, - required this.dateTime, - required this.department, - required this.author, - required this.readNumber, - required this.content, - required this.attachments, - }); - - @override - String toString() { - return { - "title": title, - "dateTime": dateTime, - "department": department, - "author": author, - "readNumber": readNumber, - "content": content, - "attachments": attachments, - }.toString(); - } -} - -@HiveType(typeId: CacheHiveType.oaAnnounceAttachment) -class OaAnnounceAttachment { - /// 附件标题 - @HiveField(0) - final String name; - - /// 附件下载网址 - @HiveField(1) - final String url; - - const OaAnnounceAttachment({ - required this.name, - required this.url, - }); - - @override - String toString() { - return { - "name": name, - "url": url, - }.toString(); - } -} diff --git a/lib/school/oa_announce/entity/announce.g.dart b/lib/school/oa_announce/entity/announce.g.dart deleted file mode 100644 index 580fe2bce..000000000 --- a/lib/school/oa_announce/entity/announce.g.dart +++ /dev/null @@ -1,136 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'announce.dart'; - -// ************************************************************************** -// TypeAdapterGenerator -// ************************************************************************** - -class OaAnnounceRecordAdapter extends TypeAdapter { - @override - final int typeId = 92; - - @override - OaAnnounceRecord read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return OaAnnounceRecord( - title: fields[0] as String, - uuid: fields[1] as String, - catalogId: fields[2] as String, - dateTime: fields[3] as DateTime, - departments: (fields[4] as List).cast(), - ); - } - - @override - void write(BinaryWriter writer, OaAnnounceRecord obj) { - writer - ..writeByte(5) - ..writeByte(0) - ..write(obj.title) - ..writeByte(1) - ..write(obj.uuid) - ..writeByte(2) - ..write(obj.catalogId) - ..writeByte(3) - ..write(obj.dateTime) - ..writeByte(4) - ..write(obj.departments); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is OaAnnounceRecordAdapter && runtimeType == other.runtimeType && typeId == other.typeId; -} - -class OaAnnounceDetailsAdapter extends TypeAdapter { - @override - final int typeId = 90; - - @override - OaAnnounceDetails read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return OaAnnounceDetails( - title: fields[0] as String, - dateTime: fields[1] as DateTime, - department: fields[2] as String, - author: fields[3] as String, - readNumber: fields[4] as int, - content: fields[5] as String, - attachments: (fields[6] as List).cast(), - ); - } - - @override - void write(BinaryWriter writer, OaAnnounceDetails obj) { - writer - ..writeByte(7) - ..writeByte(0) - ..write(obj.title) - ..writeByte(1) - ..write(obj.dateTime) - ..writeByte(2) - ..write(obj.department) - ..writeByte(3) - ..write(obj.author) - ..writeByte(4) - ..write(obj.readNumber) - ..writeByte(5) - ..write(obj.content) - ..writeByte(6) - ..write(obj.attachments); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is OaAnnounceDetailsAdapter && runtimeType == other.runtimeType && typeId == other.typeId; -} - -class OaAnnounceAttachmentAdapter extends TypeAdapter { - @override - final int typeId = 91; - - @override - OaAnnounceAttachment read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return OaAnnounceAttachment( - name: fields[0] as String, - url: fields[1] as String, - ); - } - - @override - void write(BinaryWriter writer, OaAnnounceAttachment obj) { - writer - ..writeByte(2) - ..writeByte(0) - ..write(obj.name) - ..writeByte(1) - ..write(obj.url); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is OaAnnounceAttachmentAdapter && runtimeType == other.runtimeType && typeId == other.typeId; -} diff --git a/lib/school/oa_announce/entity/page.dart b/lib/school/oa_announce/entity/page.dart deleted file mode 100644 index 33a7f66b1..000000000 --- a/lib/school/oa_announce/entity/page.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'announce.dart'; - -/// 获取到的通知页 -class OaAnnounceListPayload { - final int currentPage; - final int totalPage; - final List items; - - const OaAnnounceListPayload({ - required this.currentPage, - required this.totalPage, - required this.items, - }); - - @override - String toString() { - return { - "currentPage": currentPage, - "totalPage": totalPage, - "items": items, - }.toString(); - } -} diff --git a/lib/school/oa_announce/i18n.dart b/lib/school/oa_announce/i18n.dart deleted file mode 100644 index 1b44f439c..000000000 --- a/lib/school/oa_announce/i18n.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:sit/l10n/common.dart'; - -const i18n = _I18n(); - -class _I18n with CommonI18nMixin { - const _I18n(); - - static const ns = "oaAnnounce"; - final info = const _Info(); - - String get title => "$ns.title".tr(); - - String get noOaAnnouncementsTip => "$ns.noOaAnnouncementsTip".tr(); - - String get downloadCompleted => "$ns.downloadCompleted".tr(); - - String get downloadFailed => "$ns.downloadFailed".tr(); - - String get downloading => "$ns.downloading".tr(); - - String get infoTab => "$ns.tab.info".tr(); - - String get contentTab => "$ns.tab.content".tr(); -} - -class _Info { - const _Info(); - - static const ns = "${_I18n.ns}.info"; - - String attachmentHeader(int count) => "$ns.attachmentHeader".plural(count); - - String get title => "$ns.title".tr(); - - String get publishTime => "$ns.publishTime".tr(); - - String get department => "$ns.department".tr(); - - String get author => "$ns.author".tr(); - - String get tags => "$ns.tags".tr(); -} diff --git a/lib/school/oa_announce/index.dart b/lib/school/oa_announce/index.dart deleted file mode 100644 index 2e79e5f4c..000000000 --- a/lib/school/oa_announce/index.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:sit/design/widgets/app.dart'; -import 'package:rettulf/rettulf.dart'; - -import "i18n.dart"; - -class OaAnnounceAppCard extends StatefulWidget { - const OaAnnounceAppCard({super.key}); - - @override - State createState() => _OaAnnounceAppCardState(); -} - -class _OaAnnounceAppCardState extends State { - @override - Widget build(BuildContext context) { - return AppCard( - title: i18n.title.text(), - leftActions: [ - FilledButton.icon( - onPressed: () { - context.push("/oa-announce"); - }, - icon: const Icon(Icons.newspaper), - label: i18n.seeAll.text(), - ), - ], - ); - } -} diff --git a/lib/school/oa_announce/init.dart b/lib/school/oa_announce/init.dart deleted file mode 100644 index 6f837d2e7..000000000 --- a/lib/school/oa_announce/init.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'storage/announce.dart'; - -import 'service/announce.dart'; - -class OaAnnounceInit { - static late OaAnnounceService service; - static late OaAnnounceStorage storage; - - static void init() { - service = const OaAnnounceService(); - storage = const OaAnnounceStorage(); - } -} diff --git a/lib/school/oa_announce/page/details.dart b/lib/school/oa_announce/page/details.dart deleted file mode 100644 index 1d7653869..000000000 --- a/lib/school/oa_announce/page/details.dart +++ /dev/null @@ -1,235 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:sit/design/widgets/list_tile.dart'; -import 'package:sit/design/widgets/tags.dart'; -import 'package:sit/l10n/extension.dart'; -import 'package:sit/school/class2nd/utils.dart'; -import 'package:sit/school/oa_announce/widget/article.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:sit/utils/error.dart'; -import 'package:url_launcher/url_launcher_string.dart'; - -import '../entity/announce.dart'; -import '../init.dart'; -import '../i18n.dart'; -import '../service/announce.dart'; -import '../widget/attachment.dart'; - -class AnnounceDetailsPage extends StatefulWidget { - final OaAnnounceRecord record; - - const AnnounceDetailsPage( - this.record, { - super.key, - }); - - @override - State createState() => _AnnounceDetailsPageState(); -} - -class _Tab { - static const length = 2; - static const info = 0; - static const content = 1; -} - -class _AnnounceDetailsPageState extends State { - late OaAnnounceDetails? details = OaAnnounceInit.storage.getAnnounceDetails(widget.record.uuid); - bool isFetching = false; - - @override - void initState() { - super.initState(); - refresh(); - } - - Future refresh() async { - if (details != null) return; - if (!mounted) return; - setState(() { - isFetching = true; - }); - try { - final catalogId = widget.record.catalogId; - final uuid = widget.record.uuid; - final details = await OaAnnounceInit.service.fetchAnnounceDetails(catalogId, uuid); - OaAnnounceInit.storage.setAnnounceDetails(uuid, details); - if (!mounted) return; - setState(() { - this.details = details; - isFetching = false; - }); - } catch (error, stackTrace) { - debugPrintError(error, stackTrace); - if (!mounted) return; - setState(() { - isFetching = false; - }); - } - } - - @override - Widget build(BuildContext context) { - final details = this.details; - final record = widget.record; - return Scaffold( - body: DefaultTabController( - length: _Tab.length, - child: NestedScrollView( - floatHeaderSlivers: true, - headerSliverBuilder: (context, innerBoxIsScrolled) { - return [ - SliverOverlapAbsorber( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), - sliver: SliverAppBar( - floating: true, - title: i18n.title.text(), - actions: [ - IconButton( - onPressed: () { - launchUrlString( - OaAnnounceService.getAnnounceUrl(widget.record.catalogId, widget.record.uuid), - mode: LaunchMode.externalApplication, - ); - }, - icon: const Icon(Icons.open_in_browser), - ), - ], - forceElevated: innerBoxIsScrolled, - bottom: TabBar( - isScrollable: true, - tabs: [ - Tab(child: i18n.infoTab.text()), - Tab(child: i18n.contentTab.text()), - ], - ), - ), - ), - ]; - }, - body: TabBarView( - children: [ - OaAnnounceDetailsInfoTabView(record: record, details: details), - OaAnnounceDetailsContentTabView(details: details), - ], - ), - ), - ), - bottomNavigationBar: isFetching - ? const PreferredSize( - preferredSize: Size.fromHeight(4), - child: LinearProgressIndicator(), - ) - : null, - ); - } -} - -class OaAnnounceDetailsInfoTabView extends StatefulWidget { - final OaAnnounceRecord record; - final OaAnnounceDetails? details; - - const OaAnnounceDetailsInfoTabView({ - super.key, - required this.record, - this.details, - }); - - @override - State createState() => _OaAnnounceDetailsInfoTabViewState(); -} - -class _OaAnnounceDetailsInfoTabViewState extends State - with AutomaticKeepAliveClientMixin { - @override - bool get wantKeepAlive => true; - - @override - Widget build(BuildContext context) { - super.build(context); - final details = widget.details; - final record = widget.record; - final (:title, :tags) = separateTagsFromTitle(record.title); - return SelectionArea( - child: CustomScrollView( - slivers: [ - SliverList.list(children: [ - DetailListTile( - title: i18n.info.title, - subtitle: title, - ), - if (details != null) - DetailListTile( - title: i18n.info.author, - subtitle: details.author, - ), - DetailListTile( - title: i18n.info.publishTime, - subtitle: context.formatYmdText(record.dateTime), - ), - DetailListTile( - title: i18n.info.department, - subtitle: record.departments.join(", "), - ), - if (tags.isNotEmpty) - ListTile( - isThreeLine: true, - title: i18n.info.tags.text(), - subtitle: TagsGroup(tags), - ) - ]), - if (details != null && details.attachments.isNotEmpty) - SliverList.list(children: [ - const Divider(), - ListTile( - leading: const Icon(Icons.attach_file), - title: i18n.info.attachmentHeader(details.attachments.length).text(), - ) - ]), - if (details != null) - SliverList.builder( - itemCount: details.attachments.length, - itemBuilder: (ctx, i) => AttachmentLinkTile( - details.attachments[i], - uuid: record.uuid, - ), - ), - ], - ), - ); - } -} - -class OaAnnounceDetailsContentTabView extends StatefulWidget { - final OaAnnounceDetails? details; - - const OaAnnounceDetailsContentTabView({ - super.key, - this.details, - }); - - @override - State createState() => _OaAnnounceDetailsContentTabViewState(); -} - -class _OaAnnounceDetailsContentTabViewState extends State - with AutomaticKeepAliveClientMixin { - @override - bool get wantKeepAlive => true; - - @override - Widget build(BuildContext context) { - super.build(context); - final details = widget.details; - return SelectionArea( - child: CustomScrollView( - slivers: [ - if (details != null) - SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 8), - sliver: AnnounceArticle(details), - ) - ], - ), - ); - } -} diff --git a/lib/school/oa_announce/page/list.dart b/lib/school/oa_announce/page/list.dart deleted file mode 100644 index 851c0d34f..000000000 --- a/lib/school/oa_announce/page/list.dart +++ /dev/null @@ -1,209 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:sit/credentials/widgets/oa_scope.dart'; -import 'package:sit/design/widgets/card.dart'; -import 'package:sit/design/widgets/common.dart'; - -import 'package:sit/school/oa_announce/widget/tile.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:sit/utils/collection.dart'; -import 'package:sit/utils/error.dart'; - -import '../entity/announce.dart'; -import '../init.dart'; -import '../i18n.dart'; - -class OaAnnounceListPage extends StatefulWidget { - const OaAnnounceListPage({super.key}); - - @override - State createState() => _OaAnnounceListPageState(); -} - -class _OaAnnounceListPageState extends State { - @override - Widget build(BuildContext context) { - final cats = OaAnnounceCat.resolve(context.auth.userType); - return OaAnnounceListPageInternal(cats: cats); - } -} - -class OaAnnounceListPageInternal extends StatefulWidget { - final List cats; - - const OaAnnounceListPageInternal({ - super.key, - required this.cats, - }); - - @override - State createState() => _OaAnnounceListPageInternalState(); -} - -class _OaAnnounceListPageInternalState extends State { - late final $loadingStates = ValueNotifier(widget.cats.map((cat) => false).toList()); - - @override - void dispose() { - $loadingStates.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - bottomNavigationBar: PreferredSize( - preferredSize: const Size.fromHeight(4), - child: $loadingStates >> - (ctx, states) { - return !states.any((state) => state == true) ? const SizedBox() : const LinearProgressIndicator(); - }, - ), - body: DefaultTabController( - length: widget.cats.length, - child: NestedScrollView( - floatHeaderSlivers: true, - headerSliverBuilder: (context, innerBoxIsScrolled) { - // These are the slivers that show up in the "outer" scroll view. - return [ - SliverOverlapAbsorber( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), - sliver: SliverAppBar( - floating: true, - title: i18n.title.text(), - forceElevated: innerBoxIsScrolled, - bottom: TabBar( - isScrollable: true, - tabs: widget.cats - .map((cat) => Tab( - child: cat.l10nName().text(), - )) - .toList(), - ), - ), - ), - ]; - }, - body: TabBarView( - // These are the contents of the tab views, below the tabs. - children: widget.cats.mapIndexed((i, cat) { - return OaAnnounceLoadingList( - key: ValueKey(cat), - cat: cat, - onLoadingChanged: (state) { - final newStates = List.of($loadingStates.value); - newStates[i] = state; - $loadingStates.value = newStates; - }, - ); - }).toList(), - ), - ), - ), - ); - } -} - -class OaAnnounceLoadingList extends StatefulWidget { - final OaAnnounceCat cat; - final ValueChanged onLoadingChanged; - - const OaAnnounceLoadingList({ - super.key, - required this.cat, - required this.onLoadingChanged, - }); - - @override - State createState() => _OaAnnounceLoadingListState(); -} - -class _OaAnnounceLoadingListState extends State with AutomaticKeepAliveClientMixin { - int lastPage = 1; - bool isFetching = false; - late var announcements = OaAnnounceInit.storage.getAnnouncements(widget.cat); - - @override - bool get wantKeepAlive => true; - - @override - void initState() { - super.initState(); - Future.delayed(Duration.zero).then((value) async { - await loadMore(); - }); - } - - @override - Widget build(BuildContext context) { - super.build(context); - final announcements = this.announcements; - return NotificationListener( - onNotification: (event) { - if (event.metrics.pixels >= event.metrics.maxScrollExtent) { - loadMore(); - } - return true; - }, - child: CustomScrollView( - // CAN'T USE ScrollController, and I don't know why - // controller: scrollController, - slivers: [ - SliverOverlapInjector( - // This is the flip side of the SliverOverlapAbsorber above. - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), - ), - if (announcements != null) - if (announcements.isEmpty) - SliverFillRemaining( - child: LeavingBlank( - icon: Icons.inbox_outlined, - desc: i18n.noOaAnnouncementsTip, - ), - ) - else - SliverList.builder( - itemCount: announcements.length, - itemBuilder: (ctx, index) { - return FilledCard( - clip: Clip.hardEdge, - child: OaAnnounceTile(announcements[index]), - ); - }, - ), - ], - ), - ); - } - - Future loadMore() async { - if (isFetching) return; - if (!mounted) return; - setState(() { - isFetching = true; - }); - widget.onLoadingChanged(true); - final cat = widget.cat; - try { - final lastPayload = await OaAnnounceInit.service.getAnnounceList(cat, lastPage); - final announcements = this.announcements ?? []; - announcements.addAll(lastPayload.items); - announcements.distinctBy((a) => a.uuid); - announcements.sort((a, b) => b.dateTime.compareTo(a.dateTime)); - await OaAnnounceInit.storage.setAnnouncements(cat, announcements); - if (!mounted) return; - setState(() { - lastPage++; - isFetching = false; - }); - widget.onLoadingChanged(false); - } catch (error, stackTrace) { - debugPrintError(error, stackTrace); - if (!mounted) return; - setState(() { - isFetching = false; - }); - widget.onLoadingChanged(false); - } - } -} diff --git a/lib/school/oa_announce/service/announce.dart b/lib/school/oa_announce/service/announce.dart deleted file mode 100644 index af2014c9f..000000000 --- a/lib/school/oa_announce/service/announce.dart +++ /dev/null @@ -1,114 +0,0 @@ -import 'package:beautiful_soup_dart/beautiful_soup.dart'; -import 'package:dio/dio.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:intl/intl.dart'; -import 'package:sit/init.dart'; -import 'package:sit/school/utils.dart'; -import 'package:sit/session/sso.dart'; - -import '../entity/announce.dart'; -import '../entity/page.dart'; - -final _announceDateTimeFormat = DateFormat('yyyy-MM-dd'); -final _departmentSplitRegex = RegExp(r'\s+'); -final _dateFormat = DateFormat('yyyy年MM月dd日 hh:mm'); - -class OaAnnounceService { - SsoSession get session => Init.ssoSession; - - const OaAnnounceService(); - - List _parseAttachment(Bs4Element element) { - return element.find('#containerFrame > table')!.findAll('a').map((e) { - return OaAnnounceAttachment( - name: e.text.trim(), - url: 'https://myportal.sit.edu.cn/${e.attributes['href']!}', - ); - }).toList(); - } - - OaAnnounceDetails _parseAnnounceDetails(Bs4Element item) { - String metaHtml = item.find('div', class_: 'bulletin-info')?.innerHtml ?? ''; - // 删除注释 - metaHtml = metaHtml.replaceAll('', ''); - String meta = BeautifulSoup(metaHtml).text; - - final metaList = meta.split('|').map((e) => e.trim()).toList(); - final title = item.find('div', class_: 'bulletin-title')?.text.trim() ?? ''; - final author = metaList[2].substring(3); - final department = metaList[1].substring(5); - return OaAnnounceDetails( - title: mapChinesePunctuations(title), - content: item.find('div', class_: 'bulletin-content')?.innerHtml ?? '', - attachments: _parseAttachment(item), - dateTime: _dateFormat.parse(metaList[0].substring(5)), - department: mapChinesePunctuations(department), - author: mapChinesePunctuations(author), - readNumber: int.parse(metaList[3].substring(5)), - ); - } - - static String getAnnounceUrl(String catalogueId, String uuid) { - return 'https://myportal.sit.edu.cn/detach.portal?action=bulletinBrowser&.ia=false&.pmn=view&.pen=$catalogueId&bulletinId=$uuid'; - } - - Future fetchAnnounceDetails(String catalogId, String uuid) async { - final response = await session.request( - getAnnounceUrl(catalogId, uuid), - options: Options( - method: "GET", - ), - ); - final soup = BeautifulSoup(response.data); - return _parseAnnounceDetails(soup.html!); - } - - static OaAnnounceListPayload _parseAnnounceListPage(Bs4Element element) { - final list = element.findAll('li').map((e) { - final departmentAndDate = e.find('span', class_: 'rss-time')!.text.trim(); - final departmentAndDateLen = departmentAndDate.length; - final department = departmentAndDate.substring(0, departmentAndDateLen - 8); - final date = '20${departmentAndDate.substring(departmentAndDateLen - 8, departmentAndDateLen)}'; - - final titleElement = e.find('a', class_: 'rss-title')!; - final uri = Uri.parse(titleElement.attributes['href']!); - - return OaAnnounceRecord( - title: mapChinesePunctuations(titleElement.text.trim()), - departments: department.trim().split(_departmentSplitRegex).map(mapChinesePunctuations).toList(), - dateTime: _announceDateTimeFormat.parse(date), - catalogId: uri.queryParameters['.pen']!, - uuid: uri.queryParameters['bulletinId']!, - ); - }).toList(); - - ({int currentPage, int totalPage})? parsePage() { - final currentRaw = element.find('div', attrs: {'title': '当前页'})?.text; - if (currentRaw == null) return null; - final lastElement = element.find('a', attrs: {'title': '点击跳转到最后页'}); - if (lastElement == null) return null; - final lastElementHref = Uri.parse(lastElement.attributes['href']!); - final lastPageIndex = lastElementHref.queryParameters['pageIndex']!; - return (currentPage: int.parse(currentRaw), totalPage: int.parse(lastPageIndex)); - } - - final page = parsePage(); - - return OaAnnounceListPayload( - items: list, - currentPage: page?.currentPage ?? 1, - totalPage: page?.totalPage ?? 1, - ); - } - - Future getAnnounceList(OaAnnounceCat cat, int pageIndex) async { - final response = await session.request( - 'https://myportal.sit.edu.cn/detach.portal?pageIndex=$pageIndex&groupid=&action=bulletinsMoreView&.ia=false&pageSize=&.pmn=view&.pen=${cat.internalId}', - options: Options( - method: "GET", - ), - ); - final html = BeautifulSoup(response.data); - return _parseAnnounceListPage(html.html!); - } -} diff --git a/lib/school/oa_announce/storage/announce.dart b/lib/school/oa_announce/storage/announce.dart deleted file mode 100644 index 3dfca6fc6..000000000 --- a/lib/school/oa_announce/storage/announce.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:hive/hive.dart'; -import 'package:sit/storage/hive/init.dart'; - -import '../entity/announce.dart'; - -class _K { - static String announce(String uuid) => '/announce/$uuid'; - - static String announceDetails(String uuid) => '/announceDetails/$uuid'; - - static String announceIdList(OaAnnounceCat type) => '/announceIdList/$type'; -} - -class OaAnnounceStorage { - Box get box => HiveInit.oaAnnounce; - - const OaAnnounceStorage(); - List? getAnnounceIdList(OaAnnounceCat type) => box.get(_K.announceIdList(type)); - - Future setAnnounceIdList(OaAnnounceCat type, List? announceIdList) => - box.put(_K.announceIdList(type), announceIdList); - - OaAnnounceRecord? getAnnounce(String uuid) => box.get(_K.announce(uuid)); - - Future setAnnounce(String uuid, OaAnnounceRecord? announce) => box.put(_K.announce(uuid), announce); - - OaAnnounceDetails? getAnnounceDetails(String uuid) => box.get(_K.announceDetails(uuid)); - - Future setAnnounceDetails(String uuid, OaAnnounceDetails? details) => - box.put(_K.announceDetails(uuid), details); - - List? getAnnouncements(OaAnnounceCat type) { - final idList = getAnnounceIdList(type); - if (idList == null) return null; - final res = []; - for (final id in idList) { - final announce = getAnnounce(id); - if (announce != null) { - res.add(announce); - } - } - return res; - } - - Future? setAnnouncements(OaAnnounceCat type, List? announcements) async { - if (announcements == null) { - await setAnnouncements(type, null); - } else { - await setAnnounceIdList(type, announcements.map((e) => e.uuid).toList(growable: false)); - for (final announce in announcements) { - await setAnnounce(announce.uuid, announce); - } - } - } -} diff --git a/lib/school/oa_announce/widget/article.dart b/lib/school/oa_announce/widget/article.dart deleted file mode 100644 index e360bf0bf..000000000 --- a/lib/school/oa_announce/widget/article.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; -import 'package:sit/widgets/html.dart'; - -import '../entity/announce.dart'; - -class AnnounceArticle extends StatelessWidget { - final OaAnnounceDetails details; - - const AnnounceArticle(this.details, {super.key}); - - @override - Widget build(BuildContext context) { - final htmlContent = _linkTel(details.content); - return RestyledHtmlWidget( - htmlContent, - renderMode: RenderMode.sliverList, - ); - } -} - -final RegExp _phoneRegex = RegExp(r"(6087\d{4})"); -final RegExp _mobileRegex = RegExp(r"(\d{12})"); - -String _linkTel(String content) { - String t = content; - for (var phone in _phoneRegex.allMatches(t)) { - final num = phone.group(0).toString(); - t = t.replaceAll(num, '$num'); - } - for (var mobile in _mobileRegex.allMatches(content)) { - final num = mobile.group(0).toString(); - t = t.replaceAll(num, '$num'); - } - return t; -} diff --git a/lib/school/oa_announce/widget/attachment.dart b/lib/school/oa_announce/widget/attachment.dart deleted file mode 100644 index 8d8cdbca3..000000000 --- a/lib/school/oa_announce/widget/attachment.dart +++ /dev/null @@ -1,130 +0,0 @@ -import 'dart:io'; - -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:sanitize_filename/sanitize_filename.dart'; -import 'package:sit/design/adaptive/dialog.dart'; -import 'package:open_file/open_file.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:sit/files.dart'; -import 'package:sit/utils/error.dart'; - -import '../entity/announce.dart'; -import '../i18n.dart'; -import '../init.dart'; - -class AttachmentLinkTile extends StatefulWidget { - final String uuid; - final OaAnnounceAttachment attachment; - - const AttachmentLinkTile( - this.attachment, { - super.key, - required this.uuid, - }); - - @override - State createState() => _AttachmentLinkTileState(); -} - -class _AttachmentLinkTileState extends State { - double? progress; - - @override - Widget build(BuildContext context) { - final progress = this.progress; - return ListTile( - title: RichText( - text: TextSpan( - children: [ - TextSpan( - text: widget.attachment.name, - style: const TextStyle(color: Colors.blue), - recognizer: TapGestureRecognizer()..onTap = onDownload, - ), - ], - ), - ), - subtitle: progress == null - ? null - : LinearProgressIndicator( - value: progress.isNaN ? null : progress, - ), - ); - } - - Future onDownload() async { - final dir = await Files.oaAnnounce.attachmentDir(widget.uuid).create(recursive: true); - final target = dir.subFile(sanitizeFilename(widget.attachment.name)); - if (await target.exists()) { - await OpenFile.open(target.path); - } else { - if (!mounted) return; - context.showSnackBar( - content: i18n.downloading.text(), - duration: const Duration(seconds: 1), - ); - try { - await _onDownloadFile( - name: widget.attachment.name, - url: widget.attachment.url, - target: target, - onProgress: (progress) { - if (!mounted) return; - setState(() { - this.progress = progress; - }); - }, - ); - if (!mounted) return; - context.showSnackBar( - content: widget.attachment.name.text(), - duration: const Duration(seconds: 5), - action: SnackBarAction( - label: i18n.open, - onPressed: () async { - await OpenFile.open(target.path); - }, - ), - ); - setState(() { - progress = 1; - }); - } catch (error, stackTrace) { - debugPrintError(error, stackTrace); - if (!mounted) return; - setState(() { - progress = null; - }); - context.showSnackBar( - content: i18n.downloadFailed.text(), - duration: const Duration(seconds: 5), - action: SnackBarAction( - label: i18n.retry, - onPressed: () async { - await onDownload(); - }, - ), - ); - } - } - } -} - -Future _onDownloadFile({ - required String name, - required String url, - required File target, - void Function(double progress)? onProgress, -}) async { - debugPrint('Start downloading [$name]($url) to $target'); - // 如果文件不存在,那么下载文件 - await OaAnnounceInit.service.session.dio.download( - url, - target.path, - onReceiveProgress: (int count, int total) { - onProgress?.call(total <= 0 ? double.nan : count / total); - }, - ); - debugPrint('Downloaded [$name]($url)'); -} diff --git a/lib/school/oa_announce/widget/tile.dart b/lib/school/oa_announce/widget/tile.dart deleted file mode 100644 index 97e351897..000000000 --- a/lib/school/oa_announce/widget/tile.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:sit/design/widgets/tags.dart'; -import 'package:sit/l10n/extension.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:sit/school/class2nd/utils.dart'; - -import '../entity/announce.dart'; - -class OaAnnounceTile extends StatelessWidget { - final OaAnnounceRecord record; - - const OaAnnounceTile( - this.record, { - super.key, - }); - - @override - Widget build(BuildContext context) { - final textTheme = context.textTheme; - final (:title, :tags) = separateTagsFromTitle(record.title); - - return ListTile( - isThreeLine: true, - titleTextStyle: textTheme.titleMedium, - title: title.text(), - subtitleTextStyle: textTheme.bodySmall, - subtitle: TagsGroup(record.departments + tags), - trailing: context.formatYmdNum(record.dateTime).text(style: textTheme.bodySmall), - onTap: () { - context.push("/oa-announce/details", extra: record); - }, - ); - } -} diff --git a/lib/school/settings.dart b/lib/school/settings.dart deleted file mode 100644 index 0aa92fcbd..000000000 --- a/lib/school/settings.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:hive_flutter/hive_flutter.dart'; - -const _kClass2ndAutoRefresh = true; - -class SchoolSettings { - final Box box; - - SchoolSettings(this.box); - - late final class2nd = _Class2nd(box); - late final examResult = _ExamResult(box); - late final examArrange = _ExamArrange(box); - - static const ns = "/school"; -} - -class _Class2ndK { - static const ns = "${SchoolSettings.ns}/class2nd"; - static const autoRefresh = "$ns/autoRefresh"; -} - -class _Class2nd { - final Box box; - - const _Class2nd(this.box); - - bool get autoRefresh => box.get(_Class2ndK.autoRefresh) ?? _kClass2ndAutoRefresh; - - set autoRefresh(bool newV) => box.put(_Class2ndK.autoRefresh, newV); -} - -class _ExamResultK { - static const ns = "${SchoolSettings.ns}/examResult"; -} - -class _ExamResult { - final Box box; - - const _ExamResult(this.box); -} - -class _ExamArrangeK { - static const ns = "${SchoolSettings.ns}/examArrange"; -} - -class _ExamArrange { - final Box box; - - const _ExamArrange(this.box); -} diff --git a/lib/school/utils.dart b/lib/school/utils.dart deleted file mode 100644 index dd57b5817..000000000 --- a/lib/school/utils.dart +++ /dev/null @@ -1,80 +0,0 @@ -import 'package:sit/l10n/time.dart'; -import 'package:sit/school/entity/school.dart'; - -/// 将 "第几周、周几" 转换为日期. 如, 开学日期为 2021-9-1, 那么将第一周周一转换为 2021-9-1 -DateTime reflectWeekDayIndexToDate({ - required DateTime startDate, - required int weekIndex, - required Weekday weekday, -}) { - return startDate.add(Duration(days: weekIndex * 7 + weekday.index)); -} - -final _parenthesesRegx = RegExp(r"\((.*?)\)"); - -/// Exchange a string in brackets with a string out of brackets, -/// if the string in brackets has a substring such as "一教", "二教", and "三教". -String reformatPlace(String place) { - final matched = _parenthesesRegx.firstMatch(place); - if (matched == null) return place; - final inParentheses = matched.group(1); - if (inParentheses == null) return place; - if (!inParentheses.contains("一教") && !inParentheses.contains("二教") && !inParentheses.contains("三教")) return place; - final outParentheses = place.replaceRange(matched.start, matched.end, ""); - return "$inParentheses($outParentheses)"; -} - -/// 删去 place 括号里的描述信息. 如, 二教F301(机电18中外合作专用) -/// But it will keep the "三教" in brackets. -String beautifyPlace(String place) { - int indexOfBucket = place.indexOf('('); - return indexOfBucket != -1 ? place.substring(0, indexOfBucket) : place; -} - -/// Replace the full-width brackets to ASCII ones -String mapChinesePunctuations(String name) { - final b = StringBuffer(); - for (final c in name.runes) { - switch (c) { - case 0xFF08: // ( - b.writeCharCode(0x28); // ( - break; - - case 0xFF09: // ) - b.writeCharCode(0x29); // ) - break; - - case 0x3010: // 【 - b.writeCharCode(0x5B); // [ - break; - - case 0x3011: // 】 - b.writeCharCode(0x5D); // ] - break; - - case 0xFF06: // & - b.writeCharCode(0x26); // & - break; - default: - b.writeCharCode(c); - } - } - return b.toString(); -} - -int? getAdmissionYearFromStudentId(String? studentId) { - if (studentId == null) return null; - final fromID = int.tryParse(studentId.substring(0, 2)); - if (fromID != null) { - return 2000 + fromID; - } - return null; -} - -SemesterInfo estimateCurrentSemester() { - final now = DateTime.now(); - return SemesterInfo( - year: now.month >= 9 ? now.year : now.year - 1, - semester: now.month >= 3 && now.month <= 7 ? Semester.term2 : Semester.term1, - ); -} diff --git a/lib/school/widgets/campus.dart b/lib/school/widgets/campus.dart deleted file mode 100644 index e934ff418..000000000 --- a/lib/school/widgets/campus.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:sit/entity/campus.dart'; -import 'package:sit/settings/settings.dart'; -import 'package:rettulf/rettulf.dart'; - -class CampusSelector extends StatelessWidget { - const CampusSelector({super.key}); - - @override - Widget build(BuildContext context) { - return StatefulBuilder( - builder: (ctx, setState) => SegmentedButton( - segments: Campus.values - .map((e) => ButtonSegment( - icon: const Icon(Icons.place_outlined), - value: e, - label: e.l10nName().text(), - )) - .toList(), - selected: {Settings.campus}, - onSelectionChanged: (newSelection) async { - setState(() { - Settings.campus = newSelection.first; - }); - await HapticFeedback.mediumImpact(); - }, - ), - ); - } -} diff --git a/lib/school/widgets/course.dart b/lib/school/widgets/course.dart deleted file mode 100644 index 6e1610abe..000000000 --- a/lib/school/widgets/course.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:sit/school/entity/icon.dart'; - -class CourseIcon extends StatelessWidget { - final String courseName; - final double? size; - static const kDefaultSize = 45.0; - - const CourseIcon({ - super.key, - required this.courseName, - this.size = kDefaultSize, - }); - - @override - Widget build(BuildContext context) { - return Image.asset( - CourseCategory.iconPathOf(courseName: courseName), - width: size, - height: size, - ).sized(w: kDefaultSize, h: kDefaultSize); - } -} diff --git a/lib/school/widgets/semester.dart b/lib/school/widgets/semester.dart deleted file mode 100644 index 6ba035bf6..000000000 --- a/lib/school/widgets/semester.dart +++ /dev/null @@ -1,111 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:rettulf/rettulf.dart'; - -import '../entity/school.dart'; -import "../i18n.dart"; - -class SemesterSelector extends StatefulWidget { - final int? baseYear; - final SemesterInfo? initial; - - /// 是否显示整个学年 - final bool showEntireYear; - final void Function(SemesterInfo newSelection)? onSelected; - - const SemesterSelector({ - super.key, - required this.baseYear, - this.onSelected, - this.initial, - this.showEntireYear = false, - }); - - @override - State createState() => _SemesterSelectorState(); -} - -class _SemesterSelectorState extends State { - late final DateTime now; - - /// 四位年份 - late int selectedYear; - - /// 要查询的学期 - late Semester selectedSemester; - - @override - void initState() { - super.initState(); - now = DateTime.now(); - selectedYear = widget.initial?.year ?? (now.month >= 9 ? now.year : now.year - 1); - if (widget.showEntireYear) { - selectedSemester = widget.initial?.semester ?? Semester.all; - } else { - selectedSemester = - widget.initial?.semester ?? (now.month >= 3 && now.month <= 7 ? Semester.term2 : Semester.term1); - } - } - - @override - Widget build(BuildContext context) { - return [ - buildYearSelector().padH(4), - buildSemesterSelector().padH(4), - ].row(caa: CrossAxisAlignment.start, mas: MainAxisSize.min).padSymmetric(v: 5).center(); - } - - List _generateYearList() { - final endYear = now.month >= 9 ? now.year : now.year - 1; - List yearItems = []; - for (var year = widget.baseYear ?? now.year; year <= endYear; year++) { - yearItems.add(year); - } - return yearItems; - } - - Widget buildYearSelector() { - // 生成经历过的学期并逆序(方便用户选择) - final List yearList = _generateYearList().reversed.toList(); - - // 保证显示上初始选择年份、实际加载的年份、selectedYear 变量一致. - return DropdownMenu( - label: i18n.schoolYear.text(), - initialSelection: selectedYear, - onSelected: (int? newSelection) { - if (newSelection != null && newSelection != selectedYear) { - setState(() => selectedYear = newSelection); - widget.onSelected?.call(SemesterInfo(year: newSelection, semester: selectedSemester)); - } - }, - dropdownMenuEntries: yearList - .map((year) => DropdownMenuEntry( - value: year, - label: "$year–${year + 1}", - )) - .toList(), - ); - } - - Widget buildSemesterSelector() { - List semesters = widget.showEntireYear - ? const [Semester.all, Semester.term1, Semester.term2] - : const [Semester.term1, Semester.term2]; - // 保证显示上初始选择学期、实际加载的学期、selectedSemester 变量一致. - return DropdownMenu( - label: i18n.semester.text(), - initialSelection: selectedSemester, - onSelected: (Semester? newSelection) { - if (newSelection != null && newSelection != selectedSemester) { - setState(() => selectedSemester = newSelection); - widget.onSelected?.call(SemesterInfo(year: selectedYear, semester: newSelection)); - } - }, - dropdownMenuEntries: semesters - .map((semester) => DropdownMenuEntry( - value: semester, - label: semester.l10n(), - )) - .toList(), - ); - } -} diff --git a/lib/school/yellow_pages/entity/contact.dart b/lib/school/yellow_pages/entity/contact.dart deleted file mode 100644 index 746c9c845..000000000 --- a/lib/school/yellow_pages/entity/contact.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; -import 'package:sit/storage/hive/type_id.dart'; - -part 'contact.g.dart'; - -@JsonSerializable(createToJson: false) -@HiveType(typeId: CacheHiveType.schoolContact) -class SchoolContact { - @JsonKey() - @HiveField(0) - final String department; - - @JsonKey(includeIfNull: false) - @HiveField(1) - final String? description; - - @JsonKey(includeIfNull: false) - @HiveField(2) - final String? name; - - @JsonKey() - @HiveField(3) - final String phone; - - const SchoolContact(this.department, this.description, this.name, this.phone); - - factory SchoolContact.fromJson(Map json) => _$SchoolContactFromJson(json); - - @override - String toString() { - return '{department: $department, description: $description, name: $name, phone: $phone}'; - } - - @override - bool operator ==(Object other) => - identical(this, other) || - other is SchoolContact && - runtimeType == other.runtimeType && - department == other.department && - name == other.name && - phone == other.phone && - description == other.description; - - @override - int get hashCode => Object.hash(department, name, phone, description); -} diff --git a/lib/school/yellow_pages/entity/contact.g.dart b/lib/school/yellow_pages/entity/contact.g.dart deleted file mode 100644 index a5e220bc1..000000000 --- a/lib/school/yellow_pages/entity/contact.g.dart +++ /dev/null @@ -1,59 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'contact.dart'; - -// ************************************************************************** -// TypeAdapterGenerator -// ************************************************************************** - -class SchoolContactAdapter extends TypeAdapter { - @override - final int typeId = 100; - - @override - SchoolContact read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return SchoolContact( - fields[0] as String, - fields[1] as String?, - fields[2] as String?, - fields[3] as String, - ); - } - - @override - void write(BinaryWriter writer, SchoolContact obj) { - writer - ..writeByte(4) - ..writeByte(0) - ..write(obj.department) - ..writeByte(1) - ..write(obj.description) - ..writeByte(2) - ..write(obj.name) - ..writeByte(3) - ..write(obj.phone); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is SchoolContactAdapter && runtimeType == other.runtimeType && typeId == other.typeId; -} - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -SchoolContact _$SchoolContactFromJson(Map json) => SchoolContact( - json['department'] as String, - json['description'] as String?, - json['name'] as String?, - json['phone'] as String, - ); diff --git a/lib/school/yellow_pages/i18n.dart b/lib/school/yellow_pages/i18n.dart deleted file mode 100644 index 2a26a296d..000000000 --- a/lib/school/yellow_pages/i18n.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:sit/l10n/common.dart'; - -const i18n = _I18n(); - -class _I18n with CommonI18nMixin { - const _I18n(); - - static const ns = "yellowPages"; - - String get title => "$ns.title".tr(); -} diff --git a/lib/school/yellow_pages/index.dart b/lib/school/yellow_pages/index.dart deleted file mode 100644 index ff0e0ddb9..000000000 --- a/lib/school/yellow_pages/index.dart +++ /dev/null @@ -1,90 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:go_router/go_router.dart'; -import 'package:sit/design/widgets/app.dart'; -import 'package:sit/r.dart'; -import 'init.dart'; -import 'storage/contact.dart'; -import 'widgets/contact.dart'; -import 'package:rettulf/rettulf.dart'; - -import 'entity/contact.dart'; -import 'i18n.dart'; -import 'widgets/search.dart'; - -const _historyLength = 2; - -class YellowPagesAppCard extends StatefulWidget { - const YellowPagesAppCard({super.key}); - - @override - State createState() => _YellowPagesAppCardState(); -} - -class _YellowPagesAppCardState extends State { - final $history = YellowPagesInit.storage.listenHistory(); - - @override - void initState() { - $history.addListener(refresh); - super.initState(); - } - - @override - void dispose() { - $history.removeListener(refresh); - super.dispose(); - } - - void refresh() { - setState(() {}); - } - - @override - Widget build(BuildContext context) { - final history = YellowPagesInit.storage.interactHistory ?? const []; - return AppCard( - view: buildHistory(history), - title: i18n.title.text(), - leftActions: [ - FilledButton.icon( - onPressed: () async { - final result = await showSearch(context: context, delegate: YellowPageSearchDelegate(R.yellowPages)); - if (result == null) return; - YellowPagesInit.storage.addInteractHistory(result); - }, - label: i18n.search.text(), - icon: const Icon(Icons.search), - ), - OutlinedButton( - onPressed: () { - context.push("/yellow-pages"); - }, - child: i18n.seeAll.text(), - ) - ], - ); - } - - Widget buildHistory(List history) { - if (history.isEmpty) return const SizedBox(); - final contacts = history.sublist(0, min(_historyLength, history.length)); - return contacts - .map((contact) { - return Dismissible( - direction: DismissDirection.endToStart, - key: ValueKey("${contact.name}+${contact.phone}"), - onDismissed: (dir) async { - await HapticFeedback.heavyImpact(); - history.remove(contact); - YellowPagesInit.storage.interactHistory = history; - }, - child: ContactTile(contact).inCard(), - ); - }) - .toList() - .column(mas: MainAxisSize.min); - } -} diff --git a/lib/school/yellow_pages/init.dart b/lib/school/yellow_pages/init.dart deleted file mode 100644 index fe403b871..000000000 --- a/lib/school/yellow_pages/init.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:sit/school/yellow_pages/storage/contact.dart'; - -class YellowPagesInit { - static late YellowPagesStorage storage; - - static void init() { - storage = const YellowPagesStorage(); - } -} diff --git a/lib/school/yellow_pages/page/index.dart b/lib/school/yellow_pages/page/index.dart deleted file mode 100644 index d68a73d40..000000000 --- a/lib/school/yellow_pages/page/index.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:sit/r.dart'; -import 'package:sit/school/yellow_pages/init.dart'; -import 'package:sit/school/yellow_pages/storage/contact.dart'; -import 'package:rettulf/rettulf.dart'; - -import '../widgets/list.dart'; -import '../widgets/search.dart'; -import '../i18n.dart'; - -const _defaultRevealedDepartmentLength = 3; - -class YellowPagesListPage extends StatefulWidget { - const YellowPagesListPage({super.key}); - - @override - State createState() => _YellowPagesListPageState(); -} - -class _YellowPagesListPageState extends State { - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: i18n.title.text(), - actions: [ - IconButton( - onPressed: () async { - final result = await showSearch(context: context, delegate: YellowPageSearchDelegate(R.yellowPages)); - if (result == null) return; - YellowPagesInit.storage.addInteractHistory(result); - }, - icon: const Icon(Icons.search), - ), - ], - ), - body: SchoolContactList( - R.yellowPages, - isInitialExpanded: (i, length) => i < _defaultRevealedDepartmentLength, - ), - ); - } -} diff --git a/lib/school/yellow_pages/storage/contact.dart b/lib/school/yellow_pages/storage/contact.dart deleted file mode 100644 index bb6fc421a..000000000 --- a/lib/school/yellow_pages/storage/contact.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/foundation.dart'; -import 'package:hive_flutter/hive_flutter.dart'; -import 'package:sit/storage/hive/init.dart'; - -import '../entity/contact.dart'; - -class _K { - static const history = "/interactHistory"; -} - -class YellowPagesStorage { - Box get box => HiveInit.yellowPages; - final int maxHistoryLength; - - const YellowPagesStorage({ - this.maxHistoryLength = 2, - }); - - List? get interactHistory => (box.get(_K.history) as List?)?.cast(); - - set interactHistory(List? newV) { - if (newV != null) { - newV = newV.sublist(0, min(newV.length, maxHistoryLength)); - } - box.put(_K.history, newV); - } - - ValueListenable listenHistory() => box.listenable(keys: [_K.history]); -} - -extension YellowPagesStorageX on YellowPagesStorage { - void addInteractHistory(SchoolContact contact) { - final interactHistory = this.interactHistory ?? []; - if (interactHistory.any((e) => e == contact)) return; - interactHistory.insert(0, contact); - this.interactHistory = interactHistory; - } -} diff --git a/lib/school/yellow_pages/widgets/contact.dart b/lib/school/yellow_pages/widgets/contact.dart deleted file mode 100644 index 3a1ffbd4f..000000000 --- a/lib/school/yellow_pages/widgets/contact.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:sit/design/adaptive/dialog.dart'; -import 'package:sit/school/yellow_pages/init.dart'; -import 'package:sit/school/yellow_pages/storage/contact.dart'; -import 'package:sit/utils/guard_launch.dart'; -import 'package:rettulf/rettulf.dart'; - -import '../entity/contact.dart'; - -class ContactTile extends StatelessWidget { - final SchoolContact contact; - final bool? inHistory; - - const ContactTile( - this.contact, { - super.key, - this.inHistory, - }); - - @override - Widget build(BuildContext context) { - final name = contact.name; - final full = name == null ? contact.phone : "$name, ${contact.phone}"; - final phoneNumber = contact.phone.length == 8 ? "021${contact.phone}" : contact.phone; - return ListTile( - selected: inHistory ?? false, - leading: CircleAvatar( - backgroundColor: context.colorScheme.primary, - radius: 20, - child: name == null || name.isEmpty || _isDigit(name[0]) - ? Center(child: Icon(Icons.account_circle, size: 40, color: context.colorScheme.onPrimary)) - : name[0] - .text( - style: context.textTheme.titleLarge?.copyWith(color: context.colorScheme.onPrimary), - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - ) - .center(), - ), - title: contact.description.toString().text( - overflow: TextOverflow.ellipsis, - ), - subtitle: full.text(overflow: TextOverflow.ellipsis), - trailing: phoneNumber.isEmpty - ? null - : [ - IconButton( - icon: const Icon(Icons.phone), - onPressed: () async { - YellowPagesInit.storage.addInteractHistory(contact); - await guardLaunchUrlString(context, "tel:$phoneNumber"); - }, - ), - IconButton( - icon: const Icon(Icons.content_copy), - onPressed: () async { - YellowPagesInit.storage.addInteractHistory(contact); - await Clipboard.setData(ClipboardData(text: contact.phone)); - if (!context.mounted) return; - context.showSnackBar(content: "Phone number is copied".text()); - }, - ), - ].row(mas: MainAxisSize.min), - ); - } -} - -bool _isDigit(String char) { - return (char.codeUnitAt(0) ^ 0x30) <= 9; -} diff --git a/lib/school/yellow_pages/widgets/list.dart b/lib/school/yellow_pages/widgets/list.dart deleted file mode 100644 index 3a4c472b1..000000000 --- a/lib/school/yellow_pages/widgets/list.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:sit/design/widgets/card.dart'; -import 'package:sit/design/widgets/grouped.dart'; -import 'package:sit/school/yellow_pages/init.dart'; -import 'package:rettulf/rettulf.dart'; - -import '../entity/contact.dart'; -import 'contact.dart'; - -class SchoolContactList extends StatefulWidget { - final List contacts; - final bool Function(int index, int length)? isInitialExpanded; - - const SchoolContactList( - this.contacts, { - super.key, - this.isInitialExpanded, - }); - - @override - State createState() => _SchoolContactListState(); -} - -class _SchoolContactListState extends State { - late Map> department2contacts; - - @override - void initState() { - super.initState(); - updateGroupedContacts(); - } - - @override - void didUpdateWidget(covariant SchoolContactList oldWidget) { - super.didUpdateWidget(oldWidget); - if (!widget.contacts.equals(oldWidget.contacts)) { - updateGroupedContacts(); - } - } - - void updateGroupedContacts() { - department2contacts = widget.contacts.groupListsBy((contact) => contact.department); - } - - @override - Widget build(BuildContext context) { - final history = YellowPagesInit.storage.interactHistory; - return CustomScrollView( - slivers: department2contacts.entries - .mapIndexed( - (i, entry) => GroupedSection( - headerBuilder: (expanded, toggleExpand, defaultTrailing) { - return ListTile( - title: entry.key.text(), - titleTextStyle: context.textTheme.titleMedium, - onTap: toggleExpand, - trailing: defaultTrailing, - ); - }, - initialExpanded: widget.isInitialExpanded?.call(i, department2contacts.length) ?? true, - itemCount: entry.value.length, - itemBuilder: (ctx, i) { - final contact = entry.value[i]; - final inHistory = history?.any((e) => e == contact); - return ContactTile(contact, inHistory: inHistory).inFilledCard(); - }, - ), - ) - .toList(), - ); - } -} diff --git a/lib/school/yellow_pages/widgets/search.dart b/lib/school/yellow_pages/widgets/search.dart deleted file mode 100644 index fcedc4b4a..000000000 --- a/lib/school/yellow_pages/widgets/search.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../entity/contact.dart'; -import 'list.dart'; - -class YellowPageSearchDelegate extends SearchDelegate { - final List contacts; - - YellowPageSearchDelegate(this.contacts) : super(); - - @override - List? buildActions(BuildContext context) { - return [ - IconButton( - icon: const Icon(Icons.clear), - onPressed: () => query = "", - ), - ]; - } - - @override - Widget? buildLeading(BuildContext context) { - return null; - } - - @override - Widget buildResults(BuildContext context) { - if (query.isEmpty) return const SizedBox(); - final matched = contacts.where((e) => predicate(query, e)).toList(); - return SchoolContactList(matched); - } - - @override - void showResults(BuildContext context) { - super.showResults(context); - final matched = contacts.where((e) => predicate(query, e)).toList(); - if (matched.length == 1) { - close(context, matched[0]); - } - } - - @override - Widget buildSuggestions(BuildContext context) { - if (query.isEmpty) return const SizedBox(); - final searched = contacts.where((e) => predicate(query, e)).toList(); - return SchoolContactList(searched); - } - - bool predicate(String query, SchoolContact contact) { - query = query.toLowerCase(); - final name = contact.name?.toLowerCase(); - final department = contact.department.toLowerCase(); - final description = contact.description?.toLowerCase(); - return department.contains(query) || - (name != null && name.contains(query)) || - (description != null && description.contains(query)) || - contact.phone.contains(query); - } -} diff --git a/lib/school/ywb/entity/application.dart b/lib/school/ywb/entity/application.dart deleted file mode 100644 index 09604dec0..000000000 --- a/lib/school/ywb/entity/application.dart +++ /dev/null @@ -1,162 +0,0 @@ -import 'package:copy_with_extension/copy_with_extension.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:json_annotation/json_annotation.dart'; -import 'package:sit/storage/hive/type_id.dart'; -import 'package:sit/school/utils.dart'; - -part 'application.g.dart'; - -enum YwbApplicationType { - complete("Complete_Init"), - running("Runing_Init"), - todo("Todolist_Init"); - - final String method; - - const YwbApplicationType(this.method); - - String l10nName() => "ywb.type.$name".tr(); - - String get messageListUrl => - 'https://xgfy.sit.edu.cn/unifri-flow/WF/Comm/ProcessRequest.do?DoType=HttpHandler&DoMethod=$method&HttpHandlerName=BP.WF.HttpHandler.WF'; -} - -final _tsFormat = DateFormat("yyyy-MM-dd hh:mm"); - -DateTime _parseTimestamp(dynamic ts) { - return _tsFormat.parse(ts); -} - -@JsonSerializable(createToJson: false) -@HiveType(typeId: CacheHiveType.ywbApplication) -@CopyWith(skipFields: true) -class YwbApplication { - @JsonKey(name: 'WorkID') - @HiveField(0) - final int workId; - @JsonKey(name: 'FK_Flow') - @HiveField(1) - final String functionId; - @JsonKey(name: 'FlowName') - @HiveField(2) - final String name; - @JsonKey(name: 'FlowNote') - @HiveField(3) - final String note; - @JsonKey(name: 'RDT', fromJson: _parseTimestamp) - @HiveField(4) - final DateTime startTs; - @JsonKey(includeFromJson: false, includeToJson: false) - @HiveField(5) - final List track; - - const YwbApplication({ - required this.workId, - required this.functionId, - required this.name, - required this.note, - required this.startTs, - this.track = const [], - }); - - factory YwbApplication.fromJson(Map json) => _$YwbApplicationFromJson(json); - - @override - String toString() { - return { - "workId": workId, - "functionId": functionId, - "name": name, - "note": note, - "startTs": startTs, - "track": track, - }.toString(); - } -} - -@JsonSerializable(createToJson: false) -@HiveType(typeId: CacheHiveType.ywbApplicationTrack) -class YwbApplicationTrack { - @JsonKey(name: "ActionType") - @HiveField(0) - final int actionType; - @JsonKey(name: "ActionTypeText") - @HiveField(1) - final String action; - @JsonKey(name: "EmpFrom") - @HiveField(2) - final String senderId; - @JsonKey(name: "EmpFromT", fromJson: mapChinesePunctuations) - @HiveField(3) - final String senderName; - @JsonKey(name: "EmpTo") - @HiveField(4) - final String receiverId; - @JsonKey(name: "EmpToT", fromJson: mapChinesePunctuations) - @HiveField(5) - final String receiverName; - @JsonKey(name: "Msg", fromJson: mapChinesePunctuations) - @HiveField(6) - final String message; - @JsonKey(name: "RDT", fromJson: _parseTimestamp) - @HiveField(7) - final DateTime timestamp; - @JsonKey(name: "NDFromT", fromJson: mapChinesePunctuations) - @HiveField(8) - final String step; - - bool get isActionOk { - // 发送 - if (actionType == 1) return true; - // 退回 - if (actionType == 2) return false; - // 办结 - if (actionType == 8) return true; - return true; - } - - const YwbApplicationTrack({ - required this.actionType, - required this.action, - required this.senderId, - required this.senderName, - required this.receiverId, - required this.receiverName, - required this.message, - required this.timestamp, - required this.step, - }); - - factory YwbApplicationTrack.fromJson(Map json) => _$YwbApplicationTrackFromJson(json); - - @override - String toString() { - return { - "actionType": actionType, - "action": action, - "senderId": senderId, - "senderName": senderName, - "receiverId": receiverId, - "receiverName": receiverName, - "message": message, - "timestamp": timestamp, - "step": step, - }.toString(); - } -} - -typedef MyYwbApplications = ({ - List todo, - List running, - List complete, -}); - -extension MyYwbApplicationsX on MyYwbApplications { - List resolve(YwbApplicationType type) { - return switch (type) { - YwbApplicationType.todo => todo, - YwbApplicationType.running => running, - YwbApplicationType.complete => complete, - }; - } -} diff --git a/lib/school/ywb/entity/application.g.dart b/lib/school/ywb/entity/application.g.dart deleted file mode 100644 index eb0bb77fc..000000000 --- a/lib/school/ywb/entity/application.g.dart +++ /dev/null @@ -1,212 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'application.dart'; - -// ************************************************************************** -// CopyWithGenerator -// ************************************************************************** - -abstract class _$YwbApplicationCWProxy { - /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. - /// - /// Usage - /// ```dart - /// YwbApplication(...).copyWith(id: 12, name: "My name") - /// ```` - YwbApplication call({ - int? workId, - String? functionId, - String? name, - String? note, - DateTime? startTs, - List? track, - }); -} - -/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfYwbApplication.copyWith(...)`. -class _$YwbApplicationCWProxyImpl implements _$YwbApplicationCWProxy { - const _$YwbApplicationCWProxyImpl(this._value); - - final YwbApplication _value; - - @override - - /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. - /// - /// Usage - /// ```dart - /// YwbApplication(...).copyWith(id: 12, name: "My name") - /// ```` - YwbApplication call({ - Object? workId = const $CopyWithPlaceholder(), - Object? functionId = const $CopyWithPlaceholder(), - Object? name = const $CopyWithPlaceholder(), - Object? note = const $CopyWithPlaceholder(), - Object? startTs = const $CopyWithPlaceholder(), - Object? track = const $CopyWithPlaceholder(), - }) { - return YwbApplication( - workId: workId == const $CopyWithPlaceholder() || workId == null - ? _value.workId - // ignore: cast_nullable_to_non_nullable - : workId as int, - functionId: functionId == const $CopyWithPlaceholder() || functionId == null - ? _value.functionId - // ignore: cast_nullable_to_non_nullable - : functionId as String, - name: name == const $CopyWithPlaceholder() || name == null - ? _value.name - // ignore: cast_nullable_to_non_nullable - : name as String, - note: note == const $CopyWithPlaceholder() || note == null - ? _value.note - // ignore: cast_nullable_to_non_nullable - : note as String, - startTs: startTs == const $CopyWithPlaceholder() || startTs == null - ? _value.startTs - // ignore: cast_nullable_to_non_nullable - : startTs as DateTime, - track: track == const $CopyWithPlaceholder() || track == null - ? _value.track - // ignore: cast_nullable_to_non_nullable - : track as List, - ); - } -} - -extension $YwbApplicationCopyWith on YwbApplication { - /// Returns a callable class that can be used as follows: `instanceOfYwbApplication.copyWith(...)`. - // ignore: library_private_types_in_public_api - _$YwbApplicationCWProxy get copyWith => _$YwbApplicationCWProxyImpl(this); -} - -// ************************************************************************** -// TypeAdapterGenerator -// ************************************************************************** - -class YwbApplicationAdapter extends TypeAdapter { - @override - final int typeId = 73; - - @override - YwbApplication read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return YwbApplication( - workId: fields[0] as int, - functionId: fields[1] as String, - name: fields[2] as String, - note: fields[3] as String, - startTs: fields[4] as DateTime, - track: (fields[5] as List).cast(), - ); - } - - @override - void write(BinaryWriter writer, YwbApplication obj) { - writer - ..writeByte(6) - ..writeByte(0) - ..write(obj.workId) - ..writeByte(1) - ..write(obj.functionId) - ..writeByte(2) - ..write(obj.name) - ..writeByte(3) - ..write(obj.note) - ..writeByte(4) - ..write(obj.startTs) - ..writeByte(5) - ..write(obj.track); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is YwbApplicationAdapter && runtimeType == other.runtimeType && typeId == other.typeId; -} - -class YwbApplicationTrackAdapter extends TypeAdapter { - @override - final int typeId = 74; - - @override - YwbApplicationTrack read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return YwbApplicationTrack( - actionType: fields[0] as int, - action: fields[1] as String, - senderId: fields[2] as String, - senderName: fields[3] as String, - receiverId: fields[4] as String, - receiverName: fields[5] as String, - message: fields[6] as String, - timestamp: fields[7] as DateTime, - step: fields[8] as String, - ); - } - - @override - void write(BinaryWriter writer, YwbApplicationTrack obj) { - writer - ..writeByte(9) - ..writeByte(0) - ..write(obj.actionType) - ..writeByte(1) - ..write(obj.action) - ..writeByte(2) - ..write(obj.senderId) - ..writeByte(3) - ..write(obj.senderName) - ..writeByte(4) - ..write(obj.receiverId) - ..writeByte(5) - ..write(obj.receiverName) - ..writeByte(6) - ..write(obj.message) - ..writeByte(7) - ..write(obj.timestamp) - ..writeByte(8) - ..write(obj.step); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is YwbApplicationTrackAdapter && runtimeType == other.runtimeType && typeId == other.typeId; -} - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -YwbApplication _$YwbApplicationFromJson(Map json) => YwbApplication( - workId: json['WorkID'] as int, - functionId: json['FK_Flow'] as String, - name: json['FlowName'] as String, - note: json['FlowNote'] as String, - startTs: _parseTimestamp(json['RDT']), - ); - -YwbApplicationTrack _$YwbApplicationTrackFromJson(Map json) => YwbApplicationTrack( - actionType: json['ActionType'] as int, - action: json['ActionTypeText'] as String, - senderId: json['EmpFrom'] as String, - senderName: mapChinesePunctuations(json['EmpFromT'] as String), - receiverId: json['EmpTo'] as String, - receiverName: mapChinesePunctuations(json['EmpToT'] as String), - message: mapChinesePunctuations(json['Msg'] as String), - timestamp: _parseTimestamp(json['RDT']), - step: mapChinesePunctuations(json['NDFromT'] as String), - ); diff --git a/lib/school/ywb/entity/service.dart b/lib/school/ywb/entity/service.dart deleted file mode 100644 index beed795e2..000000000 --- a/lib/school/ywb/entity/service.dart +++ /dev/null @@ -1,117 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:json_annotation/json_annotation.dart'; -import 'package:sit/storage/hive/type_id.dart'; -import 'package:sit/utils/iconfont.dart'; - -part 'service.g.dart'; - -@JsonSerializable(createToJson: false) -@HiveType(typeId: CacheHiveType.ywbService) -class YwbService { - @JsonKey(name: 'appID') - @HiveField(0) - final String id; - @JsonKey(name: 'appName') - @HiveField(1) - final String name; - @JsonKey(name: 'appDescribe') - @HiveField(2) - final String summary; - @JsonKey(name: 'appStatus') - @HiveField(3) - final int status; - @JsonKey(name: 'appCount') - @HiveField(4) - final int count; - @JsonKey(name: 'appIcon') - @HiveField(5) - final String iconName; - - IconData get icon => IconFont.query(iconName); - - const YwbService({ - required this.id, - required this.name, - required this.summary, - required this.status, - required this.count, - required this.iconName, - }); - - factory YwbService.fromJson(Map json) => _$YwbServiceFromJson(json); - - @override - String toString() { - return { - "id": id, - "name": name, - "summary": summary, - "status": status, - "count": count, - "iconName": iconName, - }.toString(); - } -} - -@HiveType(typeId: CacheHiveType.ywbServiceDetails) -class YwbServiceDetails { - @HiveField(0) - final String id; - @HiveField(1) - final List sections; - - const YwbServiceDetails({ - required this.id, - required this.sections, - }); - - @override - String toString() { - return { - "id": id, - "sections": sections, - }.toString(); - } -} - -@JsonSerializable(createToJson: false) -@HiveType(typeId: CacheHiveType.ywbServiceDetailSection) -class YwbServiceDetailSection { - @JsonKey(name: 'formName') - @HiveField(0) - final String section; - @JsonKey() - @HiveField(1) - final String type; - @JsonKey() - @HiveField(2) - final DateTime createTime; - @JsonKey() - @HiveField(3) - final String content; - - const YwbServiceDetailSection({ - required this.type, - required this.section, - required this.createTime, - required this.content, - }); - - factory YwbServiceDetailSection.fromJson(Map json) => _$YwbServiceDetailSectionFromJson(json); - - @override - String toString() { - return { - "type": type, - "section": section, - "createTime": createTime, - "content": content, - }.toString(); - } -} - -extension YwbServiceDetailSectionX on YwbServiceDetailSection { - bool get isEmpty => content.isEmpty; - - bool get isNotEmpty => content.isNotEmpty; -} diff --git a/lib/school/ywb/entity/service.g.dart b/lib/school/ywb/entity/service.g.dart deleted file mode 100644 index ce428c958..000000000 --- a/lib/school/ywb/entity/service.g.dart +++ /dev/null @@ -1,150 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'service.dart'; - -// ************************************************************************** -// TypeAdapterGenerator -// ************************************************************************** - -class YwbServiceAdapter extends TypeAdapter { - @override - final int typeId = 72; - - @override - YwbService read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return YwbService( - id: fields[0] as String, - name: fields[1] as String, - summary: fields[2] as String, - status: fields[3] as int, - count: fields[4] as int, - iconName: fields[5] as String, - ); - } - - @override - void write(BinaryWriter writer, YwbService obj) { - writer - ..writeByte(6) - ..writeByte(0) - ..write(obj.id) - ..writeByte(1) - ..write(obj.name) - ..writeByte(2) - ..write(obj.summary) - ..writeByte(3) - ..write(obj.status) - ..writeByte(4) - ..write(obj.count) - ..writeByte(5) - ..write(obj.iconName); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is YwbServiceAdapter && runtimeType == other.runtimeType && typeId == other.typeId; -} - -class YwbServiceDetailsAdapter extends TypeAdapter { - @override - final int typeId = 70; - - @override - YwbServiceDetails read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return YwbServiceDetails( - id: fields[0] as String, - sections: (fields[1] as List).cast(), - ); - } - - @override - void write(BinaryWriter writer, YwbServiceDetails obj) { - writer - ..writeByte(2) - ..writeByte(0) - ..write(obj.id) - ..writeByte(1) - ..write(obj.sections); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is YwbServiceDetailsAdapter && runtimeType == other.runtimeType && typeId == other.typeId; -} - -class YwbServiceDetailSectionAdapter extends TypeAdapter { - @override - final int typeId = 71; - - @override - YwbServiceDetailSection read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return YwbServiceDetailSection( - type: fields[1] as String, - section: fields[0] as String, - createTime: fields[2] as DateTime, - content: fields[3] as String, - ); - } - - @override - void write(BinaryWriter writer, YwbServiceDetailSection obj) { - writer - ..writeByte(4) - ..writeByte(0) - ..write(obj.section) - ..writeByte(1) - ..write(obj.type) - ..writeByte(2) - ..write(obj.createTime) - ..writeByte(3) - ..write(obj.content); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is YwbServiceDetailSectionAdapter && runtimeType == other.runtimeType && typeId == other.typeId; -} - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -YwbService _$YwbServiceFromJson(Map json) => YwbService( - id: json['appID'] as String, - name: json['appName'] as String, - summary: json['appDescribe'] as String, - status: json['appStatus'] as int, - count: json['appCount'] as int, - iconName: json['appIcon'] as String, - ); - -YwbServiceDetailSection _$YwbServiceDetailSectionFromJson(Map json) => YwbServiceDetailSection( - type: json['type'] as String, - section: json['formName'] as String, - createTime: DateTime.parse(json['createTime'] as String), - content: json['content'] as String, - ); diff --git a/lib/school/ywb/i18n.dart b/lib/school/ywb/i18n.dart deleted file mode 100644 index ad76b0904..000000000 --- a/lib/school/ywb/i18n.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:sit/l10n/common.dart'; - -const i18n = _I18n(); - -class _I18n with CommonI18nMixin { - const _I18n(); - - static const ns = "ywb"; - - final mine = const _Mine(); - final details = const _Details(); - - String get title => "$ns.title".tr(); - - String get info => "$ns.info".tr(); - - String get mineAction => mine.title; - - String get noServicesTip => "$ns.noServicesTip".tr(); -} - -class _Mine { - const _Mine(); - - static const ns = "${_I18n.ns}.mine"; - - String get title => "$ns.title".tr(); - - String get noApplicationsTip => "$ns.noApplicationsTip".tr(); -} - -class _Details { - const _Details(); - - static const ns = "${_I18n.ns}.details"; - - String get apply => "$ns.apply".tr(); -} diff --git a/lib/school/ywb/index.dart b/lib/school/ywb/index.dart deleted file mode 100644 index 5b294aa78..000000000 --- a/lib/school/ywb/index.dart +++ /dev/null @@ -1,75 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:sit/design/widgets/app.dart'; -import 'package:sit/school/ywb/entity/application.dart'; -import 'package:sit/school/ywb/init.dart'; -import 'package:rettulf/rettulf.dart'; - -import "i18n.dart"; -import 'widgets/application.dart'; - -const _applicationLength = 2; - -class YwbAppCard extends StatefulWidget { - const YwbAppCard({super.key}); - - @override - State createState() => _YwbAppCardState(); -} - -class _YwbAppCardState extends State { - final $running = YwbInit.applicationStorage.listenApplicationListOf(YwbApplicationType.running); - @override - void initState() { - super.initState(); - $running.addListener(refresh); - } - - @override - void dispose() { - $running.removeListener(refresh); - super.dispose(); - } - - void refresh() { - setState(() {}); - } - - @override - Widget build(BuildContext context) { - return AppCard( - title: i18n.title.text(), - view: buildRunningCard(), - leftActions: [ - FilledButton.icon( - onPressed: () { - context.push("/ywb"); - }, - icon: const Icon(Icons.list_alt), - label: i18n.seeAll.text(), - ), - OutlinedButton.icon( - onPressed: () { - context.push("/ywb/mine"); - }, - label: i18n.mineAction.text(), - icon: const Icon(Icons.mail_outlined), - ) - ], - ); - } - - Widget buildRunningCard() { - final running = YwbInit.applicationStorage.getApplicationListOf(YwbApplicationType.running); - if (running == null) return const SizedBox(); - final applications = running.sublist(0, min(_applicationLength, running.length)); - return applications - .map((e) => YwbApplicationTile(e).inCard( - clip: Clip.hardEdge, - )) - .toList() - .column(); - } -} diff --git a/lib/school/ywb/init.dart b/lib/school/ywb/init.dart deleted file mode 100644 index 2b9c867f0..000000000 --- a/lib/school/ywb/init.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'service/service.dart'; -import 'service/application.dart'; -import 'storage/service.dart'; -import 'storage/application.dart'; - -class YwbInit { - static late YwbServiceService serviceService; - static late YwbServiceStorage serviceStorage; - static late YwbApplicationService applicationService; - static late YwbApplicationStorage applicationStorage; - - static void init() { - serviceService = const YwbServiceService(); - serviceStorage = const YwbServiceStorage(); - applicationService = const YwbApplicationService(); - applicationStorage = const YwbApplicationStorage(); - } -} diff --git a/lib/school/ywb/page/application.dart b/lib/school/ywb/page/application.dart deleted file mode 100644 index eb673034f..000000000 --- a/lib/school/ywb/page/application.dart +++ /dev/null @@ -1,166 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:sit/design/widgets/common.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:sit/utils/error.dart'; - -import '../entity/application.dart'; -import '../init.dart'; -import '../widgets/application.dart'; -import '../i18n.dart'; - -class YwbMyApplicationListPage extends StatefulWidget { - const YwbMyApplicationListPage({super.key}); - - @override - State createState() => _YwbMyApplicationListPageState(); -} - -class _YwbMyApplicationListPageState extends State { - late final $loadingStates = ValueNotifier(YwbApplicationType.values.map((type) => false).toList()); - - @override - void dispose() { - $loadingStates.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - bottomNavigationBar: PreferredSize( - preferredSize: const Size.fromHeight(4), - child: $loadingStates >> - (ctx, states) { - return !states.any((state) => state == true) ? const SizedBox() : const LinearProgressIndicator(); - }, - ), - body: DefaultTabController( - length: YwbApplicationType.values.length, - child: NestedScrollView( - floatHeaderSlivers: true, - headerSliverBuilder: (context, innerBoxIsScrolled) { - // These are the slivers that show up in the "outer" scroll view. - return [ - SliverOverlapAbsorber( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), - sliver: SliverAppBar( - floating: true, - title: i18n.mine.title.text(), - forceElevated: innerBoxIsScrolled, - bottom: TabBar( - isScrollable: true, - tabs: YwbApplicationType.values - .map((type) => Tab( - child: type.l10nName().text(), - )) - .toList(), - ), - ), - ), - ]; - }, - body: TabBarView( - // These are the contents of the tab views, below the tabs. - children: YwbApplicationType.values.mapIndexed((i, type) { - return YwbApplicationLoadingList( - type: type, - onLoadingChanged: (bool value) { - final newStates = List.of($loadingStates.value); - newStates[i] = value; - $loadingStates.value = newStates; - }, - ); - }).toList(), - ), - ), - ), - ); - } -} - -class YwbApplicationLoadingList extends StatefulWidget { - final YwbApplicationType type; - final ValueChanged onLoadingChanged; - - const YwbApplicationLoadingList({ - super.key, - required this.type, - required this.onLoadingChanged, - }); - - @override - State createState() => _YwbApplicationLoadingListState(); -} - -class _YwbApplicationLoadingListState extends State with AutomaticKeepAliveClientMixin { - bool isFetching = false; - late var applications = YwbInit.applicationStorage.getApplicationListOf(widget.type); - - @override - bool get wantKeepAlive => true; - - @override - void initState() { - super.initState(); - Future.delayed(Duration.zero).then((value) async { - await fetch(); - }); - } - - @override - Widget build(BuildContext context) { - super.build(context); - final applications = this.applications; - return CustomScrollView( - slivers: [ - SliverOverlapInjector( - // This is the flip side of the SliverOverlapAbsorber above. - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), - ), - if (applications != null) - if (applications.isEmpty) - SliverFillRemaining( - child: LeavingBlank( - icon: Icons.inbox_outlined, - desc: i18n.mine.noApplicationsTip, - ), - ) - else - SliverList.builder( - itemCount: applications.length, - itemBuilder: (ctx, index) { - return YwbApplicationTile(applications[index]); - }, - ), - ], - ); - } - - Future fetch() async { - if (isFetching) return; - if (!mounted) return; - setState(() { - isFetching = true; - }); - widget.onLoadingChanged(true); - final type = widget.type; - try { - final applications = await YwbInit.applicationService.getApplicationsOf(type); - await YwbInit.applicationStorage.setApplicationListOf(type, applications); - if (!mounted) return; - setState(() { - this.applications = applications; - isFetching = false; - }); - widget.onLoadingChanged(false); - } catch (error, stackTrace) { - debugPrintError(error, stackTrace); - if (!mounted) return; - setState(() { - isFetching = false; - }); - widget.onLoadingChanged(false); - } - } -} diff --git a/lib/school/ywb/page/details.dart b/lib/school/ywb/page/details.dart deleted file mode 100644 index 51457d1e8..000000000 --- a/lib/school/ywb/page/details.dart +++ /dev/null @@ -1,113 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:sit/design/widgets/fab.dart'; -import 'package:sit/utils/error.dart'; -import 'package:sit/utils/guard_launch.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:universal_platform/universal_platform.dart'; - -import '../entity/service.dart'; -import '../init.dart'; -import '../page/form.dart'; -import '../widgets/detail.dart'; -import "../i18n.dart"; - -class YwbServiceDetailsPage extends StatefulWidget { - final YwbService meta; - - const YwbServiceDetailsPage({ - super.key, - required this.meta, - }); - - @override - State createState() => _YwbServiceDetailsPageState(); -} - -class _YwbServiceDetailsPageState extends State { - String get id => widget.meta.id; - String get name => widget.meta.name; - late YwbServiceDetails? details = YwbInit.serviceStorage.getServiceDetails(id); - final controller = ScrollController(); - bool isFetching = false; - - @override - void initState() { - super.initState(); - refresh(); - } - - Future refresh() async { - if (!mounted) return; - setState(() { - isFetching = true; - }); - try { - final meta = await YwbInit.serviceService.getServiceDetails(id); - YwbInit.serviceStorage.setMetaDetails(id, meta); - if (!mounted) return; - setState(() { - isFetching = false; - details = meta; - }); - } catch (error, stackTrace) { - debugPrintError(error, stackTrace); - if (!mounted) return; - setState(() { - isFetching = false; - }); - } - } - - @override - void dispose() { - controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final details = this.details; - return Scaffold( - body: SelectionArea( - child: CustomScrollView( - controller: controller, - slivers: [ - SliverAppBar( - floating: true, - title: Text(name), - bottom: isFetching - ? const PreferredSize( - preferredSize: Size.fromHeight(4), - child: LinearProgressIndicator(), - ) - : null, - ), - if (details != null) - SliverList.separated( - itemCount: details.sections.length, - itemBuilder: (ctx, i) => YwbApplicationDetailSectionBlock(details.sections[i]), - separatorBuilder: (ctx, i) => const Divider(), - ), - ], - ), - ), - floatingActionButton: AutoHideFAB.extended( - controller: controller, - onPressed: () => openInApp(), - icon: const Icon(Icons.east), - label: i18n.details.apply.text(), - ), - ); - } - - void openInApp() { - if (UniversalPlatform.isDesktopOrWeb) { - guardLaunchUrlString(context, "http://ywb.sit.edu.cn/v1/#/"); - } else { - // 跳转到申请页面 - final String applyUrl = - 'http://ywb.sit.edu.cn/v1/#/flow?src=http://ywb.sit.edu.cn/unifri-flow/WF/MyFlow.htm?FK_Flow=$id'; - context.navigator.push(MaterialPageRoute(builder: (_) => YwbInAppViewPage(title: name, url: applyUrl))); - } - } -} diff --git a/lib/school/ywb/page/form.dart b/lib/school/ywb/page/form.dart deleted file mode 100644 index 116f540d9..000000000 --- a/lib/school/ywb/page/form.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:sit/init.dart'; -import 'package:sit/utils/cookies.dart'; -import 'package:webview_flutter/webview_flutter.dart'; - -const ywbUrl = 'https://xgfy.sit.edu.cn'; - -class YwbInAppViewPage extends StatefulWidget { - final String title; - final String url; - - const YwbInAppViewPage({super.key, required this.title, required this.url}); - - @override - State createState() => _YwbInAppViewPageState(); -} - -class _YwbInAppViewPageState extends State { - final WebViewController controller = WebViewController()..setJavaScriptMode(JavaScriptMode.unrestricted); - final WebViewCookieManager cookieManager = WebViewCookieManager(); - List? cookies; - - @override - void initState() { - super.initState(); - Init.cookieJar.loadAsWebViewCookie(Uri.parse(ywbUrl)).then((value) { - cookies = value; - for (final cookie in value) { - cookieManager.setCookie(cookie); - } - controller.loadRequest(Uri.parse(widget.url)); - setState(() {}); - }).onError((error, stackTrace) {}); - } - - @override - Widget build(BuildContext context) { - final cookies = this.cookies; - return Scaffold( - appBar: AppBar( - title: Text(widget.title), - ), - body: cookies == null ? const CircularProgressIndicator.adaptive() : buildWebPage(cookies), - ); - } - - Widget buildWebPage(List cookies) { - return WebViewWidget( - controller: controller, - ); - } -} diff --git a/lib/school/ywb/page/service.dart b/lib/school/ywb/page/service.dart deleted file mode 100644 index bf8d86548..000000000 --- a/lib/school/ywb/page/service.dart +++ /dev/null @@ -1,100 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:sit/design/widgets/common.dart'; -import 'package:sit/design/adaptive/dialog.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:sit/utils/error.dart'; - -import '../entity/service.dart'; -import '../init.dart'; -import '../widgets/service.dart'; -import '../i18n.dart'; - -class YwbServiceListPage extends StatefulWidget { - const YwbServiceListPage({super.key}); - - @override - State createState() => _YwbServiceListPageState(); -} - -class _YwbServiceListPageState extends State { - /// in descending order - List? metaList; - bool isLoading = false; - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - refresh(); - } - - Future refresh() async { - if (!mounted) return; - setState(() { - metaList = YwbInit.serviceStorage.serviceList; - isLoading = true; - }); - try { - final metaList = await YwbInit.serviceService.getServices(); - metaList.sortBy((e) => -e.count); - YwbInit.serviceStorage.serviceList = metaList; - if (!mounted) return; - setState(() { - this.metaList = metaList; - isLoading = false; - }); - } catch (error, stackTrace) { - debugPrintError(error, stackTrace); - if (!mounted) return; - setState(() { - isLoading = false; - }); - } - } - - @override - Widget build(BuildContext context) { - final metaList = this.metaList; - return Scaffold( - body: CustomScrollView( - slivers: [ - SliverAppBar( - floating: true, - title: i18n.title.text(), - actions: [ - IconButton( - icon: const Icon(Icons.info_outline), - onPressed: () async { - await context.showTip( - title: i18n.title, - desc: i18n.info, - ok: i18n.close, - ); - }, - ), - ], - bottom: isLoading - ? const PreferredSize( - preferredSize: Size.fromHeight(4), - child: LinearProgressIndicator(), - ) - : null, - ), - if (metaList != null) - if (metaList.isEmpty) - SliverFillRemaining( - child: LeavingBlank( - icon: Icons.inbox_outlined, - desc: i18n.noServicesTip, - ), - ) - else - SliverList.builder( - itemCount: metaList.length, - itemBuilder: (ctx, i) => YwbServiceTile(meta: metaList[i], isHot: i < 3), - ), - ], - ), - ); - } -} diff --git a/lib/school/ywb/service/application.dart b/lib/school/ywb/service/application.dart deleted file mode 100644 index cce98e01b..000000000 --- a/lib/school/ywb/service/application.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'dart:convert'; - -import 'package:dio/dio.dart'; -import 'package:sit/design/animation/progress.dart'; -import 'package:sit/init.dart'; - -import 'package:sit/session/ywb.dart'; - -import '../entity/application.dart'; - -class YwbApplicationService { - YwbSession get session => Init.ywbSession; - - const YwbApplicationService(); - - Future> getApplicationsOf( - YwbApplicationType type, { - void Function(double progress)? onProgress, - }) async { - final progress = ProgressWatcher(callback: onProgress); - final response = await session.request( - type.messageListUrl, - data: { - "myFlow": 1, - "pageIdx": 1, - "pageSize": 99, - }, - options: Options( - contentType: Headers.formUrlEncodedContentType, - method: "POST", - ), - ); - progress.value = 0.2; - final List data = jsonDecode(response.data); - // filter empty application - data.retainWhere((e) => e["WorkID"] is int); - final List messages = data.map((e) => YwbApplication.fromJson(e)).toList(); - final res = []; - for (final msg in messages) { - final track = await getTrack(workId: msg.workId, functionId: msg.functionId); - res.add(msg.copyWith(track: track)); - progress.value += 0.8 / messages.length; - } - progress.value = 1; - return res; - } - - Future> getTrack({ - required int workId, - required String functionId, - }) async { - // Authentication cookie is even not required! - final res = await session.request( - "http://ywb.sit.edu.cn/unifri-flow/WF/Comm/ProcessRequest.do?&DoType=HttpHandler&DoMethod=TimeBase_Init&HttpHandlerName=BP.WF.HttpHandler.WF_WorkOpt_OneWork", - data: { - "WorkID": workId, - "FK_Flow": functionId, - }, - options: Options( - contentType: Headers.formUrlEncodedContentType, - method: "POST", - ), - ); - final Map payload = jsonDecode(res.data); - final List trackRaw = payload["Track"]; - final track = trackRaw.map((e) => YwbApplicationTrack.fromJson(e)).toList(); - return track; - } -} diff --git a/lib/school/ywb/service/service.dart b/lib/school/ywb/service/service.dart deleted file mode 100644 index bb09f0b52..000000000 --- a/lib/school/ywb/service/service.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:dio/dio.dart'; -import 'package:sit/init.dart'; - -import 'package:sit/session/ywb.dart'; - -import '../entity/service.dart'; - -const String _serviceFunctionList = 'https://xgfy.sit.edu.cn/app/public/queryAppManageJson'; -const String _serviceFunctionDetail = 'https://xgfy.sit.edu.cn/app/public/queryAppFormJson'; - -class YwbServiceService { - YwbSession get session => Init.ywbSession; - - const YwbServiceService(); - - Future> getServices() async { - final response = await session.request( - _serviceFunctionList, - data: '{"appObject":"student","appName":null}', - options: Options( - responseType: ResponseType.json, - method: "POST", - ), - ); - - final Map data = response.data; - final List functionList = (data['value'] as List) - .map((e) => YwbService.fromJson(e)) - .where((element) => element.status == 1) // Filter functions unavailable. - .toList(); - - return functionList; - } - - Future getServiceDetails(String functionId) async { - final response = await session.request( - _serviceFunctionDetail, - data: '{"appID":"$functionId"}', - options: Options( - responseType: ResponseType.json, - method: "POST", - ), - ); - final Map data = response.data; - final List sections = - (data['value'] as List).map((e) => YwbServiceDetailSection.fromJson(e)).toList(); - - return YwbServiceDetails(id: functionId, sections: sections); - } -} diff --git a/lib/school/ywb/storage/application.dart b/lib/school/ywb/storage/application.dart deleted file mode 100644 index bfd842295..000000000 --- a/lib/school/ywb/storage/application.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:hive_flutter/hive_flutter.dart'; -import 'package:sit/storage/hive/init.dart'; - -import '../entity/application.dart'; - -class _K { - static const ns = "/application"; - - static String applicationListOf(YwbApplicationType type) => "$ns/$type"; -} - -class YwbApplicationStorage { - Box get box => HiveInit.ywb; - - const YwbApplicationStorage(); - - List? getApplicationListOf(YwbApplicationType type) => - (box.get(_K.applicationListOf(type)) as List?)?.cast(); - - Future setApplicationListOf(YwbApplicationType type, List? newV) => - box.put(_K.applicationListOf(type), newV); - - Listenable listenApplicationListOf(YwbApplicationType type) => box.listenable(keys: [_K.applicationListOf(type)]); -} diff --git a/lib/school/ywb/storage/service.dart b/lib/school/ywb/storage/service.dart deleted file mode 100644 index f6bd5eeaf..000000000 --- a/lib/school/ywb/storage/service.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:hive/hive.dart'; -import 'package:sit/storage/hive/init.dart'; - -import '../entity/service.dart'; - -class _K { - static const ns = "/meta"; - static const serviceList = "$ns/serviceList"; - - static String details(String applicationId) => "$ns/details/$applicationId"; -} - -class YwbServiceStorage { - Box get box => HiveInit.ywb; - - const YwbServiceStorage(); - - YwbServiceDetails? getServiceDetails(String applicationId) => box.get(_K.details(applicationId)); - - void setMetaDetails(String applicationId, YwbServiceDetails? newV) => box.put(_K.details(applicationId), newV); - - List? get serviceList => (box.get(_K.serviceList) as List?)?.cast(); - - set serviceList(List? newV) => box.put(_K.serviceList, newV); -} diff --git a/lib/school/ywb/widgets/application.dart b/lib/school/ywb/widgets/application.dart deleted file mode 100644 index 3cde3c4f6..000000000 --- a/lib/school/ywb/widgets/application.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:sit/design/widgets/expansion_tile.dart'; -import 'package:sit/l10n/extension.dart'; -import 'package:rettulf/rettulf.dart'; - -import '../entity/application.dart'; - -class YwbApplicationTile extends StatelessWidget { - final YwbApplication application; - - const YwbApplicationTile( - this.application, { - super.key, - }); - - @override - Widget build(BuildContext context) { - return AnimatedExpansionTile( - title: "${application.name} #${application.workId}".text(), - subtitle: context.formatYmdWeekText(application.startTs).text(), - trailing: const Icon(Icons.keyboard_arrow_down), - children: application.track.map((e) => YwbApplicationTrackTile(e)).toList(), - ); - } -} - -class YwbApplicationTrackTile extends StatelessWidget { - final YwbApplicationTrack track; - - const YwbApplicationTrackTile( - this.track, { - super.key, - }); - - @override - Widget build(BuildContext context) { - return ListTile( - isThreeLine: true, - leading: track.isActionOk - ? const Icon(Icons.check, color: Colors.green) - : const Icon(Icons.error_outline, color: Colors.redAccent), - title: track.step.text(), - subtitle: [ - context.formatYmdhmNum(track.timestamp).text(), - if (track.message.isNotEmpty) track.message.text(), - track.action.text(), - ].column(caa: CrossAxisAlignment.start), - trailing: track.senderName.text(), - ); - } -} - -// final String resultUrl = -// 'https://xgfy.sit.edu.cn/unifri-flow/WF/mobile/index.html?ismobile=1&FK_Flow=${msg.functionId}&WorkID=${msg.workId}&IsReadonly=1&IsView=1'; -// Navigator.of(context) -// .push(MaterialPageRoute(builder: (_) => YwbInAppViewPage(title: msg.name, url: resultUrl))); diff --git a/lib/school/ywb/widgets/detail.dart b/lib/school/ywb/widgets/detail.dart deleted file mode 100644 index 4a80a07a8..000000000 --- a/lib/school/ywb/widgets/detail.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'dart:convert'; - -import 'package:flutter/material.dart'; -import 'package:sit/widgets/html.dart'; -import 'package:rettulf/rettulf.dart'; - -import '../entity/service.dart'; - -class YwbApplicationDetailSectionBlock extends StatelessWidget { - final YwbServiceDetailSection section; - - const YwbApplicationDetailSectionBlock(this.section, {super.key}); - - @override - Widget build(BuildContext context) { - final bodyWidget = switch (section.type) { - 'html' => buildHtmlSection(section.content), - 'json' => buildJsonSection(section.content), - _ => const SizedBox(), - }; - - return Padding( - padding: const EdgeInsets.fromLTRB(10, 5, 10, 5), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(section.section, style: context.textTheme.headlineSmall), - bodyWidget, - ], - ), - ); - } - - Widget buildJsonSection(String content) { - final Map kvPairs = jsonDecode(content); - List items = []; - kvPairs.forEach((key, value) => items.add(Text('$key: $value'))); - return Column(crossAxisAlignment: CrossAxisAlignment.start, children: items); - } - - Widget buildHtmlSection(String content) { - // TODO: cannot download pdf files - final html = content.replaceAll('../app/files/', 'https://xgfy.sit.edu.cn/app/files/'); - return RestyledHtmlWidget(html); - } -} diff --git a/lib/school/ywb/widgets/service.dart b/lib/school/ywb/widgets/service.dart deleted file mode 100644 index 72f8ce604..000000000 --- a/lib/school/ywb/widgets/service.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/material.dart'; -import 'package:rettulf/rettulf.dart'; - -import '../entity/service.dart'; -import '../page/details.dart'; - -const List _serviceColors = [ - Colors.orangeAccent, - Colors.redAccent, - Colors.blueAccent, - Colors.grey, - Colors.green, - Colors.yellowAccent, - Colors.cyan, - Colors.purple, - Colors.teal, -]; - -class YwbServiceTile extends StatelessWidget { - final YwbService meta; - final bool isHot; - - const YwbServiceTile({super.key, required this.meta, required this.isHot}); - - @override - Widget build(BuildContext context) { - final colorIndex = Random(meta.id.hashCode).nextInt(_serviceColors.length); - final color = _serviceColors[colorIndex]; - final style = context.textTheme.bodyMedium; - final views = isHot - ? [ - Text(meta.count.toString(), style: style), - const Icon( - Icons.local_fire_department_rounded, - color: Colors.red, - ), - ].row(mas: MainAxisSize.min) - : Text(meta.count.toString(), style: style); - - return ListTile( - leading: Icon(meta.icon, size: 35, color: color).center().sized(w: 40, h: 40), - title: Text( - meta.name, - overflow: TextOverflow.ellipsis, - ), - subtitle: Text( - meta.summary, - overflow: TextOverflow.ellipsis, - ), - trailing: views, - onTap: () { - // TODO: details page - context.navigator.push(MaterialPageRoute(builder: (_) => YwbServiceDetailsPage(meta: meta))); - }, - ); - } -} diff --git a/lib/session/auth.dart b/lib/session/auth.dart deleted file mode 100644 index f3494fb81..000000000 --- a/lib/session/auth.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'dart:convert'; - -import 'package:dio/dio.dart'; -import 'package:flutter/foundation.dart'; -import 'package:sit/init.dart'; -import 'package:sit/utils/error.dart'; - -class AuthSession { - static final String _ocrServerUrl = - utf8.decode(base64Decode('aHR0cHM6Ly9hcGkua2l0ZS5zdW5ueXNhYi5jbi9hcGkvb2NyL2NhcHRjaGE=')); - - static Dio get dio => Init.dio; - - static Future recognizeOaCaptcha(Uint8List imageData) async { - try { - final response = await dio.post(_ocrServerUrl, data: base64Encode(imageData)); - final result = response.data; - return result['code'] == 0 ? result['data'] as String? : null; - } catch (error, stackTrace) { - debugPrintError(error, stackTrace); - return null; - } - } -} diff --git a/lib/session/class2nd.dart b/lib/session/class2nd.dart deleted file mode 100644 index 932737303..000000000 --- a/lib/session/class2nd.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:dio/dio.dart'; -import 'package:sit/session/sso.dart'; - -class Class2ndSession { - final SsoSession ssoSession; - - Class2ndSession({required this.ssoSession}); - - bool _needRedirectToLoginPage(String data) { - return data.startsWith(''); - } - - Future request( - String url, { - Map? para, - data, - Options? options, - ProgressCallback? onSendProgress, - ProgressCallback? onReceiveProgress, - }) async { - Future fetch() { - return ssoSession.request( - url, - para: para, - data: data, - options: options, - onSendProgress: onSendProgress, - onReceiveProgress: onReceiveProgress, - ); - } - - var res = await fetch(); - final responseData = res.data; - // 如果返回值是登录页面,那就从 SSO 跳转一次以登录. - if (responseData is String && _needRedirectToLoginPage(responseData)) { - await ssoSession.request( - 'https://authserver.sit.edu.cn/authserver/login?service=http%3A%2F%2Fsc.sit.edu.cn%2Flogin.jsp', - options: Options( - method: "GET", - ), - ); - res = await fetch(); - } - return res; - } -} diff --git a/lib/session/gms.dart b/lib/session/gms.dart deleted file mode 100644 index 22db0bfb9..000000000 --- a/lib/session/gms.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:dio/dio.dart'; - -import 'package:sit/session/sso.dart'; - -/// gms.sit.edu.cn -/// for postgraduate -class GmsSession { - final SsoSession ssoSession; - - const GmsSession({required this.ssoSession}); - - Future request( - String url, { - Map? para, - data, - Options? options, - ProgressCallback? onSendProgress, - ProgressCallback? onReceiveProgress, - }) async { - options ??= Options(); - // TODO: is this really necessary? - options.contentType = 'application/x-www-form-urlencoded;charset=utf-8'; - - Future fetch() async { - return await ssoSession.request( - url, - para: para, - data: data, - options: options, - onSendProgress: onSendProgress, - onReceiveProgress: onReceiveProgress, - ); - } - - final response = await fetch(); - final content = response.data; - if (content is String && content.contains("正在登录") == true) { - await authGmsService(); - return await fetch(); - } - return response; - } - - Future authGmsService() async { - final authRes = await ssoSession.request( - "https://authserver.sit.edu.cn/authserver/login?service=http%3A%2F%2Fgms.sit.edu.cn%2Fepstar%2Fweb%2Fswms%2Fmainframe%2Fhome%2Findex.jsp", - options: Options( - method: "GET", - ), - ); - return authRes.statusCode == 302; - } -} diff --git a/lib/session/jwxt.dart b/lib/session/jwxt.dart deleted file mode 100644 index 02e518694..000000000 --- a/lib/session/jwxt.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'package:dio/dio.dart'; -import 'package:flutter/foundation.dart'; - -import 'package:sit/session/sso.dart'; - -/// jwxt.sit.edu.cn -/// for undergraduate -class JwxtSession { - final SsoSession ssoSession; - - const JwxtSession({required this.ssoSession}); - - Future refreshCookie() async { - await ssoSession.request( - 'http://jwxt.sit.edu.cn/sso/jziotlogin', - options: Options( - method: "GET", - ), - ); - } - - bool _isRedirectedToLoginPage(Response response) { - return response.realUri.path == '/jwglxt/xtgl/login_slogin.html'; - } - - Future request( - String url, { - Map? para, - data, - Options? options, - ProgressCallback? onSendProgress, - ProgressCallback? onReceiveProgress, - }) async { - options ??= Options(); - // TODO: is this really necessary? - options.contentType = 'application/x-www-form-urlencoded;charset=utf-8'; - Future fetch() async { - return await ssoSession.request( - url, - para: para, - data: data, - options: options, - onSendProgress: onSendProgress, - onReceiveProgress: onReceiveProgress, - ); - } - - final response = await fetch(); - // 如果返回值是登录页面,那就从 SSO 跳转一次以登录. - if (_isRedirectedToLoginPage(response)) { - debugPrint('JwxtSession requires login'); - await refreshCookie(); - return await fetch(); - } - // 如果还是需要登录 - if (_isRedirectedToLoginPage(response)) { - debugPrint('JwxtSession still requires login'); - await refreshCookie(); - return await fetch(); - } - return response; - } -} diff --git a/lib/session/library.dart b/lib/session/library.dart deleted file mode 100644 index 80115242b..000000000 --- a/lib/session/library.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:dio/dio.dart'; -import 'package:sit/credentials/init.dart'; - -import 'package:sit/school/library/init.dart'; - -class LibrarySession { - final Dio dio; - - const LibrarySession({required this.dio}); - - Future request( - String url, { - Map? para, - data, - Options? options, - ProgressCallback? onSendProgress, - ProgressCallback? onReceiveProgress, - }) async { - Future fetch() { - return dio.request( - url, - queryParameters: para, - data: data, - options: options, - ); - } - - final response = await fetch(); - final resData = response.data; - if (resData is String) { - // renew login - final credentials = CredentialsInit.storage.libraryCredentials; - if (credentials != null) { - if (resData.contains("/opac/reader/doLogin")) { - await LibraryInit.auth.login(credentials); - return await fetch(); - } - } - } - return response; - } -} diff --git a/lib/session/sso.dart b/lib/session/sso.dart deleted file mode 100644 index f8d4c078a..000000000 --- a/lib/session/sso.dart +++ /dev/null @@ -1,405 +0,0 @@ -import 'dart:math'; - -import 'package:beautiful_soup_dart/beautiful_soup.dart'; -import 'package:collection/collection.dart'; -import 'package:cookie_jar/cookie_jar.dart'; -import 'package:dio/dio.dart'; -import 'package:flutter/foundation.dart' hide Key; -import 'package:sit/credentials/entity/credential.dart'; -import 'package:sit/credentials/error.dart'; -import 'package:sit/credentials/init.dart'; -import 'package:sit/init.dart'; - -import 'package:sit/route.dart'; -import 'package:sit/session/auth.dart'; -import 'package:sit/session/widgets/scope.dart'; -import 'package:sit/utils/error.dart'; -import 'package:synchronized/synchronized.dart'; -import 'package:encrypt/encrypt.dart'; - -import '../utils/dio.dart'; - -class LoginCaptchaCancelledException implements Exception { - const LoginCaptchaCancelledException(); -} - -class OaCredentialsRequiredException implements Exception { - final String url; - - const OaCredentialsRequiredException({required this.url}); - - @override - String toString() { - return "OaCredentialsRequiredException: $url"; - } -} - -const _neededHeaders = { - "Accept-Encoding": "gzip, deflate, br", - 'Origin': 'https://authserver.sit.edu.cn', - "Upgrade-Insecure-Requests": "1", - "Sec-Fetch-Dest": "document", - "Sec-Fetch-Mode": "navigate", - "Sec-Fetch-Site": "same-origin", - "Sec-Fetch-User": "?1", - "Referer": "https://authserver.sit.edu.cn/authserver/login", -}; - -/// Single Sign-On -class SsoSession { - static const String _authServerUrl = 'https://authserver.sit.edu.cn/authserver'; - static const String _loginUrl = '$_authServerUrl/login'; - static const String _needCaptchaUrl = '$_authServerUrl/needCaptcha.html'; - static const String _captchaUrl = '$_authServerUrl/captcha.html'; - static const String _loginSuccessUrl = 'https://authserver.sit.edu.cn/authserver/index.do'; - - final Dio dio; - final CookieJar cookieJar; - - /// Session错误拦截器 - final void Function(Object error, StackTrace stackTrace)? onError; - - /// 手动验证码 - final Future Function(Uint8List imageBytes) inputCaptcha; - - /// Lock it to prevent simultaneous login. - final loginLock = Lock(); - - SsoSession({ - required this.dio, - required this.cookieJar, - required this.inputCaptcha, - this.onError, - }); - - Future checkConnectivity({ - String url = 'http://jwxt.sit.edu.cn/', - }) async { - try { - await _dioRequest( - url, - options: Options( - method: "GET", - contentType: Headers.formUrlEncodedContentType, - followRedirects: false, - validateStatus: (status) => status! < 400, - ), - ); - return true; - } catch (e) { - return false; - } - } - - /// 判断该请求是否为登录页 - bool isLoginPage(Response response) { - return response.realUri.toString().contains(_loginUrl); - } - - /// - User try to log in actively on a login page. - Future loginLocked(Credentials credentials) async { - return await loginLock.synchronized(() async { - try { - final autoCaptcha = await _login( - credentials: credentials, - inputCaptcha: (captchaImage) => AuthSession.recognizeOaCaptcha(captchaImage), - ); - return autoCaptcha; - } catch (error, stackTrace) { - debugPrintError(error, stackTrace); - } - final manuallyCaptcha = await _login( - credentials: credentials, - inputCaptcha: inputCaptcha, - ); - return manuallyCaptcha; - }); - } - - Future _dioRequest( - String url, { - Map? queryParameters, - dynamic data, - Options? options, - ProgressCallback? onSendProgress, - ProgressCallback? onReceiveProgress, - }) async { - try { - return await _request( - url, - queryParameters: queryParameters, - data: data, - options: options, - ); - } catch (error, stackTrace) { - onError?.call(error, stackTrace); - rethrow; - } - } - - Future _request( - String url, { - Map? queryParameters, - dynamic data, - Options? options, - ProgressCallback? onSendProgress, - ProgressCallback? onReceiveProgress, - }) async { - options ??= Options(); - - /// 正常地请求 - Future requestNormally() async { - final response = await dio.request( - url, - queryParameters: queryParameters, - options: options?.copyWith( - followRedirects: false, - validateStatus: (status) { - return status! < 400; - }, - ), - data: data, - onSendProgress: onSendProgress, - onReceiveProgress: onReceiveProgress, - ); - // 处理重定向 - return await processRedirect(dio, response, headers: _neededHeaders); - } - - // 第一次先正常请求 - final firstResponse = await requestNormally(); - - // 如果跳转登录页,那就先登录 - if (isLoginPage(firstResponse)) { - final credentials = CredentialsInit.storage.oaCredentials; - if (credentials == null) { - throw OaCredentialsRequiredException(url: url); - } - await loginLocked(credentials); - return await requestNormally(); - } else { - return firstResponse; - } - } - - void _setOnline(bool isOnline) { - final ctx = $Key.currentContext; - if (ctx != null && ctx.mounted) { - OaOnlineManagerState.of(ctx).isOnline = isOnline; - } - } - - Future getJSessionId() async { - final cookies = await Init.cookieJar.loadForRequest(Uri.parse(_authServerUrl)); - return cookies.firstWhereOrNull((cookie) => cookie.name == "JSESSIONID"); - } - - Future _login({ - required Credentials credentials, - required Future Function(Uint8List imageBytes) inputCaptcha, - }) async { - debugPrint('${credentials.account} logging in'); - debugPrint('UA: ${dio.options.headers['User-Agent']}'); - // 在 OA 登录时, 服务端会记录同一 cookie 用户登录次数和输入错误次数, - // 所以需要在登录前清除所有 cookie, 避免用户重试时出错. - await cookieJar.delete(Uri.parse(_authServerUrl)); - final Response response; - try { - // 首先获取AuthServer首页 - final html = await _getAuthServerHtml(); - var captcha = ''; - if (await isCaptchaRequired(credentials.account)) { - final captchaImage = await getCaptcha(); - final c = await inputCaptcha(captchaImage); - if (c != null) { - captcha = c; - debugPrint("Captcha entered is $captcha"); - } else { - throw const LoginCaptchaCancelledException(); - } - } - // 获取casTicket - final casTicket = _getCasTicketFromAuthHtml(html); - // 获取salt - final salt = _getSaltFromAuthHtml(html); - // 加密密码 - final hashedPwd = hashPassword(salt, credentials.password); - // 登录系统,获得cookie - response = await _postLoginRequest(credentials.account, hashedPwd, captcha, casTicket); - } catch (e) { - _setOnline(false); - rethrow; - } - final page = BeautifulSoup(response.data); - - final emptyPage = BeautifulSoup(''); - // For desktop - final authError = (page.find('span', id: 'msg', class_: 'auth_error') ?? emptyPage).text.trim(); - // For mobile - final mobileError = (page.find('span', id: 'errorMsg') ?? emptyPage).text.trim(); - if (authError.isNotEmpty || mobileError.isNotEmpty) { - final errorMessage = authError + mobileError; - final type = parseInvalidType(errorMessage); - _setOnline(false); - throw CredentialsException(message: errorMessage, type: type); - } - - if (response.realUri.toString() != _loginSuccessUrl) { - debugPrint('Unknown auth error at "${response.realUri}"'); - _setOnline(false); - throw Exception(response.data.toString()); - } - debugPrint('${credentials.account} logged in'); - CredentialsInit.storage.oaLastAuthTime = DateTime.now(); - _setOnline(true); - return response; - } - - static CredentialsErrorType parseInvalidType(String errorMessage) { - if (errorMessage.contains("验证码")) { - return CredentialsErrorType.captcha; - } else if (errorMessage.contains("冻结")) { - return CredentialsErrorType.frozen; - } - return CredentialsErrorType.accountPassword; - } - - /// 提取认证页面中的加密盐 - String _getSaltFromAuthHtml(String htmlText) { - final a = RegExp(r'var pwdDefaultEncryptSalt = "(.*?)";'); - final matchResult = a.firstMatch(htmlText)!.group(0)!; - final salt = matchResult.substring(29, matchResult.length - 2); - debugPrint('Salt: $salt'); - return salt; - } - - /// 提取认证页面中的Cas Ticket - String _getCasTicketFromAuthHtml(String htmlText) { - final a = RegExp(r' _getAuthServerHtml() async { - final response = await dio.get( - _loginUrl, - options: Options(headers: Map.from(_neededHeaders)..remove('Referer')), - ); - return response.data; - } - - /// 判断是否需要验证码 - Future isCaptchaRequired(String username) async { - final response = await dio.get( - _needCaptchaUrl, - queryParameters: { - 'username': username, - 'pwdEncrypt2': 'pwdEncryptSalt', - }, - options: Options(headers: _neededHeaders), - ); - final needCaptcha = response.data == 'true'; - debugPrint('Account: $username, Captcha required: $needCaptcha'); - return needCaptcha; - } - - /// 获取验证码 - Future getCaptcha() async { - final response = await dio.get( - _captchaUrl, - options: Options( - responseType: ResponseType.bytes, - headers: _neededHeaders, - ), - ); - Uint8List captchaData = response.data; - return captchaData; - } - - /// 登录统一认证平台 - Future _postLoginRequest(String username, String hashedPassword, String captcha, String casTicket) async { - // 登录系统 - final res = await dio.post(_loginUrl, - data: { - 'username': username, - 'password': hashedPassword, - 'captchaResponse': captcha, - 'lt': casTicket, - 'dllt': 'userNamePasswordLogin', - 'execution': 'e1s1', - '_eventId': 'submit', - 'rmShown': '1', - }, - options: Options( - contentType: Headers.formUrlEncodedContentType, - followRedirects: false, - validateStatus: (status) { - return status! < 400; - }, - headers: _neededHeaders, - )); - // 处理重定向 - return await processRedirect(dio, res, headers: _neededHeaders); - } - - Future request( - String url, { - Map? para, - data, - Options? options, - ProgressCallback? onSendProgress, - ProgressCallback? onReceiveProgress, - }) async { - Response response = await _dioRequest( - url, - queryParameters: para, - data: data, - options: options, - onSendProgress: onSendProgress, - onReceiveProgress: onReceiveProgress, - ); - return response; - } -} - -String hashPassword(String salt, String password) { - var iv = rds(16); - var encrypt = SsoEncryption(salt, iv); - return encrypt.aesEncrypt(rds(64) + password); -} - -String rds(int num) { - var chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678'; - return List.generate( - num, - (index) => chars[Random.secure().nextInt(chars.length)], - ).join(); -} - -class SsoEncryption { - Key? _key; - IV? _iv; - - SsoEncryption(String key, String iv) { - _key = Key.fromUtf8(key); - _iv = IV.fromUtf8(iv); - } - - String aesEncrypt(String content) { - return Encrypter( - AES( - _key!, - mode: AESMode.cbc, - padding: 'PKCS7', - ), - ) - .encrypt( - content, - iv: _iv, - ) - .base64; - } -} diff --git a/lib/session/widgets/scope.dart b/lib/session/widgets/scope.dart deleted file mode 100644 index 5a45b5a1a..000000000 --- a/lib/session/widgets/scope.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'package:flutter/widgets.dart'; - -class OaOnlineManager extends StatefulWidget { - final Widget child; - - const OaOnlineManager({ - super.key, - required this.child, - }); - - @override - State createState() => OaOnlineManagerState(); -} - -class OaOnlineManagerState extends State { - bool _isOnline = false; - - @override - Widget build(BuildContext context) { - return OaOnlineScope( - isOnline: _isOnline, - child: widget.child, - ); - } - - static OaOnlineManagerState of(BuildContext context) { - final OaOnlineManagerState? result = context.findAncestorStateOfType(); - assert(result != null, 'No OaOnlineScope found in context'); - return result!; - } - - bool get isOnline => _isOnline; - - set isOnline(bool newV) { - if (_isOnline != newV) { - setState(() { - _isOnline = newV; - }); - } - } -} - -class OaOnlineScope extends InheritedWidget { - final bool isOnline; - - const OaOnlineScope({ - super.key, - required this.isOnline, - required super.child, - }); - - static OaOnlineScope of(BuildContext context) { - final OaOnlineScope? result = context.dependOnInheritedWidgetOfExactType(); - assert(result != null, 'No OaOnlineScope found in context'); - return result!; - } - - @override - bool updateShouldNotify(OaOnlineScope oldWidget) { - return isOnline != oldWidget.isOnline; - } -} diff --git a/lib/session/ywb.dart b/lib/session/ywb.dart deleted file mode 100644 index f7334c7f4..000000000 --- a/lib/session/ywb.dart +++ /dev/null @@ -1,110 +0,0 @@ -import 'dart:convert'; -import 'package:crypto/crypto.dart'; -import 'package:dio/dio.dart'; -import 'package:sit/credentials/init.dart'; -import 'package:sit/session/sso.dart'; - -/// 应网办 official website -const _ywbUrl = "https://ywb.sit.edu.cn/v1"; - -class YwbCredentialsException implements Exception { - final String message; - - const YwbCredentialsException({ - required this.message, - }); - - @override - String toString() { - return "YwbCredentialsException: $message"; - } -} - -class YwbSession { - bool isLogin = false; - String? username; - String? jwtToken; - final Dio dio; - - YwbSession({ - required this.dio, - }); - - Future _login({ - required String username, - required String password, - }) async { - final response = await dio.post( - "https://xgfy.sit.edu.cn/unifri-flow/login", - data: { - 'account': username, - 'userPassword': password, - 'remember': 'true', - }, - options: Options( - contentType: Headers.jsonContentType, - ), - ); - final resData = response.data as Map; - final int code = resData['code']; - - if (code != 0) { - final String errMessage = resData['msg']; - throw YwbCredentialsException(message: '($code) $errMessage'); - } - jwtToken = resData['data']['authorization']; - this.username = username; - isLogin = true; - } - - /// 获取当前以毫秒为单位的时间戳. - static String _getTimestamp() => DateTime.now().millisecondsSinceEpoch.toString(); - - /// 为时间戳生成签名. 此方案是联鹏习惯的反爬方式. - static String _sign(String ts) { - final content = const Utf8Encoder().convert('unifri.com$ts'); - return md5.convert(content).toString(); - } - - Future request( - String url, { - Map? para, - dynamic data, - Options? options, - ProgressCallback? onSendProgress, - ProgressCallback? onReceiveProgress, - }) async { - if (!isLogin) { - final credentials = CredentialsInit.storage.oaCredentials; - if (credentials == null) throw OaCredentialsRequiredException(url: url); - await _login( - username: credentials.account, - password: credentials.password, - ); - } - - Options newOptions = options ?? Options(); - - // Make default options. - final String ts = _getTimestamp(); - final String sign = _sign(ts); - final Map newHeaders = { - 'timestamp': ts, - 'signature': sign, - 'Authorization': jwtToken, - }; - - newOptions.headers == null ? newOptions.headers = newHeaders : newOptions.headers?.addAll(newHeaders); - newOptions.method = options?.method; - - final response = await dio.request( - url, - queryParameters: para, - data: data, - options: newOptions, - onSendProgress: onSendProgress, - onReceiveProgress: onReceiveProgress, - ); - return response; - } -} diff --git a/lib/settings/i18n.dart b/lib/settings/i18n.dart deleted file mode 100644 index a38169b5a..000000000 --- a/lib/settings/i18n.dart +++ /dev/null @@ -1,219 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:sit/credentials/i18n.dart'; -import 'package:sit/l10n/common.dart'; - -const i18n = _I18n(); - -class _I18n with CommonI18nMixin { - const _I18n(); - - final credentials = const _Credentials(); - final proxy = const _Proxy(); - final dev = const _DevOptions(); - final timetable = const _Timetable(); - final school = const _School(); - final life = const _Life(); - final about = const _About(); - - static const ns = "settings"; - - String get title => "$ns.title".tr(); - - String get language => "$ns.language".tr(); - - String get version => "$ns.version".tr(); - - String get themeColor => "$ns.themeColor".tr(); - - String get fromSystem => "$ns.fromSystem".tr(); - - String get themeModeTitle => "$ns.themeMode.title".tr(); - - String get clearCacheTitle => "$ns.clearCache.title".tr(); - - String get clearCacheDesc => "$ns.clearCache.desc".tr(); - - String get clearCacheRequest => "$ns.clearCache.request".tr(); - - String get wipeDataTitle => "$ns.wipeData.title".tr(); - - String get wipeDataDesc => "$ns.wipeData.desc".tr(); - - String get wipeDataRequest => "$ns.wipeData.request".tr(); - - String get wipeDataRequestDesc => "$ns.wipeData.requestDesc".tr(); -} - -class _Proxy { - const _Proxy(); - - static const ns = "${_I18n.ns}.proxy"; - - String get title => "$ns.title".tr(); - - String get desc => "$ns.desc".tr(); - - String get enableProxy => "$ns.enableProxy.title".tr(); - - String get enableProxyDesc => "$ns.enableProxy.desc".tr(); - - String get proxyMode => "$ns.proxyMode.title".tr(); - - String get shareQrCode => "$ns.shareQrCode.title".tr(); - - String get shareQrCodeDesc => "$ns.shareQrCode.desc".tr(); - - String get protocol => "$ns.protocol".tr(); - - String get hostname => "$ns.hostname".tr(); - - String get port => "$ns.port".tr(); - - String get authentication => "$ns.authentication".tr(); - - String get username => "$ns.username".tr(); - - String get password => "$ns.password".tr(); - - String get invalidProxyFormatTip => "$ns.invalidProxyFormatTip".tr(); - - String get proxyChangedTip => "$ns.proxyChangedTip".tr(); -} - -class _Timetable { - const _Timetable(); - - static const ns = "${_I18n.ns}.timetable"; - - String get title => "$ns.title".tr(); - - String get autoUseImported => "$ns.autoUseImported.title".tr(); - - String get autoUseImportedDesc => "$ns.autoUseImported.desc".tr(); - - String get palette => "$ns.palette.title".tr(); - - String get paletteDesc => "$ns.palette.desc".tr(); - - String get cellStyle => "$ns.cellStyle.title".tr(); - - String get cellStyleDesc => "$ns.cellStyle.desc".tr(); - - String get background => "$ns.background.title".tr(); - - String get backgroundDesc => "$ns.background.desc".tr(); -} - -class _School { - const _School(); - - static const ns = "${_I18n.ns}.school"; - final class2nd = const _Class2nd(); - final examResult = const _ExamResult(); - - String get title => "$ns.title".tr(); -} - -class _Class2nd { - static const ns = "${_School.ns}.class2nd"; - - const _Class2nd(); - - String get autoRefresh => "$ns.autoRefresh.title".tr(); - - String get autoRefreshDesc => "$ns.autoRefresh.desc".tr(); -} - -class _ExamResult { - static const ns = "${_School.ns}.examResult"; - - const _ExamResult(); - - String get appCardShowResultDetails => "$ns.appCardShowResultDetails.title".tr(); - - String get appCardShowResultDetailsDesc => "$ns.appCardShowResultDetails.desc".tr(); -} - -class _Life { - const _Life(); - - final electricity = const _Electricity(); - final expense = const _Expense(); - static const ns = "${_I18n.ns}.life"; - - String get title => "$ns.title".tr(); -} - -class _About { - const _About(); - - static const ns = "${_I18n.ns}.about"; - - String get title => "$ns.title".tr(); -} - -class _Electricity { - static const ns = "${_Life.ns}.electricity"; - - const _Electricity(); - - String get autoRefresh => "$ns.autoRefresh.title".tr(); - - String get autoRefreshDesc => "$ns.autoRefresh.desc".tr(); -} - -class _Expense { - static const ns = "${_Life.ns}.expenseRecords"; - - const _Expense(); - - String get autoRefresh => "$ns.autoRefresh.title".tr(); - - String get autoRefreshDesc => "$ns.autoRefresh.desc".tr(); -} - -class _DevOptions { - const _DevOptions(); - - final storage = const _Storage(); - - static const ns = "${_I18n.ns}.dev"; - - String get title => "$ns.title".tr(); - - String get devMode => "$ns.devMode.title".tr(); - - String get reload => "$ns.reload.title".tr(); - - String get reloadDesc => "$ns.reload.desc".tr(); - - String get localStorage => "$ns.localStorage.title".tr(); - - String get localStorageDesc => "$ns.localStorage.desc".tr(); -} - -class _Storage with CommonI18nMixin { - const _Storage(); - - static const ns = "${_DevOptions.ns}.localStorage"; - - String get title => "$ns.title".tr(); - - String get selectBoxTip => "$ns.selectBoxTip".tr(); - - String get clearBoxDesc => "$ns.clearBoxDesc".tr(); - - String get deleteItemDesc => "$ns.deleteItemDesc".tr(); - - String get emptyValueDesc => "$ns.emptyValueDesc".tr(); -} - -class _Credentials extends CredentialsI18n { - static const ns = "${_I18n.ns}.credentials"; - - const _Credentials(); - - String get testLoginOa => "$ns.testLoginOa.title".tr(); - - String get testLoginOaDesc => "$ns.testLoginOa.desc".tr(); -} diff --git a/lib/settings/meta.dart b/lib/settings/meta.dart deleted file mode 100644 index ae30a5c7f..000000000 --- a/lib/settings/meta.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:hive_flutter/hive_flutter.dart'; - -class _K { - static const lastLaunchTime = "/lastLaunchTime"; - static const thisLaunchTime = "/thisLaunchTime"; -} - -// ignore: non_constant_identifier_names -late MetaImpl Meta; - -class MetaImpl { - final Box box; - - const MetaImpl(this.box); - - DateTime? get lastLaunchTime => box.get(_K.lastLaunchTime); - - set lastLaunchTime(DateTime? newV) => box.put(_K.lastLaunchTime, newV); - - DateTime? get thisLaunchTime => box.get(_K.thisLaunchTime); - - set thisLaunchTime(DateTime? newV) => box.put(_K.thisLaunchTime, newV); -} diff --git a/lib/settings/page/about.dart b/lib/settings/page/about.dart deleted file mode 100644 index bc5a8f3af..000000000 --- a/lib/settings/page/about.dart +++ /dev/null @@ -1,105 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:sit/design/adaptive/dialog.dart'; -import 'package:sit/r.dart'; -import 'package:sit/settings/settings.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:sit/entity/version.dart'; -import 'package:unicons/unicons.dart'; -import '../i18n.dart'; - -class AboutSettingsPage extends StatefulWidget { - const AboutSettingsPage({ - super.key, - }); - - @override - State createState() => _AboutSettingsPageState(); -} - -class _AboutSettingsPageState extends State { - @override - Widget build(BuildContext context) { - return Scaffold( - body: CustomScrollView( - physics: const RangeMaintainingScrollPhysics(), - slivers: [ - SliverAppBar.large( - pinned: true, - snap: false, - floating: false, - title: i18n.about.title.text(), - ), - SliverList.list( - children: [ - const VersionTile(), - AboutListTile( - applicationName: "SIT Life", - applicationVersion: R.currentVersion.full.toString(), - applicationLegalese: "2023 SIT Life all rights reserved.", - ), - ], - ), - ], - ), - ); - } -} - -class VersionTile extends StatefulWidget { - const VersionTile({super.key}); - - @override - State createState() => _VersionTileState(); -} - -class _VersionTileState extends State { - int clickCount = 0; - final $isDeveloperMode = Settings.listenIsDeveloperMode(); - - @override - void initState() { - super.initState(); - $isDeveloperMode.addListener(refresh); - } - - @override - void dispose() { - $isDeveloperMode.removeListener(refresh); - super.dispose(); - } - - void refresh() { - setState(() {}); - } - - @override - Widget build(BuildContext context) { - final version = R.currentVersion; - return ListTile( - leading: switch (version.platform) { - AppPlatform.iOS || AppPlatform.macOS => const Icon(UniconsLine.apple), - AppPlatform.android => const Icon(Icons.android), - AppPlatform.linux => const Icon(UniconsLine.linux), - AppPlatform.windows => const Icon(UniconsLine.windows), - AppPlatform.web => const Icon(UniconsLine.browser), - AppPlatform.unknown => const Icon(Icons.device_unknown_outlined), - }, - title: i18n.version.text(), - subtitle: "${version.platform.name} ${version.full.toString()}".text(), - onTap: Settings.isDeveloperMode && clickCount <= 10 - ? null - : () async { - if (Settings.isDeveloperMode) return; - clickCount++; - if (clickCount >= 10) { - clickCount = 0; - Settings.isDeveloperMode = true; - // TODO: i18n - context.showSnackBar(content: const Text("Developer mode activated")); - await HapticFeedback.mediumImpact(); - } - }, - ); - } -} diff --git a/lib/settings/page/credentials.dart b/lib/settings/page/credentials.dart deleted file mode 100644 index 667e64c92..000000000 --- a/lib/settings/page/credentials.dart +++ /dev/null @@ -1,164 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:sit/credentials/entity/credential.dart'; -import 'package:sit/credentials/init.dart'; -import 'package:sit/credentials/widgets/oa_scope.dart'; -import 'package:sit/design/adaptive/dialog.dart'; -import 'package:sit/design/adaptive/editor.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:sit/init.dart'; -import 'package:sit/login/utils.dart'; -import '../i18n.dart'; - -const _changePasswordUrl = "https://authserver.sit.edu.cn/authserver/passwordChange.do"; - -class CredentialsPage extends StatefulWidget { - const CredentialsPage({super.key}); - - @override - State createState() => _CredentialsPageState(); -} - -class _CredentialsPageState extends State { - var showPassword = false; - - @override - Widget build(BuildContext context) { - return Scaffold( - body: CustomScrollView( - physics: const RangeMaintainingScrollPhysics(), - slivers: [ - SliverAppBar.large( - pinned: true, - snap: false, - floating: false, - title: i18n.credentials.oaAccount.text(), - ), - buildBody(), - ], - ), - ); - } - - Widget buildBody() { - final credentials = context.auth.credentials; - final all = []; - if (credentials != null) { - all.add((_) => buildAccount(credentials)); - all.add((_) => const Divider()); - all.add((_) => buildPassword(credentials)); - all.add((_) => TestLoginTile(credentials: credentials)); - } - return SliverList( - delegate: SliverChildBuilderDelegate( - childCount: all.length, - (ctx, index) { - return all[index](ctx); - }, - ), - ); - } - - Widget buildAccount(Credentials credential) { - return ListTile( - title: i18n.credentials.oaAccount.text(), - subtitle: credential.account.text(), - leading: const Icon(Icons.person_rounded), - trailing: const Icon(Icons.copy_rounded), - onTap: () async { - context.showSnackBar(content: i18n.copyTipOf(i18n.credentials.oaAccount).text()); - // Copy the student ID to clipboard - await Clipboard.setData(ClipboardData(text: credential.account)); - }, - ); - } - - Widget buildPassword(Credentials credential) { - return AnimatedSize( - duration: const Duration(milliseconds: 100), - child: ListTile( - title: i18n.credentials.savedOaPwd.text(), - subtitle: Text(!showPassword ? i18n.credentials.savedOaPwdDesc : credential.password), - leading: const Icon(Icons.password_rounded), - trailing: [ - IconButton( - icon: const Icon(Icons.edit), - onPressed: () async { - final newPwd = await Editor.showStringEditor( - context, - desc: i18n.credentials.savedOaPwd, - initial: credential.password, - ); - if (newPwd != credential.password) { - if (!mounted) return; - CredentialsInit.storage.oaCredentials = credential.copyWith(password: newPwd); - setState(() {}); - } - }, - ), - IconButton( - onPressed: () { - setState(() { - showPassword = !showPassword; - }); - }, - icon: showPassword ? const Icon(Icons.visibility) : const Icon(Icons.visibility_off)), - ].wrap(), - ), - ); - } -} - -enum _TestLoginState { - notStart, - loggingIn, - success, -} - -class TestLoginTile extends StatefulWidget { - final Credentials credentials; - - const TestLoginTile({ - super.key, - required this.credentials, - }); - - @override - State createState() => _TestLoginTileState(); -} - -class _TestLoginTileState extends State { - var loggingState = _TestLoginState.notStart; - - @override - Widget build(BuildContext context) { - return ListTile( - enabled: loggingState != _TestLoginState.loggingIn, - title: i18n.credentials.testLoginOa.text(), - subtitle: i18n.credentials.testLoginOaDesc.text(), - leading: const Icon(Icons.login), - trailing: Padding( - padding: const EdgeInsets.all(8), - child: switch (loggingState) { - _TestLoginState.loggingIn => const CircularProgressIndicator.adaptive(), - _TestLoginState.success => const Icon(Icons.check, color: Colors.green), - _ => null, - }, - ), - onTap: loggingState == _TestLoginState.loggingIn - ? null - : () async { - setState(() => loggingState = _TestLoginState.loggingIn); - try { - await Init.ssoSession.loginLocked(widget.credentials); - if (!mounted) return; - setState(() => loggingState = _TestLoginState.success); - } on Exception catch (error, stackTrace) { - setState(() => loggingState = _TestLoginState.notStart); - if (!mounted) return; - await handleLoginException(context: context, error: error, stackTrace: stackTrace); - } - }, - ); - } -} diff --git a/lib/settings/page/developer.dart b/lib/settings/page/developer.dart deleted file mode 100644 index d2e685c89..000000000 --- a/lib/settings/page/developer.dart +++ /dev/null @@ -1,224 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:sit/credentials/entity/credential.dart'; -import 'package:sit/credentials/init.dart'; -import 'package:sit/credentials/utils.dart'; -import 'package:sit/design/adaptive/editor.dart'; -import 'package:sit/design/widgets/expansion_tile.dart'; -import 'package:sit/init.dart'; -import 'package:sit/login/aggregated.dart'; -import 'package:sit/login/utils.dart'; -import 'package:sit/settings/settings.dart'; -import 'package:sit/design/widgets/navigation.dart'; -import 'package:rettulf/rettulf.dart'; -import '../i18n.dart'; - -class DeveloperOptionsPage extends StatefulWidget { - const DeveloperOptionsPage({ - super.key, - }); - - @override - State createState() => _DeveloperOptionsPageState(); -} - -class _DeveloperOptionsPageState extends State { - @override - Widget build(BuildContext context) { - final oaCredentials = CredentialsInit.storage.oaCredentials; - return Scaffold( - body: CustomScrollView( - physics: const RangeMaintainingScrollPhysics(), - slivers: [ - SliverAppBar.large( - pinned: true, - snap: false, - floating: false, - title: i18n.dev.title.text(), - ), - SliverList( - delegate: SliverChildListDelegate([ - buildDevModeToggle(), - PageNavigationTile( - title: i18n.dev.localStorage.text(), - subtitle: i18n.dev.localStorageDesc.text(), - leading: const Icon(Icons.storage), - path: "/settings/developer/local-storage", - ), - buildReload(), - if (oaCredentials != null) - SwitchOaUserTile( - currentCredentials: oaCredentials, - ), - const DebugGoRouteTile(), - ]), - ), - ], - ), - ); - } - - Widget buildDevModeToggle() { - return StatefulBuilder( - builder: (ctx, setState) => ListTile( - title: i18n.dev.devMode.text(), - leading: const Icon(Icons.developer_mode_outlined), - trailing: Switch.adaptive( - value: Settings.isDeveloperMode, - onChanged: (newV) { - setState(() { - Settings.isDeveloperMode = newV; - }); - }, - ), - ), - ); - } - - Widget buildReload() { - return ListTile( - title: i18n.dev.reload.text(), - subtitle: i18n.dev.reloadDesc.text(), - leading: const Icon(Icons.refresh_rounded), - onTap: () async { - await Init.initNetwork(); - await Init.initModules(); - final engine = WidgetsFlutterBinding.ensureInitialized(); - engine.performReassemble(); - if (!mounted) return; - context.navigator.pop(); - }, - ); - } -} - -class DebugGoRouteTile extends StatefulWidget { - const DebugGoRouteTile({super.key}); - - @override - State createState() => _DebugGoRouteTileState(); -} - -class _DebugGoRouteTileState extends State { - final $route = TextEditingController(); - - @override - void dispose() { - $route.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - // TODO: i18n - return ListTile( - isThreeLine: true, - leading: const Icon(Icons.route_outlined), - title: "Go route".text(), - subtitle: TextField( - controller: $route, - decoration: const InputDecoration( - hintText: "/anywhere", - ), - ), - trailing: [ - $route >> - (ctx, route) => IconButton( - onPressed: route.text.isEmpty - ? null - : () { - context.push(route.text); - }, - icon: const Icon(Icons.arrow_forward)) - ].row(mas: MainAxisSize.min), - ); - } -} - -class SwitchOaUserTile extends StatefulWidget { - final Credentials currentCredentials; - - const SwitchOaUserTile({ - super.key, - required this.currentCredentials, - }); - - @override - State createState() => _SwitchOaUserTileState(); -} - -class _SwitchOaUserTileState extends State { - bool isLoggingIn = false; - - @override - Widget build(BuildContext context) { - final credentialsList = Settings.getSavedOaCredentialsList() ?? []; - if (credentialsList.none((c) => c.account == widget.currentCredentials.account)) { - credentialsList.add(widget.currentCredentials); - } - return AnimatedExpansionTile( - title: "Switch OA user".text(), - subtitle: "Without logging out".text(), - initiallyExpanded: true, - leading: const Icon(Icons.swap_horiz), - trailing: isLoggingIn - ? const Padding( - padding: EdgeInsets.all(8), - child: CircularProgressIndicator.adaptive(), - ) - : null, - children: [ - ...credentialsList.map(buildCredentialsHistoryTile), - buildLoginNewTile(), - ], - ); - } - - Widget buildCredentialsHistoryTile(Credentials credentials) { - final isCurrent = credentials == widget.currentCredentials; - return ListTile( - leading: const Icon(Icons.account_circle), - title: credentials.account.text(), - subtitle: isCurrent ? "Current user".text() : estimateOaUserType(credentials.account)?.l10n().text(), - trailing: const Icon(Icons.login).padAll(8), - enabled: !isCurrent, - onTap: () async { - await loginWith(credentials); - }, - ).padH(12); - } - - Widget buildLoginNewTile() { - return ListTile( - leading: const Icon(Icons.add), - title: "New account".text(), - onTap: () async { - final credentials = await await Editor.showAnyEditor( - context, - Credentials(account: "", password: ""), - ); - if (credentials == null) return; - await loginWith(credentials); - }, - ).padH(12); - } - - Future loginWith(Credentials credentials) async { - setState(() => isLoggingIn = true); - try { - await Init.cookieJar.deleteAll(); - await LoginAggregated.login(credentials); - final former = Settings.getSavedOaCredentialsList() ?? []; - former.add(credentials); - await Settings.setSavedOaCredentialsList(former); - if (!mounted) return; - setState(() => isLoggingIn = false); - context.go("/"); - } on Exception catch (error, stackTrace) { - if (!mounted) return; - setState(() => isLoggingIn = false); - await handleLoginException(context: context, error: error, stackTrace: stackTrace); - } - } -} diff --git a/lib/settings/page/index.dart b/lib/settings/page/index.dart deleted file mode 100644 index ad8f9bcf9..000000000 --- a/lib/settings/page/index.dart +++ /dev/null @@ -1,330 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:go_router/go_router.dart'; -import 'package:sit/credentials/entity/login_status.dart'; -import 'package:sit/credentials/widgets/oa_scope.dart'; -import 'package:sit/design/adaptive/dialog.dart'; -import 'package:sit/design/widgets/card.dart'; -import 'package:sit/network/widgets/entry.dart'; -import 'package:sit/storage/hive/init.dart'; -import 'package:sit/init.dart'; -import 'package:sit/l10n/extension.dart'; -import 'package:sit/session/widgets/scope.dart'; -import 'package:sit/settings/settings.dart'; -import 'package:sit/school/widgets/campus.dart'; -import 'package:sit/utils/color.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:system_theme/system_theme.dart'; -import 'package:flex_color_picker/flex_color_picker.dart'; -import 'package:locale_names/locale_names.dart'; - -import '../i18n.dart'; -import '../../design/widgets/navigation.dart'; - -class SettingsPage extends StatefulWidget { - const SettingsPage({super.key}); - - @override - State createState() => _SettingsPageState(); -} - -class _SettingsPageState extends State { - final $isDeveloperMode = Settings.listenIsDeveloperMode(); - - @override - void initState() { - super.initState(); - $isDeveloperMode.addListener(refresh); - } - - @override - void dispose() { - $isDeveloperMode.removeListener(refresh); - super.dispose(); - } - - void refresh() { - setState(() {}); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - body: CustomScrollView( - physics: const RangeMaintainingScrollPhysics(), - slivers: [ - SliverAppBar.large( - pinned: true, - snap: false, - floating: false, - title: i18n.title.text(), - ), - SliverList.list( - children: buildEntries(), - ), - ], - ), - ); - } - - List buildEntries() { - final all = []; - final auth = context.auth; - if (auth.loginStatus != LoginStatus.never) { - all.add(const CampusSelector().padSymmetric(h: 8)); - } - final credential = auth.credentials; - if (credential != null) { - all.add(PageNavigationTile( - title: i18n.credentials.oaAccount.text(), - subtitle: credential.account.text(), - leading: const Icon(Icons.person_rounded), - path: "/settings/credentials", - )); - } else { - // TODO: i18n - all.add(ListTile( - title: "Login".text(), - subtitle: "Please login".text(), - leading: const Icon(Icons.person_rounded), - onTap: () { - context.go("/login"); - }, - )); - } - all.add(const Divider()); - - all.add(buildLanguageSelector()); - all.add(buildThemeMode()); - all.add(buildThemeColorPicker()); - all.add(const Divider()); - - if (auth.loginStatus != LoginStatus.never) { - all.add(PageNavigationTile( - title: i18n.timetable.title.text(), - leading: const Icon(Icons.calendar_month_outlined), - path: "/settings/timetable", - )); - if (!kIsWeb) { - all.add(PageNavigationTile( - title: i18n.school.title.text(), - leading: const Icon(Icons.school_outlined), - path: "/settings/school", - )); - all.add(PageNavigationTile( - title: i18n.life.title.text(), - leading: const Icon(Icons.spa_outlined), - path: "/settings/life", - )); - } - all.add(const Divider()); - } - if (Settings.isDeveloperMode) { - all.add(PageNavigationTile( - title: i18n.dev.title.text(), - leading: const Icon(Icons.developer_mode_outlined), - path: "/settings/developer", - )); - } - if (!kIsWeb) { - all.add(PageNavigationTile( - title: i18n.proxy.title.text(), - subtitle: i18n.proxy.desc.text(), - leading: const Icon(Icons.vpn_key), - path: "/settings/proxy", - )); - all.add(const NetworkToolEntryTile()); - } - if (auth.loginStatus != LoginStatus.never) { - all.add(const ClearCacheTile()); - } - all.add(const WipeDataTile()); - all.add(PageNavigationTile( - title: i18n.about.title.text(), - leading: const Icon(Icons.info), - path: "/settings/about", - )); - return all; - } - - Widget buildThemeMode() { - return Settings.theme.listenThemeMode() >> - (ctx, _) => ListTile( - leading: switch (Settings.theme.themeMode) { - ThemeMode.dark => const Icon(Icons.dark_mode), - ThemeMode.light => const Icon(Icons.light_mode), - ThemeMode.system => const Icon(Icons.brightness_auto), - }, - title: i18n.themeModeTitle.text(), - subtitle: ThemeMode.values - .map((mode) => ChoiceChip( - label: mode.l10n().text(), - selected: Settings.theme.themeMode == mode, - onSelected: (value) async { - Settings.theme.themeMode = mode; - await HapticFeedback.mediumImpact(); - }, - )) - .toList() - .wrap(spacing: 4), - ); - } - - Widget buildLanguageSelector() { - final curLocale = context.locale; - return ListTile( - leading: const Icon(Icons.translate_rounded), - title: i18n.language.text(), - subtitle: curLocale.nativeDisplayLanguageScript.text(), - trailing: DropdownMenu( - initialSelection: curLocale, - onSelected: (Locale? locale) async { - if (locale == null) return; - await HapticFeedback.mediumImpact(); - if (!mounted) return; - await context.setLocale(locale); - final engine = WidgetsFlutterBinding.ensureInitialized(); - engine.performReassemble(); - }, - dropdownMenuEntries: R.supportedLocales - .map>( - (locale) => DropdownMenuEntry( - value: locale, - label: locale.nativeDisplayLanguageScript, - ), - ) - .toList(), - ), - ); - } - - Widget buildThemeColorPicker() { - // TODO: Better UI - final selected = Settings.theme.themeColor ?? SystemTheme.accentColor.maybeAccent ?? context.colorScheme.primary; - final usingSystemDefault = supportsSystemAccentColor && Settings.theme.themeColor == null; - - Future selectNewThemeColor() async { - final newColor = await showColorPickerDialog( - context, - selected, - enableOpacity: true, - enableShadesSelection: true, - enableTonalPalette: true, - showColorCode: true, - pickersEnabled: const { - ColorPickerType.both: true, - ColorPickerType.primary: false, - ColorPickerType.accent: false, - ColorPickerType.custom: true, - ColorPickerType.wheel: true, - }, - ); - if (newColor != selected) { - await HapticFeedback.mediumImpact(); - Settings.theme.themeColor = newColor; - } - } - - return ListTile( - leading: const Icon(Icons.color_lens_outlined), - title: i18n.themeColor.text(), - subtitle: "#${selected.hexAlpha}".text(), - onTap: usingSystemDefault ? selectNewThemeColor : null, - trailing: usingSystemDefault - ? i18n.fromSystem.text(style: context.textTheme.bodyMedium) - : [ - IconButton( - onPressed: () { - Settings.theme.themeColor = null; - }, - icon: const Icon(Icons.delete), - ), - FilledCard( - color: selected, - clip: Clip.hardEdge, - child: InkWell( - onTap: selectNewThemeColor, - child: const SizedBox( - width: 32, - height: 32, - ), - ), - ), - ].wrap(), - ); - } -} - -class ClearCacheTile extends StatelessWidget { - const ClearCacheTile({super.key}); - - @override - Widget build(BuildContext context) { - return ListTile( - title: i18n.clearCacheTitle.text(), - subtitle: i18n.clearCacheDesc.text(), - leading: const Icon(Icons.folder_delete_outlined), - onTap: () { - _onClearCache(context); - }, - ); - } -} - -void _onClearCache(BuildContext context) async { - final confirm = await context.showRequest( - title: i18n.clearCacheTitle, - desc: i18n.clearCacheRequest, - yes: i18n.confirm, - no: i18n.notNow, - highlight: true, - serious: true, - ); - if (confirm == true) { - await HiveInit.clearCache(); - } -} - -class WipeDataTile extends StatelessWidget { - const WipeDataTile({super.key}); - - @override - Widget build(BuildContext context) { - return ListTile( - title: i18n.wipeDataTitle.text(), - subtitle: i18n.wipeDataDesc.text(), - leading: const Icon(Icons.delete_forever_rounded), - onTap: () { - _onWipeData(context); - }, - ); - } -} - -void _onWipeData(BuildContext context) async { - final confirm = await context.showRequest( - title: i18n.wipeDataRequest, - desc: i18n.wipeDataRequestDesc, - yes: i18n.confirm, - no: i18n.notNow, - highlight: true, - serious: true, - ); - if (confirm == true) { - await HiveInit.clear(); // 清除存储 - await Init.initNetwork(); - if (!context.mounted) return; - OaOnlineManagerState.of(context).isOnline = false; - _gotoLogin(context); - } -} - -void _gotoLogin(BuildContext context) { - final navigator = context.navigator; - while (navigator.canPop()) { - navigator.pop(); - } - context.go("/login"); -} diff --git a/lib/settings/page/life.dart b/lib/settings/page/life.dart deleted file mode 100644 index e9ba268c8..000000000 --- a/lib/settings/page/life.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:sit/settings/settings.dart'; -import 'package:rettulf/rettulf.dart'; -import '../i18n.dart'; - -class LifeSettingsPage extends StatefulWidget { - const LifeSettingsPage({ - super.key, - }); - - @override - State createState() => _LifeSettingsPageState(); -} - -class _LifeSettingsPageState extends State { - @override - Widget build(BuildContext context) { - return Scaffold( - body: CustomScrollView( - physics: const RangeMaintainingScrollPhysics(), - slivers: [ - SliverAppBar.large( - pinned: true, - snap: false, - floating: false, - title: i18n.life.title.text(), - ), - SliverList( - delegate: SliverChildListDelegate([ - buildElectricityAutoRefreshToggle(), - buildExpenseAutoRefreshToggle(), - ]), - ), - ], - ), - ); - } - - Widget buildElectricityAutoRefreshToggle() { - return StatefulBuilder( - builder: (ctx, setState) => ListTile( - title: i18n.life.electricity.autoRefresh.text(), - subtitle: i18n.life.electricity.autoRefreshDesc.text(), - leading: const Icon(Icons.refresh_outlined), - trailing: Switch.adaptive( - value: Settings.life.electricity.autoRefresh, - onChanged: (newV) { - setState(() { - Settings.life.electricity.autoRefresh = newV; - }); - }, - ), - ), - ); - } - - Widget buildExpenseAutoRefreshToggle() { - return StatefulBuilder( - builder: (ctx, setState) => ListTile( - title: i18n.life.expense.autoRefresh.text(), - subtitle: i18n.life.expense.autoRefreshDesc.text(), - leading: const Icon(Icons.refresh_outlined), - trailing: Switch.adaptive( - value: Settings.life.expense.autoRefresh, - onChanged: (newV) { - setState(() { - Settings.life.expense.autoRefresh = newV; - }); - }, - ), - ), - ); - } -} diff --git a/lib/settings/page/proxy.dart b/lib/settings/page/proxy.dart deleted file mode 100644 index e744c4a3b..000000000 --- a/lib/settings/page/proxy.dart +++ /dev/null @@ -1,485 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; -import 'package:go_router/go_router.dart'; -import 'package:sit/design/adaptive/dialog.dart'; -import 'package:sit/design/adaptive/editor.dart'; -import 'package:sit/design/adaptive/foundation.dart'; -import 'package:sit/design/widgets/list_tile.dart'; -import 'package:sit/network/checker.dart'; -import 'package:sit/qrcode/page/view.dart'; -import 'package:sit/qrcode/protocol.dart'; -import 'package:sit/settings/settings.dart'; -import 'package:rettulf/rettulf.dart'; -import '../i18n.dart'; - -class ProxySettingsPage extends StatefulWidget { - const ProxySettingsPage({ - super.key, - }); - - @override - State createState() => _ProxySettingsPageState(); -} - -class _ProxySettingsPageState extends State { - @override - Widget build(BuildContext context) { - return Scaffold( - body: CustomScrollView( - physics: const RangeMaintainingScrollPhysics(), - slivers: [ - SliverAppBar.large( - pinned: true, - snap: false, - floating: false, - title: i18n.proxy.title.text(), - ), - SliverList( - delegate: SliverChildListDelegate([ - buildEnableProxyToggle(), - buildProxyModeSwitcher(), - const Divider(), - buildProxyTypeTile( - ProxyType.http, - icon: const Icon(Icons.http), - ), - buildProxyTypeTile( - ProxyType.https, - icon: const Icon(Icons.https), - ), - if (Settings.isDeveloperMode) - buildProxyTypeTile( - ProxyType.all, - icon: const Icon(Icons.public), - ), - const Divider(), - const TestConnectionTile(), - const ProxyShareQrCodeTile(), - ]), - ), - ], - ), - ); - } - - Widget buildProxyTypeTile( - ProxyType type, { - required Widget icon, - }) { - return Settings.proxy.listenAnyChange(type: type) >> - (ctx) { - final profile = Settings.proxy.resolve(type); - return ListTile( - leading: icon, - title: type.l10n().text(), - subtitle: profile.enabled ? profile.address?.text() : null, - trailing: const Icon(Icons.open_in_new), - onTap: () async { - final profile = - await context.show$Sheet$((ctx) => ProxyProfileEditorPage(type: type)); - if (profile != null) { - Settings.proxy.setProfile(type, profile); - } - }, - ); - }; - } - - Widget buildEnableProxyToggle() { - return Settings.proxy.listenAnyEnabled() >> - (ctx) => _EnableProxyToggleTile( - enabled: Settings.proxy.anyEnabled, - onChanged: (newV) { - setState(() { - Settings.proxy.anyEnabled = newV; - }); - }, - ); - } - - Widget buildProxyModeSwitcher() { - return Settings.proxy.listenProxyMode() >> - (ctx) => _ProxyModeSwitcherTile( - proxyMode: Settings.proxy.getIntegratedProxyMode(), - onChanged: (value) { - setState(() { - Settings.proxy.setIntegratedProxyMode(value); - }); - }, - ); - } -} - -class ProxyShareQrCodeTile extends StatelessWidget { - const ProxyShareQrCodeTile({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return ListTile( - leading: const Icon(Icons.qr_code), - title: i18n.proxy.shareQrCode.text(), - subtitle: i18n.proxy.shareQrCodeDesc.text(), - trailing: const Icon(Icons.share).padAll(8), - onTap: () async { - final proxy = Settings.proxy; - final qrCodeData = const ProxyDeepLink().encode( - http: proxy.http.isDefaultAddress ? null : proxy.http.address, - https: proxy.https.isDefaultAddress ? null : proxy.https.address, - all: proxy.all.isDefaultAddress ? null : proxy.all.address, - ); - context.show$Sheet$( - (context) => QrCodePage( - title: i18n.proxy.title.text(), - data: qrCodeData.toString(), - ), - ); - }, - ); - } -} - -Future onProxyFromQrCode({ - required BuildContext context, - required Uri? http, - required Uri? https, - required Uri? all, -}) async { - if (http != null) { - Settings.proxy.resolve(ProxyType.http).address = http.toString(); - } - if (https != null) { - Settings.proxy.resolve(ProxyType.https).address = https.toString(); - } - if (http != null) { - Settings.proxy.resolve(ProxyType.all).address = all.toString(); - } - await HapticFeedback.mediumImpact(); - if (!context.mounted) return; - context.showSnackBar(content: i18n.proxy.proxyChangedTip.text()); - context.push("/settings/proxy"); -} - -class ProxyProfileEditorPage extends StatefulWidget { - final ProxyType type; - - const ProxyProfileEditorPage({ - super.key, - required this.type, - }); - - @override - State createState() => _ProxyProfileEditorPageState(); -} - -class _ProxyProfileEditorPageState extends State { - late final profile = Settings.proxy.resolve(widget.type); - late var uri = (profile.address == null ? null : Uri.tryParse(profile.address!)) ?? type.buildDefaultUri(); - late var enabled = profile.enabled; - late var globalMode = profile.proxyMode; - - ProxyType get type => widget.type; - - @override - void initState() { - super.initState(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - body: CustomScrollView( - slivers: [ - SliverAppBar( - title: widget.type.l10n().text(), - actions: [ - buildSaveAction(), - ], - ), - SliverList.list(children: [ - buildEnableProxyToggle(), - buildProxyModeSwitcher(), - buildProxyUrlTile(), - const Divider(), - buildProxyProtocolTile(), - buildProxyHostTile(), - buildProxyPortTile(), - buildProxyAuthTile(), - ]), - ], - ), - ); - } - - Widget buildSaveAction() { - return PlatformTextButton( - child: i18n.save.text(), - onPressed: () { - context.pop((address: uri.toString(), enabled: enabled, proxyMode: globalMode)); - }, - ); - } - - Widget buildProxyUrlTile() { - final uri = this.uri; - return DetailListTile( - leading: const Icon(Icons.link), - title: "URL", - subtitle: uri.toString(), - trailing: [ - if (!type.isDefaultUri(uri)) - IconButton( - onPressed: () { - setState(() { - this.uri = type.buildDefaultUri(); - }); - }, - icon: const Icon(Icons.delete), - ), - IconButton( - icon: const Icon(Icons.edit), - onPressed: () async { - final newFullProxy = await Editor.showStringEditor( - context, - desc: i18n.proxy.title, - initial: uri.toString(), - ); - if (newFullProxy == null) return; - final newUri = Uri.tryParse(newFullProxy.trim()); - if (newUri == null || !newUri.isAbsolute || !type.supportedProtocols.contains(newUri.scheme)) { - if (!mounted) return; - context.showTip( - title: i18n.error, - desc: i18n.proxy.invalidProxyFormatTip, - ok: i18n.close, - ); - return; - } - if (newUri != uri) { - setState(() { - this.uri = newUri; - }); - } - }, - ), - ].wrap(), - ); - } - - Widget buildProxyProtocolTile() { - final scheme = uri.scheme; - return ListTile( - isThreeLine: true, - leading: const Icon(Icons.https), - title: i18n.proxy.protocol.text(), - subtitle: type.supportedProtocols - .map((protocol) => ChoiceChip( - label: protocol.toUpperCase().text(), - selected: protocol == scheme, - onSelected: (value) { - setState(() { - uri = uri.replace( - scheme: protocol, - ); - }); - }, - )) - .toList() - .wrap(spacing: 4), - ); - } - - Widget buildProxyHostTile() { - final host = uri.host; - return DetailListTile( - leading: const Icon(Icons.link), - title: i18n.proxy.hostname, - subtitle: host, - trailing: IconButton( - icon: const Icon(Icons.edit), - onPressed: () async { - final newHostRaw = await Editor.showStringEditor( - context, - desc: i18n.proxy.hostname, - initial: host, - ); - if (newHostRaw == null) return; - final newHost = newHostRaw.trim(); - if (newHost != host) { - setState(() { - uri = uri.replace( - host: newHost, - ); - }); - } - }, - ), - ); - } - - Widget buildProxyPortTile() { - int port = uri.port; - return DetailListTile( - leading: const Icon(Icons.settings_input_component_outlined), - title: i18n.proxy.port, - subtitle: port.toString(), - trailing: IconButton( - icon: const Icon(Icons.edit), - onPressed: () async { - final newPort = await Editor.showIntEditor( - context, - desc: i18n.proxy.port, - initial: port, - ); - if (newPort == null) return; - if (newPort != port) { - setState(() { - uri = uri.replace( - port: newPort, - ); - }); - } - }, - ), - ); - } - - Widget buildProxyAuthTile() { - final userInfoParts = uri.userInfo.split(":"); - final auth = userInfoParts.length == 2 ? (username: userInfoParts[0], password: userInfoParts[1]) : null; - final text = auth != null ? "${auth.username}:${auth.password}" : null; - return ListTile( - leading: const Icon(Icons.key), - title: i18n.proxy.authentication.text(), - subtitle: text?.text(), - trailing: [ - if (auth != null) - IconButton( - onPressed: () { - setState(() { - uri = uri.replace(userInfo: ""); - }); - }, - icon: const Icon(Icons.delete), - ), - IconButton( - icon: const Icon(Icons.edit), - onPressed: () async { - final newAuth = await showAdaptiveDialog<({String username, String password})>( - context: context, - builder: (_) => StringsEditor( - fields: [ - (name: "username", initial: auth?.username ?? ""), - (name: "password", initial: auth?.password ?? ""), - ], - title: i18n.proxy.authentication, - ctor: (values) => (username: values[0].trim(), password: values[1].trim()), - ), - ); - if (newAuth != null && newAuth != auth) { - setState(() { - uri = uri.replace( - userInfo: - newAuth.password.isNotEmpty ? "${newAuth.username}:${newAuth.password}" : newAuth.username); - }); - } - }, - ), - ].wrap(), - ); - } - - Widget buildEnableProxyToggle() { - return _EnableProxyToggleTile( - enabled: enabled, - onChanged: (newV) { - setState(() { - enabled = newV; - }); - }, - ); - } - - Widget buildProxyModeSwitcher() { - return _ProxyModeSwitcherTile( - proxyMode: globalMode, - onChanged: (value) { - setState(() { - globalMode = value; - }); - }, - ); - } -} - -class _EnableProxyToggleTile extends StatelessWidget { - final bool enabled; - final ValueChanged onChanged; - - const _EnableProxyToggleTile({ - super.key, - required this.enabled, - required this.onChanged, - }); - - @override - Widget build(BuildContext context) { - return ListTile( - title: i18n.proxy.enableProxy.text(), - subtitle: i18n.proxy.enableProxyDesc.text(), - leading: const Icon(Icons.vpn_key), - trailing: Switch.adaptive( - value: enabled, - onChanged: onChanged, - ), - ); - } -} - -class _ProxyModeSwitcherTile extends StatelessWidget { - final ProxyMode? proxyMode; - final ValueChanged onChanged; - - const _ProxyModeSwitcherTile({ - super.key, - required this.proxyMode, - required this.onChanged, - }); - - @override - Widget build(BuildContext context) { - return ListTile( - isThreeLine: true, - leading: const Icon(Icons.public), - title: i18n.proxy.proxyMode.text(), - subtitle: ProxyMode.values - .map((mode) => ChoiceChip( - label: mode.l10nName().text(), - selected: proxyMode == mode, - onSelected: (value) { - onChanged(mode); - }, - )) - .toList() - .wrap(spacing: 4), - trailing: Tooltip( - triggerMode: TooltipTriggerMode.tap, - message: buildTooltip(), - child: const Icon(Icons.info_outline), - ).padAll(8), - ); - } - - String buildTooltip() { - final proxyMode = this.proxyMode; - if (proxyMode == null) { - return ProxyMode.values.map((mode) => "${mode.l10nName()}: ${mode.l10nTip()}").join("\n"); - } else { - return proxyMode.l10nTip(); - } - } -} diff --git a/lib/settings/page/school.dart b/lib/settings/page/school.dart deleted file mode 100644 index 2bb94f8f2..000000000 --- a/lib/settings/page/school.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:sit/credentials/entity/user_type.dart'; -import 'package:sit/credentials/widgets/oa_scope.dart'; -import 'package:sit/settings/settings.dart'; -import 'package:rettulf/rettulf.dart'; -import '../i18n.dart'; - -class SchoolSettingsPage extends StatefulWidget { - const SchoolSettingsPage({ - super.key, - }); - - @override - State createState() => _SchoolSettingsPageState(); -} - -class _SchoolSettingsPageState extends State { - OaUserType? userType; - - @override - void didChangeDependencies() { - final auth = context.auth; - final newUserType = auth.userType; - if (userType != newUserType) { - setState(() { - userType = newUserType; - }); - } - super.didChangeDependencies(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - body: CustomScrollView( - physics: const RangeMaintainingScrollPhysics(), - slivers: [ - SliverAppBar.large( - pinned: true, - snap: false, - floating: false, - title: i18n.school.title.text(), - ), - SliverList.list( - children: [ - if (userType?.capability.enableClass2nd == true) buildClass2ndAutoRefreshToggle(), - ], - ), - ], - ), - ); - } - - Widget buildClass2ndAutoRefreshToggle() { - return StatefulBuilder( - builder: (ctx, setState) => ListTile( - title: i18n.school.class2nd.autoRefresh.text(), - subtitle: i18n.school.class2nd.autoRefreshDesc.text(), - leading: const Icon(Icons.refresh_outlined), - trailing: Switch.adaptive( - value: Settings.school.class2nd.autoRefresh, - onChanged: (newV) { - setState(() { - Settings.school.class2nd.autoRefresh = newV; - }); - }, - ), - ), - ); - } -} diff --git a/lib/settings/page/storage.dart b/lib/settings/page/storage.dart deleted file mode 100644 index d570d6795..000000000 --- a/lib/settings/page/storage.dart +++ /dev/null @@ -1,473 +0,0 @@ -import 'dart:math'; - -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:go_router/go_router.dart'; -import 'package:hive_flutter/hive_flutter.dart'; -import 'package:sit/design/widgets/card.dart'; -import 'package:sit/design/widgets/common.dart'; -import 'package:sit/design/adaptive/dialog.dart'; -import 'package:sit/design/adaptive/editor.dart'; -import 'package:sit/storage/hive/init.dart'; -import 'package:sit/widgets/page_grouper.dart'; -import 'package:rettulf/rettulf.dart'; - -import '../i18n.dart'; - -class LocalStoragePage extends StatefulWidget { - const LocalStoragePage({super.key}); - - @override - State createState() => _LocalStoragePageState(); -} - -class _LocalStoragePageState extends State { - final Map name2Box = {}; - - @override - void initState() { - super.initState(); - for (final entry in HiveInit.name2Box.entries) { - final boxName = entry.key; - final box = entry.value; - if (box.isOpen) { - name2Box[boxName] = box; - } - } - } - - @override - Widget build(BuildContext context) { - final boxes = name2Box.entries.map((e) => (name: e.key, box: e.value)).toList(); - boxes.sortBy((entry) => entry.name); - return context.isPortrait ? StorageListPortrait(boxes) : StorageListLandscape(boxes); - } -} - -class StorageListPortrait extends StatefulWidget { - final List<({String name, Box box})> boxes; - - const StorageListPortrait(this.boxes, {super.key}); - - @override - State createState() => _StorageListPortraitState(); -} - -class _StorageListPortraitState extends State { - @override - void initState() { - super.initState(); - } - - @override - Widget build(BuildContext ctx) { - return Scaffold( - resizeToAvoidBottomInset: false, - appBar: AppBar(title: i18n.title.text()), - body: ListView.separated( - itemCount: widget.boxes.length, - itemBuilder: (ctx, i) { - final (:name, :box) = widget.boxes[i]; - return BoxSection(box: box, boxName: name); - }, - separatorBuilder: (BuildContext context, int index) { - return const Divider(); - }, - ), - ); - } -} - -class BoxSection extends StatefulWidget { - final String boxName; - final Box box; - - const BoxSection({ - super.key, - required this.box, - required this.boxName, - }); - - @override - State createState() => _BoxSectionState(); -} - -class _BoxSectionState extends State { - String get boxName => widget.boxName; - - Box get box => widget.box; - - Widget buildTitle(BuildContext ctx) { - final box = this.box; - final boxNameStyle = ctx.textTheme.headlineSmall; - final action = PopupMenuButton( - position: PopupMenuPosition.under, - padding: EdgeInsets.zero, - itemBuilder: (ctx) => [ - PopupMenuItem( - child: ListTile( - leading: const Icon(Icons.edit, color: Colors.redAccent), - title: i18n.clear.text(style: const TextStyle(color: Colors.redAccent)), - ), - onTap: () async { - final confirm = await _showDeleteBoxRequest(ctx); - if (confirm == true) { - box.clear(); - // Add a delay to ensure the box is really empty. - await Future.delayed(const Duration(milliseconds: 500)); - if (!mounted) return; - setState(() {}); - } - }, - ), - ], - ); - return ListTile( - title: Text(boxName, style: boxNameStyle, textAlign: TextAlign.center), - trailing: action, - ).inOutlinedCard(); - } - - @override - Widget build(BuildContext context) { - final curBox = box; - return [ - buildTitle(context), - BoxItemList(box: curBox), - ].column(mas: MainAxisSize.min).sized(w: double.infinity).padSymmetric(v: 5, h: 10); - } -} - -class BoxItemList extends StatefulWidget { - final Box box; - - const BoxItemList({super.key, required this.box}); - - @override - State createState() => _BoxItemListState(); -} - -class _BoxItemListState extends State { - int currentPage = 0; - static const pageSize = 6; - - late final $box = widget.box.listenable(); - - @override - Widget build(BuildContext context) { - return $box >> (ctx, _) => buildBody(ctx); - } - - Widget buildBody(BuildContext context) { - final keys = widget.box.keys.toList(); - final length = keys.length; - if (length < pageSize) { - return buildBoxItems(context, keys); - } else { - final start = currentPage * pageSize; - var totalPages = length ~/ pageSize; - if (length % pageSize != 0) { - totalPages++; - } - final end = min(start + pageSize, length); - return [ - buildPaginated(context, totalPages).padAll(10), - AnimatedSize( - duration: const Duration(milliseconds: 300), - curve: Curves.fastEaseInToSlowEaseOut, - child: buildBoxItems(context, keys.sublist(start, end)), - ), - ].column(); - } - } - - Widget buildBoxItems(BuildContext ctx, List keys) { - final routeStyle = context.textTheme.titleMedium; - final typeStyle = context.textTheme.bodySmall; - final contentStyle = context.textTheme.bodyMedium; - return keys - .map((e) => BoxItem( - keyInBox: e, - box: widget.box, - routeStyle: routeStyle, - typeStyle: typeStyle, - contentStyle: contentStyle, - onBoxChanged: () { - if (!mounted) return; - setState(() {}); - }, - )) - .toList() - .column(); - } - - Widget buildPaginated(BuildContext ctx, int totalPage) { - return PageGrouper( - paginateButtonStyles: PageBtnStyles(), - preBtnStyles: SkipBtnStyle( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(20), - bottomLeft: Radius.circular(20), - ), - ), - onPageChange: (number) { - setState(() { - currentPage = number - 1; - }); - }, - totalPage: totalPage, - btnPerGroup: (ctx.mediaQuery.size.width / 50.w).round().clamp(1, totalPage), - currentPageIndex: currentPage + 1, - ); - } -} - -class BoxItem extends StatefulWidget { - final TextStyle? routeStyle; - - final TextStyle? typeStyle; - final TextStyle? contentStyle; - final dynamic keyInBox; - final Box box; - final VoidCallback? onBoxChanged; - - const BoxItem({ - super.key, - this.routeStyle, - this.typeStyle, - this.contentStyle, - required this.keyInBox, - required this.box, - this.onBoxChanged, - }); - - @override - State createState() => _BoxItemState(); -} - -class _BoxItemState extends State { - @override - Widget build(BuildContext context) { - final key = widget.keyInBox.toString(); - final value = widget.box.get(widget.keyInBox); - final type = value.runtimeType.toString(); - Widget res = ListTile( - isThreeLine: true, - title: key.text(style: widget.routeStyle), - trailing: buildActionButton(key, value), - subtitle: [ - type.text(style: widget.typeStyle?.copyWith(color: Editor.isSupport(value) ? Colors.green : null)), - '$value'.text( - maxLines: 5, - style: widget.contentStyle?.copyWith(overflow: TextOverflow.ellipsis), - ) - ].column(caa: CrossAxisAlignment.start).align(at: Alignment.topLeft), - ).inFilledCard(); - if (value != null) { - res = res.on(tap: () async => showContentDialog(context, widget.box, key, value)); - } - return res; - } - - Widget buildActionButton(String key, dynamic value) { - return PopupMenuButton( - position: PopupMenuPosition.under, - padding: EdgeInsets.zero, - itemBuilder: (ctx) => [ - PopupMenuItem( - child: ListTile( - leading: const Icon(Icons.cleaning_services_outlined, color: Colors.redAccent), - title: i18n.clear.text(style: const TextStyle(color: Colors.redAccent)), - ), - onTap: () async { - final confirm = await context.showRequest( - title: i18n.warning, - desc: i18n.dev.storage.emptyValueDesc, - yes: i18n.confirm, - no: i18n.cancel, - highlight: true); - if (confirm == true) { - widget.box.put(key, _emptyValue(value)); - if (!mounted) return; - setState(() {}); - } - }, - ), - PopupMenuItem( - child: ListTile( - leading: const Icon(Icons.delete_outline_outlined, color: Colors.redAccent), - title: i18n.delete.text(style: const TextStyle(color: Colors.redAccent)), - onTap: () async { - ctx.pop(); - final confirm = await _showDeleteItemRequest(ctx); - if (confirm == true) { - widget.box.delete(key); - widget.onBoxChanged?.call(); - } - }, - ), - ), - ], - ); - } - - Future showContentDialog(BuildContext context, Box box, String key, dynamic value, - {bool readonly = false}) async { - if (readonly || !Editor.isSupport(value)) { - await Editor.showReadonlyEditor(context, desc: key, initial: value); - } else { - final newValue = await Editor.showAnyEditor(context, value, desc: key); - if (newValue == null) return; - bool isModified = value != newValue; - if (isModified) { - box.put(key, newValue); - if (!mounted) return; - setState(() {}); - } - } - } -} - -class StorageListLandscape extends StatefulWidget { - final List<({String name, Box box})> boxes; - - const StorageListLandscape(this.boxes, {super.key}); - - @override - State createState() => _StorageListLandscapeState(); -} - -class _StorageListLandscapeState extends State { - late String? selectedBoxName = widget.boxes.firstOrNull?.name; - - @override - void initState() { - super.initState(); - } - - @override - Widget build(BuildContext ctx) { - return Scaffold( - resizeToAvoidBottomInset: false, - appBar: AppBar( - title: i18n.title.text(), - elevation: 0, - ), - body: [ - buildBoxTitle().expanded(), - const VerticalDivider( - thickness: 5, - ), - AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: buildBoxContentView(ctx), - ).padAll(10).flexible(flex: 2) - ].row()); - } - - Widget buildBoxTitle() { - final boxNameStyle = context.textTheme.titleMedium; - return ListView.builder( - itemCount: widget.boxes.length, - itemBuilder: (ctx, i) { - final (:name, :box) = widget.boxes[i]; - final color = name == selectedBoxName ? context.theme.secondaryHeaderColor : null; - final action = PopupMenuButton( - itemBuilder: (ctx) => [ - PopupMenuItem( - child: ListTile( - leading: const Icon(Icons.edit, color: Colors.redAccent), - title: i18n.clear.text(style: const TextStyle(color: Colors.redAccent)), - ), - onTap: () async { - final confirm = await _showDeleteBoxRequest(ctx); - if (confirm == true) { - box.clear(); - if (!mounted) return; - setState(() {}); - } - }, - ), - ], - ); - return [ - name.text(style: boxNameStyle).padAll(10).on(tap: () { - if (selectedBoxName != name) { - setState(() { - selectedBoxName = name; - }); - } - }).expanded(), - action, - ].row().inCard(elevation: 3, color: color); - }, - ); - } - - Widget buildBoxContentView(BuildContext ctx) { - final name = selectedBoxName; - final selected = widget.boxes.firstWhereOrNull((tuple) => tuple.name == name); - if (selected == null) { - return LeavingBlank( - key: ValueKey(name), - icon: Icons.unarchive_outlined, - desc: i18n.dev.storage.selectBoxTip, - ); - } - final routeStyle = context.textTheme.titleMedium; - final typeStyle = context.textTheme.bodySmall; - final contentStyle = context.textTheme.bodyMedium; - final keys = selected.box.keys.toList(); - return ListView.builder( - itemCount: keys.length, - itemBuilder: (ctx, i) { - return BoxItem( - keyInBox: keys[i], - box: selected.box, - routeStyle: routeStyle, - typeStyle: typeStyle, - contentStyle: contentStyle, - onBoxChanged: () { - if (!mounted) return; - setState(() {}); - }, - ); - }, - ); - } -} - -/// THIS IS VERY DANGEROUS!!! -dynamic _emptyValue(dynamic value) { - if (value is String) { - return ""; - } else if (value is bool) { - return false; - } else if (value is int) { - return 0; - } else if (value is double) { - return 0.0; - } else if (value is List) { - value.clear(); - return value; - } else if (value is Set) { - value.clear(); - return value; - } else if (value is Map) { - value.clear(); - return value; - } else { - return value; - } -} - -Future _showDeleteBoxRequest(BuildContext ctx) async { - return await ctx.showRequest( - title: i18n.delete, desc: i18n.dev.storage.clearBoxDesc, yes: i18n.confirm, no: i18n.cancel, highlight: true); -} - -Future _showDeleteItemRequest(BuildContext ctx) async { - return await ctx.showRequest( - title: i18n.delete, desc: i18n.dev.storage.deleteItemDesc, yes: i18n.delete, no: i18n.cancel, highlight: true); -} diff --git a/lib/settings/page/timetable.dart b/lib/settings/page/timetable.dart deleted file mode 100644 index d1ac5f933..000000000 --- a/lib/settings/page/timetable.dart +++ /dev/null @@ -1,94 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:sit/settings/settings.dart'; -import 'package:rettulf/rettulf.dart'; -import '../i18n.dart'; - -class TimetableSettingsPage extends StatefulWidget { - const TimetableSettingsPage({ - super.key, - }); - - @override - State createState() => _TimetableSettingsPageState(); -} - -class _TimetableSettingsPageState extends State { - @override - Widget build(BuildContext context) { - return Scaffold( - body: CustomScrollView( - physics: const RangeMaintainingScrollPhysics(), - slivers: [ - SliverAppBar.large( - pinned: true, - snap: false, - floating: false, - title: i18n.timetable.title.text(), - ), - SliverList.list( - children: [ - buildAutoUseImportedToggle(), - const Divider(), - buildCellStyle(), - buildP13n(), - buildBackground(), - ], - ), - ], - ), - ); - } - - Widget buildAutoUseImportedToggle() { - return ListTile( - title: i18n.timetable.autoUseImported.text(), - subtitle: i18n.timetable.autoUseImportedDesc.text(), - leading: const Icon(Icons.auto_mode_outlined), - trailing: Switch.adaptive( - value: Settings.timetable.autoUseImported, - onChanged: (newV) { - setState(() { - Settings.timetable.autoUseImported = newV; - }); - }, - ), - ); - } - - Widget buildP13n() { - return ListTile( - leading: const Icon(Icons.color_lens_outlined), - title: i18n.timetable.palette.text(), - subtitle: i18n.timetable.paletteDesc.text(), - trailing: const Icon(Icons.open_in_new), - onTap: () async { - await context.push("/timetable/p13n"); - }, - ); - } - - Widget buildCellStyle() { - return ListTile( - leading: const Icon(Icons.view_comfortable_outlined), - title: i18n.timetable.cellStyle.text(), - subtitle: i18n.timetable.cellStyleDesc.text(), - trailing: const Icon(Icons.open_in_new), - onTap: () async { - await context.push("/timetable/cell-style"); - }, - ); - } - - Widget buildBackground() { - return ListTile( - leading: const Icon(Icons.image_outlined), - title: i18n.timetable.background.text(), - subtitle: i18n.timetable.backgroundDesc.text(), - trailing: const Icon(Icons.open_in_new), - onTap: () async { - await context.push("/timetable/background"); - }, - ); - } -} diff --git a/lib/settings/settings.dart b/lib/settings/settings.dart deleted file mode 100644 index d3c37f2d4..000000000 --- a/lib/settings/settings.dart +++ /dev/null @@ -1,289 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:hive_flutter/hive_flutter.dart'; -import 'package:sit/credentials/entity/credential.dart'; -import 'package:sit/entity/campus.dart'; -import 'package:sit/school/settings.dart'; -import 'package:sit/storage/hive/type_id.dart'; -import 'package:sit/timetable/settings.dart'; -import 'package:sit/utils/collection.dart'; - -import '../life/settings.dart'; - -part "settings.g.dart"; - -class _K { - static const ns = "/settings"; - static const campus = '$ns/campus'; - static const focusTimetable = '$ns/focusTimetable'; - static const lastSignature = '$ns/lastSignature'; -} - -class _DeveloperK { - static const ns = '/developer'; - static const devMode = '$ns/devMode'; - static const savedOaCredentialsList = '$ns/savedOaCredentialsList'; -} - -// ignore: non_constant_identifier_names -late SettingsImpl Settings; - -class SettingsImpl { - final Box box; - - SettingsImpl(this.box); - - late final life = LifeSettings(box); - late final timetable = TimetableSettings(box); - late final school = SchoolSettings(box); - late final theme = _Theme(box); - late final proxy = _Proxy(box); - - Campus get campus => box.get(_K.campus) ?? Campus.fengxian; - - set campus(Campus newV) => box.put(_K.campus, newV); - - ValueListenable listenCampus() => box.listenable(keys: [_K.campus]); - - bool get focusTimetable => box.get(_K.focusTimetable) ?? false; - - set focusTimetable(bool newV) => box.put(_K.focusTimetable, newV); - - ValueListenable listenFocusTimetable() => box.listenable(keys: [_K.focusTimetable]); - - String? get lastSignature => box.get(_K.lastSignature); - - set lastSignature(String? value) => box.put(_K.lastSignature, value); - - /// [false] by default. - bool get isDeveloperMode => box.get(_DeveloperK.devMode) ?? false; - - set isDeveloperMode(bool newV) => box.put(_DeveloperK.devMode, newV); - - ValueListenable listenIsDeveloperMode() => box.listenable(keys: [_DeveloperK.devMode]); - - List? getSavedOaCredentialsList() => - (box.get(_DeveloperK.savedOaCredentialsList) as List?)?.cast(); - - Future setSavedOaCredentialsList(List? newV) async { - newV?.distinctBy((c) => c.account); - await box.put(_DeveloperK.savedOaCredentialsList, newV); - } -} - -class _ThemeK { - static const ns = '/theme'; - static const themeColor = '$ns/themeColor'; - static const themeMode = '$ns/themeMode'; -} - -class _Theme { - final Box box; - - const _Theme(this.box); - - // theme - Color? get themeColor { - final value = box.get(_ThemeK.themeColor); - if (value == null) { - return null; - } else { - return Color(value); - } - } - - set themeColor(Color? v) { - box.put(_ThemeK.themeColor, v?.value); - } - - /// [ThemeMode.system] by default. - ThemeMode get themeMode => box.get(_ThemeK.themeMode) ?? ThemeMode.system; - - set themeMode(ThemeMode value) => box.put(_ThemeK.themeMode, value); - - ValueListenable listenThemeMode() => box.listenable(keys: [_ThemeK.themeMode]); - - ValueListenable listenThemeChange() => box.listenable(keys: [_ThemeK.themeMode, _ThemeK.themeColor]); -} - -enum ProxyType { - http( - defaultHost: "localhost", - defaultPort: 3128, - supportedProtocols: [ - "http", - "https", - ], - defaultProtocol: "http", - ), - https( - defaultHost: "localhost", - defaultPort: 443, - supportedProtocols: [ - "http", - "https", - ], - defaultProtocol: "https", - ), - all( - defaultHost: "localhost", - defaultPort: 1080, - supportedProtocols: [ - "socks5", - ], - defaultProtocol: "socks5", - ); - - final String defaultHost; - final int defaultPort; - final List supportedProtocols; - final String defaultProtocol; - - String l10n() => "settings.proxy.proxyType.$name".tr(); - - const ProxyType({ - required this.defaultHost, - required this.defaultPort, - required this.supportedProtocols, - required this.defaultProtocol, - }); - - Uri buildDefaultUri() => Uri(scheme: defaultProtocol, host: defaultHost, port: defaultPort); - - bool isDefaultUri(Uri uri) { - if (uri.scheme != defaultProtocol) return false; - if (uri.host != defaultHost) return false; - if (uri.port != defaultPort) return false; - if (uri.hasQuery) return false; - if (uri.hasFragment) return false; - if (uri.userInfo.isNotEmpty) return false; - return true; - } -} - -@HiveType(typeId: CoreHiveType.proxyMode) -enum ProxyMode { - @HiveField(0) - global, - @HiveField(1) - schoolOnly; - - String l10nName() => "settings.proxy.proxyMode.$name.name".tr(); - - String l10nTip() => "settings.proxy.proxyMode.$name.tip".tr(); -} - -class _ProxyK { - static const ns = '/proxy'; - - static String address(ProxyType type) => "$ns/${type.name}/address"; - - static String enabled(ProxyType type) => "$ns/${type.name}/enabled"; - - static String proxyMode(ProxyType type) => "$ns/${type.name}/proxyMode"; -} - -typedef ProxyProfileRecords = ({String? address, bool enabled, ProxyMode proxyMode}); - -class ProxyProfile { - final Box box; - final ProxyType type; - - ProxyProfile(this.box, String ns, this.type); - - String? get address => box.get(_ProxyK.address(type)); - - set address(String? newV) => box.put(_ProxyK.address(type), newV); - - /// [false] by default. - bool get enabled => box.get(_ProxyK.enabled(type)) ?? false; - - set enabled(bool newV) => box.put(_ProxyK.enabled(type), newV); - - /// [ProxyMode.schoolOnly] by default. - ProxyMode get proxyMode => box.get(_ProxyK.proxyMode(type)) ?? ProxyMode.schoolOnly; - - set proxyMode(ProxyMode newV) => box.put(_ProxyK.proxyMode(type), newV); - - bool get isDefaultAddress { - final address = this.address; - if (address == null) return true; - final uri = Uri.tryParse(address); - if (uri == null) return true; - return type.isDefaultUri(uri); - } -} - -class _Proxy { - final Box box; - - _Proxy(this.box) - : http = ProxyProfile(box, _ProxyK.ns, ProxyType.http), - https = ProxyProfile(box, _ProxyK.ns, ProxyType.https), - all = ProxyProfile(box, _ProxyK.ns, ProxyType.all); - - final ProxyProfile http; - final ProxyProfile https; - final ProxyProfile all; - - ProxyProfile resolve(ProxyType type) { - return switch (type) { - ProxyType.http => http, - ProxyType.https => https, - ProxyType.all => all, - }; - } - - void setProfile(ProxyType type, ProxyProfileRecords value) { - final profile = resolve(type); - profile.address = value.address; - profile.enabled = value.enabled; - profile.proxyMode = value.proxyMode; - } - - bool get anyEnabled => http.enabled || https.enabled || all.enabled; - - set anyEnabled(bool value) { - http.enabled = value; - https.enabled = value; - all.enabled = value; - } - - Listenable listenAnyEnabled() => box.listenable(keys: ProxyType.values.map((type) => _ProxyK.enabled(type)).toList()); - - /// return null if their proxy mode are not identical. - ProxyMode? getIntegratedProxyMode() { - final httpMode = http.proxyMode; - final httpsMode = https.proxyMode; - final allMode = all.proxyMode; - if (httpMode == httpsMode && httpMode == allMode) { - return httpMode; - } else { - return null; - } - } - - setIntegratedProxyMode(ProxyMode mode) { - http.proxyMode = mode; - https.proxyMode = mode; - all.proxyMode = mode; - } - - Listenable listenProxyMode() => - box.listenable(keys: ProxyType.values.map((type) => _ProxyK.proxyMode(type)).toList()); - - Listenable listenAnyChange({bool address = true, bool enabled = true, ProxyType? type}) { - if (type == null) { - return box.listenable(keys: [ - if (address) ProxyType.values.map((type) => _ProxyK.address(type)), - if (enabled) ProxyType.values.map((type) => _ProxyK.enabled(type)), - ]); - } else { - return box.listenable(keys: [ - if (address) _ProxyK.address(type), - if (enabled) _ProxyK.enabled(type), - ]); - } - } -} diff --git a/lib/settings/settings.g.dart b/lib/settings/settings.g.dart deleted file mode 100644 index 2b1c63e3f..000000000 --- a/lib/settings/settings.g.dart +++ /dev/null @@ -1,43 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'settings.dart'; - -// ************************************************************************** -// TypeAdapterGenerator -// ************************************************************************** - -class ProxyModeAdapter extends TypeAdapter { - @override - final int typeId = 7; - - @override - ProxyMode read(BinaryReader reader) { - switch (reader.readByte()) { - case 0: - return ProxyMode.global; - case 1: - return ProxyMode.schoolOnly; - default: - return ProxyMode.global; - } - } - - @override - void write(BinaryWriter writer, ProxyMode obj) { - switch (obj) { - case ProxyMode.global: - writer.writeByte(0); - break; - case ProxyMode.schoolOnly: - writer.writeByte(1); - break; - } - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || other is ProxyModeAdapter && runtimeType == other.runtimeType && typeId == other.typeId; -} diff --git a/lib/storage/hive/adapter.dart b/lib/storage/hive/adapter.dart deleted file mode 100644 index 77f316c32..000000000 --- a/lib/storage/hive/adapter.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:hive/hive.dart'; -import 'package:sit/credentials/entity/credential.dart'; -import 'package:sit/credentials/entity/login_status.dart'; -import 'package:sit/credentials/entity/user_type.dart'; -import 'package:sit/entity/campus.dart'; -import 'package:sit/life/electricity/entity/balance.dart'; -import 'package:sit/life/expense_records/entity/local.dart'; -import 'package:sit/school/exam_result/entity/result.pg.dart'; -import 'package:sit/school/library/entity/book.dart'; -import 'package:sit/school/library/entity/borrow.dart'; -import 'package:sit/school/library/entity/image.dart'; -import 'package:sit/school/ywb/entity/service.dart'; -import 'package:sit/school/ywb/entity/application.dart'; -import 'package:sit/school/exam_result/entity/result.ug.dart'; -import 'package:sit/school/oa_announce/entity/announce.dart'; -import 'package:sit/school/class2nd/entity/details.dart'; -import 'package:sit/school/class2nd/entity/list.dart'; -import 'package:sit/school/class2nd/entity/attended.dart'; -import 'package:sit/school/entity/school.dart'; -import 'package:sit/school/yellow_pages/entity/contact.dart'; -import 'package:sit/settings/settings.dart'; -import 'package:sit/storage/hive/init.dart'; - -import 'builtin.dart'; - -class HiveAdapter { - HiveAdapter._(); - - static void registerCoreAdapters(HiveInterface hive) { - debugPrint("Register core Hive type"); - // Basic - hive.addAdapter(SizeAdapter()); - hive.addAdapter(VersionAdapter()); - hive.addAdapter(ThemeModeAdapter()); - hive.addAdapter(CampusAdapter()); - - // Credential - hive.addAdapter(CredentialsAdapter()); - hive.addAdapter(LoginStatusAdapter()); - hive.addAdapter(OaUserTypeAdapter()); - - // Settings - hive.addAdapter(ProxyModeAdapter()); - } - - static void registerCacheAdapters(HiveInterface hive) { - debugPrint("Register cache Hive type"); - // Electric Bill - hive.addAdapter(ElectricityBalanceAdapter()); - - // Activity - hive.addAdapter(Class2ndActivityDetailsAdapter()); - hive.addAdapter(Class2ndActivityAdapter()); - hive.addAdapter(Class2ndPointsSummaryAdapter()); - hive.addAdapter(Class2ndActivityApplicationAdapter()); - hive.addAdapter(Class2ndPointItemAdapter()); - hive.addAdapter(Class2ndActivityCatAdapter()); - hive.addAdapter(Class2ndPointTypeAdapter()); - - // OA Announcement - hive.addAdapter(OaAnnounceDetailsAdapter()); - hive.addAdapter(OaAnnounceRecordAdapter()); - hive.addAdapter(OaAnnounceAttachmentAdapter()); - - // Application - hive.addAdapter(YwbServiceDetailSectionAdapter()); - hive.addAdapter(YwbServiceDetailsAdapter()); - hive.addAdapter(YwbServiceAdapter()); - hive.addAdapter(YwbApplicationAdapter()); - hive.addAdapter(YwbApplicationTrackAdapter()); - - // Exam Result - hive.addAdapter(ExamResultUgAdapter()); - hive.addAdapter(ExamResultItemAdapter()); - hive.addAdapter(UgExamTypeAdapter()); - hive.addAdapter(ExamResultPgAdapter()); - - // Expense Records - hive.addAdapter(TransactionAdapter()); - hive.addAdapter(TransactionTypeAdapter()); - - // Yellow Pages - hive.addAdapter(SchoolContactAdapter()); - - // Library - hive.addAdapter(BookAdapter()); - hive.addAdapter(BookDetailsAdapter()); - hive.addAdapter(BorrowedBookItemAdapter()); - hive.addAdapter(BookBorrowingHistoryItemAdapter()); - hive.addAdapter(BookBorrowingHistoryOperationAdapter()); - hive.addAdapter(BookImageAdapter()); - - // School - hive.addAdapter(SemesterAdapter()); - hive.addAdapter(SemesterInfoAdapter()); - hive.addAdapter(CourseCatAdapter()); - } -} diff --git a/lib/storage/hive/builtin.dart b/lib/storage/hive/builtin.dart deleted file mode 100644 index 42fe5c776..000000000 --- a/lib/storage/hive/builtin.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:version/version.dart'; - -import 'type_id.dart'; - -class VersionAdapter extends TypeAdapter { - @override - final int typeId = CoreHiveType.version; - - @override - Version read(BinaryReader reader) { - final major = reader.readInt(); - final minor = reader.readInt(); - final patch = reader.readInt(); - final build = reader.readString(); - return Version(major, minor, patch, build: build); - } - - @override - void write(BinaryWriter writer, Version obj) { - writer.writeInt(obj.major); - writer.writeInt(obj.minor); - writer.writeInt(obj.patch); - writer.writeString(obj.build); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || other is VersionAdapter && runtimeType == other.runtimeType && typeId == other.typeId; -} - -class ThemeModeAdapter extends TypeAdapter { - @override - final int typeId = CoreHiveType.themeMode; - - @override - ThemeMode read(BinaryReader reader) { - final index = reader.readInt32(); - return ThemeMode.values[index]; - } - - @override - void write(BinaryWriter writer, ThemeMode obj) { - writer.writeInt32(obj.index); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || other is ThemeModeAdapter && runtimeType == other.runtimeType && typeId == other.typeId; -} - -/// There is no need to consider revision -class SizeAdapter extends TypeAdapter { - @override - final int typeId = CoreHiveType.size; - - @override - Size read(BinaryReader reader) { - var x = reader.readDouble(); - var y = reader.readDouble(); - return Size(x, y); - } - - @override - void write(BinaryWriter writer, Size obj) { - writer.writeDouble(obj.width); - writer.writeDouble(obj.height); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || other is SizeAdapter && runtimeType == other.runtimeType && typeId == other.typeId; -} diff --git a/lib/storage/hive/cookie.dart b/lib/storage/hive/cookie.dart deleted file mode 100644 index f86c183a2..000000000 --- a/lib/storage/hive/cookie.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:cookie_jar/cookie_jar.dart'; -import 'package:hive/hive.dart'; - -class HiveCookieJar implements Storage { - final Box box; - - const HiveCookieJar(this.box); - - @override - Future init(bool persistSession, bool ignoreExpires) async {} - - @override - Future write(String key, String value) async => box.put(key, value); - - @override - Future read(String key) async => box.get(key); - - @override - Future delete(String key) async => box.delete(key); - - @override - Future deleteAll(List keys) async => box.deleteAll(keys); -} diff --git a/lib/storage/hive/init.dart b/lib/storage/hive/init.dart deleted file mode 100644 index 6cbff7f83..000000000 --- a/lib/storage/hive/init.dart +++ /dev/null @@ -1,118 +0,0 @@ -import 'dart:io'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; -import 'package:hive/hive.dart'; -import 'package:sit/settings/meta.dart'; -import 'package:sit/settings/settings.dart'; -import "package:hive/src/hive_impl.dart"; -import 'adapter.dart'; - -class HiveInit { - const HiveInit._(); - - static final core = HiveImpl(); - static final cache = HiveImpl(); - - static late Box credentials; - static late Box library; - static late Box timetable; - static late Box expense; - static late Box yellowPages; - static late Box class2nd; - static late Box examArrange; - static late Box examResult; - static late Box oaAnnounce; - static late Box ywb; - static late Box eduEmail; - static late Box settings; - static late Box electricity; - static late Box meta; - static late Box cookies; - - static late Map name2Box; - static late List cacheBoxes; - - static Future initLocalStorage({ - required Directory coreDir, - required Directory cacheDir, - }) async { - debugPrint("Initializing hive"); - await core.initFlutter(coreDir); - await cache.initFlutter(cacheDir); - } - - static void initAdapters() async { - debugPrint("Initializing hive adapters"); - HiveAdapter.registerCoreAdapters(core); - HiveAdapter.registerCacheAdapters(cache); - } - - static Future initBox() async { - debugPrint("Initializing hive box"); - name2Box = _name2Box([ - credentials = await core.openBox('credentials'), - settings = await core.openBox('settings'), - meta = await core.openBox('meta'), - timetable = await core.openBox('timetable'), - ...cacheBoxes = [ - yellowPages = await cache.openBox('yellow-pages'), - eduEmail = await cache.openBox('edu-email'), - if (!kIsWeb) cookies = await cache.openBox('cookies'), - if (!kIsWeb) expense = await cache.openBox('expense'), - if (!kIsWeb) library = await cache.openBox('library'), - if (!kIsWeb) examArrange = await cache.openBox('exam-arrange'), - if (!kIsWeb) examResult = await cache.openBox('exam-result'), - if (!kIsWeb) oaAnnounce = await cache.openBox('oa-announce'), - if (!kIsWeb) class2nd = await cache.openBox('class2nd'), - if (!kIsWeb) ywb = await cache.openBox('ywb'), - if (!kIsWeb) electricity = await cache.openBox('electricity'), - ], - ]); - Settings = SettingsImpl(settings); - Meta = MetaImpl(meta); - } - - static Map _name2Box(List boxes) { - final map = {}; - for (final box in boxes) { - map[box.name] = box; - } - return map; - } - - static Future clear() async { - for (final box in name2Box.values) { - await box.clear(); - } - } - - static Future clearCache() async { - for (final box in cacheBoxes) { - await box.clear(); - } - } -} - -/// Flutter extensions for Hive. -extension HiveX on HiveInterface { - /// Initializes Hive with the path from [getApplicationDocumentsDirectory]. - /// - /// You can provide a [subDir] where the boxes should be stored. - Future initFlutter(Directory dir) async { - WidgetsFlutterBinding.ensureInitialized(); - if (kIsWeb) return; - init(dir.path); - } - - void addAdapter(TypeAdapter adapter) { - assert( - !isAdapterRegistered(adapter.typeId), - "Trying to register adapter of $T, but the type ID #${adapter.typeId} is occupied by ${(this as dynamic).findAdapterForTypeId(adapter.typeId)}", - ); - if (!isAdapterRegistered(adapter.typeId)) { - registerAdapter(adapter); - debugPrint("Register type adapter of $T at ${adapter.typeId}"); - } - } -} diff --git a/lib/storage/hive/table.dart b/lib/storage/hive/table.dart deleted file mode 100644 index 73275cfc4..000000000 --- a/lib/storage/hive/table.dart +++ /dev/null @@ -1,177 +0,0 @@ -import 'dart:convert'; - -import 'package:flutter/foundation.dart'; -import 'package:hive/hive.dart'; -import 'package:hive_flutter/hive_flutter.dart'; - -const _kLastId = "lastId"; -const _kIdList = "idList"; -const _kRows = "rows"; -const _kSelectedId = "selectedId"; -const _kLastIdStart = 0; - -class Notifier with ChangeNotifier { - void notifier() => notifyListeners(); -} - -class HiveTable { - final String base; - final Box box; - - final String _lastIdK; - final String _idListK; - final String _rowsK; - final String _selectedIdK; - final ({T Function(Map json) fromJson, Map Function(T row) toJson})? useJson; - - /// notify if selected row was changed. - final $selected = Notifier(); - - /// notify if any row was changed. - final $any = Notifier(); - - /// The delegate of getting row - final T? Function(int id, T? Function(int id) builtin)? get; - - /// The delegate of setting row - final void Function(int id, T? newV, void Function(int id, T? newV) builtin)? set; - - HiveTable({ - required this.base, - required this.box, - this.get, - this.set, - this.useJson, - }) : _lastIdK = "$base/$_kLastId", - _idListK = "$base/$_kIdList", - _rowsK = "$base/$_kRows", - _selectedIdK = "$base/$_kSelectedId"; - - bool get hasAny => idList?.isNotEmpty ?? false; - - int get lastId => box.get(_lastIdK) ?? _kLastIdStart; - - set lastId(int newValue) => box.put(_lastIdK, newValue); - - List? get idList => box.get(_idListK); - - set idList(List? newValue) => box.put(_idListK, newValue); - - int? get selectedId => box.get(_selectedIdK); - - bool get isEmpty => idList?.isEmpty != false; - - bool get isNotEmpty => !isEmpty; - - set selectedId(int? newValue) { - box.put(_selectedIdK, newValue); - $selected.notifier(); - $any.notifier(); - } - - T? get selectedRow { - final id = selectedId; - if (id == null) { - return null; - } - return this[id]; - } - - T? operator [](int id) { - final get = this.get; - if (get != null) { - return get(id, _get); - } - return _get(id); - } - - T? _get(int id) { - final row = box.get("$_rowsK/$id"); - final useJson = this.useJson; - if (useJson == null || row == null) { - return row; - } else { - return useJson.fromJson(jsonDecode(row)); - } - } - - void operator []=(int id, T? newValue) { - final set = this.set; - if (set != null) { - set(id, newValue, _set); - } - return _set(id, newValue); - } - - void _set(int id, T? newValue) { - final useJson = this.useJson; - if (useJson == null || newValue == null) { - box.put("$_rowsK/$id", newValue); - } else { - box.put("$_rowsK/$id", jsonEncode(useJson.toJson(newValue))); - } - if (selectedId == id) { - $selected.notifier(); - } - $any.notifier(); - } - - /// Return a new ID for the [row]. - int add(T row) { - final curId = lastId++; - final ids = idList ?? []; - ids.add(curId); - this[curId] = row; - idList = ids; - return curId; - } - - /// Delete the timetable by [id]. - /// If [selectedId] is deleted, an available timetable would be switched to. - void delete(int id) { - final ids = idList; - if (ids == null) return; - if (ids.remove(id)) { - idList = ids; - if (selectedId == id) { - if (ids.isNotEmpty) { - selectedId = ids.first; - } else { - selectedId = null; - } - } - box.delete("$_rowsK/$id"); - $any.notifier(); - } - } - - void drop() { - final ids = idList; - if (ids == null) return; - for (final id in ids) { - box.delete("$_rowsK/$id"); - } - box.delete(_idListK); - box.delete(_selectedIdK); - box.delete(_lastIdK); - $selected.notifier(); - $any.notifier(); - } - - // TODO: Row delegate? - /// ignore null row - List<({int id, T row})> getRows() { - final ids = idList; - final res = <({int id, T row})>[]; - if (ids == null) return res; - for (final id in ids) { - final row = this[id]; - if (row != null) { - res.add((id: id, row: row)); - } - } - return res; - } - - Listenable listenRowChange(int id) => box.listenable(keys: ["$_rowsK/$id"]); -} diff --git a/lib/storage/hive/type_id.dart b/lib/storage/hive/type_id.dart deleted file mode 100644 index 73c47ff85..000000000 --- a/lib/storage/hive/type_id.dart +++ /dev/null @@ -1,74 +0,0 @@ -export "package:hive/hive.dart"; - -/// Basic 0-19 -class CoreHiveType { - static const size = 0; - static const themeMode = 1; - static const version = 2; - static const campus = 3; - static const credentials = 4; - static const loginStatus = 5; - static const oaUserType = 6; - static const proxyMode = 7; -} - -class CacheHiveType { - // School 20 - static const _school = 0; - static const semester = _school + 0; - static const semesterInfo = _school + 1; - static const courseCat = _school + 2; - - // Exam result 10 - static const _examResult = _school + 20; - static const examResultUg = _examResult + 0; - static const examResultUgItem = _examResult + 1; - static const examResultUgExamType = _examResult + 2; - static const examResultPg = _examResult + 3; - - // Second class 20 - static const _class2nd = _examResult + 10; - static const activity = _class2nd + 0; - static const activityDetails = _class2nd + 1; - static const activityCat = _class2nd + 2; - static const class2ndPointsSummary = _class2nd + 3; - static const class2ndActivityApplication = _class2nd + 4; - static const class2ndPointItem = _class2nd + 5; - static const class2ndScoreType = _class2nd + 6; - - // Expense 10 - static const _expense = _class2nd + 20; - static const expenseTransactionType = _expense + 0; - static const expenseTransaction = _expense + 1; - - // Electricity 10 - static const _electricity = _expense + 10; - static const electricityBalance = _electricity + 0; - - // Ywb 20 - static const _ywb = _electricity + 10; - static const ywbServiceDetails = _ywb + 0; - static const ywbServiceDetailSection = _ywb + 1; - static const ywbService = _ywb + 2; - static const ywbApplication = _ywb + 3; - static const ywbApplicationTrack = _ywb + 4; - - // OaAnnounce 10 - static const _oaAnnounce = _ywb + 20; - static const oaAnnounceDetails = _oaAnnounce + 0; - static const oaAnnounceAttachment = _oaAnnounce + 1; - static const oaAnnounceRecord = _oaAnnounce + 2; - - // School yellow pages 5 - static const _yellowPages = _oaAnnounce + 10; - static const schoolContact = _yellowPages + 0; - - // Library 20 - static const _library = _yellowPages + 5; - static const libraryBook = _library + 0; - static const libraryBookImage = _library + 1; - static const libraryBookDetails = _library + 2; - static const libraryBorrowedBook = _library + 3; - static const libraryBorrowingHistory = _library + 4; - static const libraryBorrowingHistoryOp = _library + 5; -} diff --git a/lib/storage/prefs.dart b/lib/storage/prefs.dart deleted file mode 100644 index 36974cbf6..000000000 --- a/lib/storage/prefs.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'dart:ui'; - -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:sit/r.dart'; - -class _K { - static const lastVersion = "${R.appId}.lastVersion"; - static const lastWindowSize = "${R.appId}.lastWindowSize"; - static const installTime = "${R.appId}.installTime"; -} - -extension PrefsX on SharedPreferences { - String? getLastVersion() => getString(_K.lastVersion); - - Future setLastVersion(String value) => setString(_K.lastVersion, value); - - Size? getLastWindowSize() => _string2Size(getString(_K.lastWindowSize)); - - Future setLastWindowSize(Size value) => setString(_K.lastWindowSize, _size2String(value)); - - /// The first time when user launch this app - DateTime? getInstallTime() { - final raw = getString(_K.installTime); - if (raw == null) return null; - return DateTime.tryParse(raw); - } - - Future setInstallTime(DateTime value) => setString(_K.installTime, value.toString()); -} - -Size? _string2Size(String? value) { - if (value == null) return null; - final parts = value.split(","); - if (parts.length != 2) return null; - final width = int.tryParse(parts[0]); - final height = int.tryParse(parts[1]); - if (width == null || height == null) return null; - return Size(width.toDouble(), height.toDouble()); -} - -String _size2String(Size size) { - return "${size.width.toInt()},${size.height.toInt()}"; -} diff --git a/lib/timetable/entity/background.dart b/lib/timetable/entity/background.dart deleted file mode 100644 index 9b338fca1..000000000 --- a/lib/timetable/entity/background.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:copy_with_extension/copy_with_extension.dart'; -import 'package:json_annotation/json_annotation.dart'; - -part 'background.g.dart'; - -@JsonSerializable() -@CopyWith(skipFields: true) -class BackgroundImage { - @JsonKey() - final String path; - @JsonKey() - final double opacity; - @JsonKey() - final bool repeat; - @JsonKey() - final bool antialias; - - const BackgroundImage({ - required this.path, - this.opacity = 1.0, - this.repeat = true, - this.antialias = true, - }); - - const BackgroundImage.disabled({ - this.opacity = 1.0, - this.repeat = true, - this.antialias = true, - }) : path = ""; - - bool get enabled => path.isNotEmpty; - - factory BackgroundImage.fromJson(Map json) => _$BackgroundImageFromJson(json); - - Map toJson() => _$BackgroundImageToJson(this); - - @override - bool operator ==(Object other) { - return identical(this, other) || - other is BackgroundImage && - runtimeType == other.runtimeType && - path == other.path && - opacity == other.opacity && - repeat == other.repeat && - antialias == other.antialias; - } - - @override - int get hashCode => Object.hash(path, opacity, repeat, antialias); - - @override - String toString() { - return toJson().toString(); - } -} diff --git a/lib/timetable/entity/background.g.dart b/lib/timetable/entity/background.g.dart deleted file mode 100644 index fea878c7a..000000000 --- a/lib/timetable/entity/background.g.dart +++ /dev/null @@ -1,87 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'background.dart'; - -// ************************************************************************** -// CopyWithGenerator -// ************************************************************************** - -abstract class _$BackgroundImageCWProxy { - /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. - /// - /// Usage - /// ```dart - /// BackgroundImage(...).copyWith(id: 12, name: "My name") - /// ```` - BackgroundImage call({ - String? path, - double? opacity, - bool? repeat, - bool? antialias, - }); -} - -/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfBackgroundImage.copyWith(...)`. -class _$BackgroundImageCWProxyImpl implements _$BackgroundImageCWProxy { - const _$BackgroundImageCWProxyImpl(this._value); - - final BackgroundImage _value; - - @override - - /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. - /// - /// Usage - /// ```dart - /// BackgroundImage(...).copyWith(id: 12, name: "My name") - /// ```` - BackgroundImage call({ - Object? path = const $CopyWithPlaceholder(), - Object? opacity = const $CopyWithPlaceholder(), - Object? repeat = const $CopyWithPlaceholder(), - Object? antialias = const $CopyWithPlaceholder(), - }) { - return BackgroundImage( - path: path == const $CopyWithPlaceholder() || path == null - ? _value.path - // ignore: cast_nullable_to_non_nullable - : path as String, - opacity: opacity == const $CopyWithPlaceholder() || opacity == null - ? _value.opacity - // ignore: cast_nullable_to_non_nullable - : opacity as double, - repeat: repeat == const $CopyWithPlaceholder() || repeat == null - ? _value.repeat - // ignore: cast_nullable_to_non_nullable - : repeat as bool, - antialias: antialias == const $CopyWithPlaceholder() || antialias == null - ? _value.antialias - // ignore: cast_nullable_to_non_nullable - : antialias as bool, - ); - } -} - -extension $BackgroundImageCopyWith on BackgroundImage { - /// Returns a callable class that can be used as follows: `instanceOfBackgroundImage.copyWith(...)`. - // ignore: library_private_types_in_public_api - _$BackgroundImageCWProxy get copyWith => _$BackgroundImageCWProxyImpl(this); -} - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -BackgroundImage _$BackgroundImageFromJson(Map json) => BackgroundImage( - path: json['path'] as String, - opacity: (json['opacity'] as num?)?.toDouble() ?? 1.0, - repeat: json['repeat'] as bool? ?? true, - antialias: json['antialias'] as bool? ?? true, - ); - -Map _$BackgroundImageToJson(BackgroundImage instance) => { - 'path': instance.path, - 'opacity': instance.opacity, - 'repeat': instance.repeat, - 'antialias': instance.antialias, - }; diff --git a/lib/timetable/entity/course.dart b/lib/timetable/entity/course.dart deleted file mode 100644 index 1203f82d6..000000000 --- a/lib/timetable/entity/course.dart +++ /dev/null @@ -1,111 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; - -part 'course.g.dart'; - -@JsonSerializable(createToJson: false) -class UndergraduateCourseRaw { - /// 课程名称 - @JsonKey(name: 'kcmc') - final String courseName; - - /// 星期 - @JsonKey(name: 'xqjmc') - final String weekDayText; - - /// 节次 - @JsonKey(name: 'jcs') - final String timeslotsText; - - /// 周次 - @JsonKey(name: 'zcd') - final String weekText; - - /// 教室 - @JsonKey(name: 'cdmc') - final String place; - - /// 教师 - @JsonKey(name: 'xm', defaultValue: "") - final String teachers; - - /// 校区 - @JsonKey(name: 'xqmc') - final String campus; - - /// 学分 - @JsonKey(name: 'xf') - final String courseCredit; - - /// 学时 - @JsonKey(name: 'zxs') - final String creditHour; - - /// 教学班 - @JsonKey(name: 'jxbmc') - final String classCode; - - /// 课程代码 - @JsonKey(name: 'kch') - final String courseCode; - - factory UndergraduateCourseRaw.fromJson(Map json) => _$UndergraduateCourseRawFromJson(json); - - const UndergraduateCourseRaw({ - required this.courseName, - required this.weekDayText, - required this.timeslotsText, - required this.weekText, - required this.place, - required this.teachers, - required this.campus, - required this.courseCredit, - required this.creditHour, - required this.classCode, - required this.courseCode, - }); -} - -class PostgraduateCourseRaw { - /// 课程名称 - late String courseName; - - /// 星期 - late String weekDayText; - - /// 节次 - late String timeslotsText; - - /// 周次 - late String weekText; - - /// 教室 - late String place; - - /// 教师 - late String teachers; - - /// 学分 - late String courseCredit; - - /// 学时 - late String creditHour; - - /// 教学班 - late String classCode; - - /// 课程代码 - late String courseCode; - - PostgraduateCourseRaw({ - required this.courseName, - required this.weekDayText, - required this.timeslotsText, - required this.weekText, - required this.place, - required this.teachers, - required this.courseCredit, - required this.creditHour, - required this.classCode, - required this.courseCode, - }); -} diff --git a/lib/timetable/entity/course.g.dart b/lib/timetable/entity/course.g.dart deleted file mode 100644 index ebb625570..000000000 --- a/lib/timetable/entity/course.g.dart +++ /dev/null @@ -1,21 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'course.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -UndergraduateCourseRaw _$UndergraduateCourseRawFromJson(Map json) => UndergraduateCourseRaw( - courseName: json['kcmc'] as String, - weekDayText: json['xqjmc'] as String, - timeslotsText: json['jcs'] as String, - weekText: json['zcd'] as String, - place: json['cdmc'] as String, - teachers: json['xm'] as String? ?? '', - campus: json['xqmc'] as String, - courseCredit: json['xf'] as String, - creditHour: json['zxs'] as String, - classCode: json['jxbmc'] as String, - courseCode: json['kch'] as String, - ); diff --git a/lib/timetable/entity/display.dart b/lib/timetable/entity/display.dart deleted file mode 100644 index ed1c33a5e..000000000 --- a/lib/timetable/entity/display.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; - -/// 课表显示模式 -enum DisplayMode { - weekly, - daily; - - static DisplayMode? at(int? index) { - if (index == null) { - return null; - } else if (0 <= index && index < DisplayMode.values.length) { - return DisplayMode.values[index]; - } - return null; - } - - DisplayMode toggle() => DisplayMode.values[(index + 1) & 1]; - - String l10n() => "timetable.displayMode.$name".tr(); -} diff --git a/lib/timetable/entity/platte.dart b/lib/timetable/entity/platte.dart deleted file mode 100644 index 32f3fed8e..000000000 --- a/lib/timetable/entity/platte.dart +++ /dev/null @@ -1,156 +0,0 @@ -import 'dart:convert'; -import 'dart:typed_data'; -import 'dart:ui'; - -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:json_annotation/json_annotation.dart'; -import 'package:sit/utils/byte_io.dart'; - -part 'platte.g.dart'; - -int _colorToJson(Color color) => color.value; - -Color _colorFromJson(int value) => Color(value); - -typedef Color2Mode = ({Color light, Color dark}); - -Color2Mode _color2ModeFromJson(Map json) { - return ( - light: _colorFromJson(json["light"]), - dark: _colorFromJson(json["dark"]), - ); -} - -Map _color2ModeToJson(Color2Mode colors) { - return { - "light": _colorToJson(colors.light), - "dark": _colorToJson(colors.dark), - }; -} - -List _colorsFromJson(List json) { - return json.map((entry) => _color2ModeFromJson(entry)).toList(); -} - -List _colorsToJson(List colors) { - return colors.map((entry) => _color2ModeToJson(entry)).toList(); -} - -@JsonSerializable() -class TimetablePalette { - @JsonKey() - final String name; - @JsonKey() - final String author; - @JsonKey(fromJson: _colorsFromJson, toJson: _colorsToJson) - final List colors; - @JsonKey() - final DateTime? lastModified; - - const TimetablePalette({ - required this.name, - required this.author, - required this.colors, - this.lastModified, - }); - - factory TimetablePalette.fromJson(Map json) => _$TimetablePaletteFromJson(json); - - Map toJson() => _$TimetablePaletteToJson(this); - - static TimetablePalette decodeFromBase64(String encoded) { - final bytes = base64Decode(encoded); - return decodeFromByteList(bytes); - } - - static decodeFromByteList(Uint8List bytes) { - final reader = ByteReader(bytes); - final name = reader.strUtf8(); - final author = reader.strUtf8(); - final colorLen = reader.uint32(); - - List colors = []; - for (int i = 0; i < colorLen; i++) { - Color light = Color(reader.uint32()); - Color dark = Color(reader.uint32()); - colors.add((light: light, dark: dark)); - } - - return TimetablePalette( - name: name, - author: author, - colors: colors, - lastModified: DateTime.now(), - ); - } -} - -extension TimetablePaletteX on TimetablePalette { - TimetablePalette copyWith({ - String? name, - List? colors, - String? author, - DateTime? lastModified, - }) { - return TimetablePalette( - name: name ?? this.name, - colors: colors ?? List.of(this.colors), - author: author ?? this.author, - lastModified: lastModified ?? this.lastModified, - ); - } - - String encodeBase64() { - final bytes = encodeByteList(); - final encoded = base64Encode(bytes); - return encoded; - } - - List encodeByteList() { - final writer = ByteWriter(1024); - writer.strUtf8(name); - writer.strUtf8(author); - writer.uint32(colors.length); - for (var color in colors) { - writer.uint32(color.light.value); - writer.uint32(color.dark.value); - } - - return writer.build(); - } -} - -class BuiltinTimetablePalette implements TimetablePalette { - final int id; - final String key; - final String? nameOverride; - final String? authorOverride; - - @override - String get name => nameOverride ?? "timetable.p13n.builtinPalette.$key.name".tr(); - - @override - String get author => authorOverride ?? "timetable.p13n.builtinPalette.$key.author".tr(); - @override - final List colors; - - @override - DateTime? get lastModified => null; - - const BuiltinTimetablePalette({ - required this.key, - required this.id, - required this.colors, - String? name, - String? author, - }) : nameOverride = name, - authorOverride = author; - - @override - Map toJson() => { - "id": id, - "author": author, - "colors": _colorsToJson(colors), - }; -} diff --git a/lib/timetable/entity/platte.g.dart b/lib/timetable/entity/platte.g.dart deleted file mode 100644 index b971b7332..000000000 --- a/lib/timetable/entity/platte.g.dart +++ /dev/null @@ -1,21 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'platte.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -TimetablePalette _$TimetablePaletteFromJson(Map json) => TimetablePalette( - name: json['name'] as String, - author: json['author'] as String, - colors: _colorsFromJson(json['colors'] as List), - lastModified: json['lastModified'] == null ? null : DateTime.parse(json['lastModified'] as String), - ); - -Map _$TimetablePaletteToJson(TimetablePalette instance) => { - 'name': instance.name, - 'author': instance.author, - 'colors': _colorsToJson(instance.colors), - 'lastModified': instance.lastModified?.toIso8601String(), - }; diff --git a/lib/timetable/entity/pos.dart b/lib/timetable/entity/pos.dart deleted file mode 100644 index 53e4d4da6..000000000 --- a/lib/timetable/entity/pos.dart +++ /dev/null @@ -1,68 +0,0 @@ -import 'package:copy_with_extension/copy_with_extension.dart'; -import 'package:sit/l10n/time.dart'; - -import 'timetable.dart'; - -part "pos.g.dart"; - -@CopyWith(skipFields: true) -class TimetablePos { - /// starts with 0 - final int weekIndex; - - /// starts with 0 - final Weekday weekday; - - const TimetablePos({ - required this.weekIndex, - required this.weekday, - }); - - static const initial = TimetablePos(weekIndex: 0, weekday: Weekday.monday); - - static TimetablePos locate( - DateTime current, { - required DateTime relativeTo, - TimetablePos? fallback, - }) { - // calculate how many days have passed. - int totalDays = current.clearTime().difference(relativeTo.clearTime()).inDays; - - int week = totalDays ~/ 7 + 1; - int day = totalDays % 7 + 1; - if (totalDays >= 0 && 1 <= week && week <= 20 && 1 <= day && day <= 7) { - return TimetablePos(weekIndex: week - 1, weekday: Weekday.fromIndex(day - 1)); - } else { - // if out of range, fallback will be return. - return fallback ?? initial; - } - } - - @override - bool operator ==(Object other) { - return other is TimetablePos && - runtimeType == other.runtimeType && - weekIndex == other.weekIndex && - weekday == other.weekday; - } - - @override - int get hashCode => Object.hash(weekIndex, weekday); - - @override - String toString() { - return (week: weekIndex, day: weekday).toString(); - } -} - -extension _DateTimeX on DateTime { - DateTime clearTime([int hour = 0, int minute = 0, int second = 0]) { - return DateTime(year, month, day, hour, minute, second); - } -} - -extension TimetableX on SitTimetable { - TimetablePos locate(DateTime current) { - return TimetablePos.locate(current, relativeTo: startDate); - } -} diff --git a/lib/timetable/entity/pos.g.dart b/lib/timetable/entity/pos.g.dart deleted file mode 100644 index 86ffc4d17..000000000 --- a/lib/timetable/entity/pos.g.dart +++ /dev/null @@ -1,57 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'pos.dart'; - -// ************************************************************************** -// CopyWithGenerator -// ************************************************************************** - -abstract class _$TimetablePosCWProxy { - /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. - /// - /// Usage - /// ```dart - /// TimetablePos(...).copyWith(id: 12, name: "My name") - /// ```` - TimetablePos call({ - int? weekIndex, - Weekday? weekday, - }); -} - -/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfTimetablePos.copyWith(...)`. -class _$TimetablePosCWProxyImpl implements _$TimetablePosCWProxy { - const _$TimetablePosCWProxyImpl(this._value); - - final TimetablePos _value; - - @override - - /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. - /// - /// Usage - /// ```dart - /// TimetablePos(...).copyWith(id: 12, name: "My name") - /// ```` - TimetablePos call({ - Object? weekIndex = const $CopyWithPlaceholder(), - Object? weekday = const $CopyWithPlaceholder(), - }) { - return TimetablePos( - weekIndex: weekIndex == const $CopyWithPlaceholder() || weekIndex == null - ? _value.weekIndex - // ignore: cast_nullable_to_non_nullable - : weekIndex as int, - weekday: weekday == const $CopyWithPlaceholder() || weekday == null - ? _value.weekday - // ignore: cast_nullable_to_non_nullable - : weekday as Weekday, - ); - } -} - -extension $TimetablePosCopyWith on TimetablePos { - /// Returns a callable class that can be used as follows: `instanceOfTimetablePos.copyWith(...)`. - // ignore: library_private_types_in_public_api - _$TimetablePosCWProxy get copyWith => _$TimetablePosCWProxyImpl(this); -} diff --git a/lib/timetable/entity/timetable.dart b/lib/timetable/entity/timetable.dart deleted file mode 100644 index 973df4b4e..000000000 --- a/lib/timetable/entity/timetable.dart +++ /dev/null @@ -1,512 +0,0 @@ -import 'package:copy_with_extension/copy_with_extension.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:json_annotation/json_annotation.dart'; -import 'package:sit/entity/campus.dart'; -import 'package:sit/l10n/time.dart'; -import 'package:sit/school/entity/school.dart'; -import 'package:sit/school/entity/timetable.dart'; - -import '../utils.dart'; - -part 'timetable.g.dart'; - -@JsonSerializable() -@CopyWith(skipFields: true) -class SitTimetable { - @JsonKey() - final String name; - @JsonKey() - final DateTime startDate; - @JsonKey() - final int schoolYear; - @JsonKey() - final Semester semester; - @JsonKey() - final int lastCourseKey; - @JsonKey() - final String signature; - - /// The index is the CourseKey. - @JsonKey() - final Map courses; - - @JsonKey() - final int version; - - const SitTimetable({ - required this.courses, - required this.lastCourseKey, - required this.name, - required this.startDate, - required this.schoolYear, - required this.semester, - this.signature = "", - this.version = 1, - }); - - SitTimetableEntity resolve() { - return resolveTimetableEntity(this); - } - - @override - String toString() { - return { - "name": name, - "startDate": startDate, - "schoolYear": schoolYear, - "semester": semester, - "signature": signature, - }.toString(); - } - - factory SitTimetable.fromJson(Map json) => _$SitTimetableFromJson(json); - - Map toJson() => _$SitTimetableToJson(this); -} - -@JsonSerializable() -class SitCourse { - @JsonKey() - final int courseKey; - @JsonKey() - final String courseName; - @JsonKey() - final String courseCode; - @JsonKey() - final String classCode; - @JsonKey(unknownEnumValue: Campus.fengxian) - final Campus campus; - @JsonKey() - final String place; - - @JsonKey() - final TimetableWeekIndices weekIndices; - - /// e.g.: (start:1, end: 3) means `2nd slot to 4th slot`. - /// Starts with 0 - @JsonKey() - final ({int start, int end}) timeslots; - @JsonKey() - final double courseCredit; - - /// e.g.: `0` means `Monday` - /// Starts with 0 - @JsonKey() - final int dayIndex; - @JsonKey() - final List teachers; - - const SitCourse({ - required this.courseKey, - required this.courseName, - required this.courseCode, - required this.classCode, - required this.campus, - required this.place, - required this.weekIndices, - required this.timeslots, - required this.courseCredit, - required this.dayIndex, - required this.teachers, - }); - - @override - String toString() => "[$courseKey] $courseName"; - - factory SitCourse.fromJson(Map json) => _$SitCourseFromJson(json); - - Map toJson() => _$SitCourseToJson(this); -} - -extension SitCourseEx on SitCourse { - List get buildingTimetable => getTeachingBuildingTimetable(campus, place); - - /// Based on [SitCourse.timeslots], compose a full-length class time. - /// Starts with the first part starts. - /// Ends with the last part ends. - ClassTime calcBeginEndTimePoint() { - final timetable = buildingTimetable; - final (:start, :end) = timeslots; - return (begin: timetable[start].begin, end: timetable[end].end); - } - - List calcBeginEndTimePointForEachLesson() { - final timetable = buildingTimetable; - final (:start, :end) = timeslots; - final result = []; - for (var timeslot = start; timeslot <= end; timeslot++) { - result.add(timetable[timeslot]); - } - return result; - } - - ClassTime calcBeginEndTimePointOfLesson(int timeslot) { - final timetable = buildingTimetable; - return timetable[timeslot]; - } -} - -const _kAll = "a"; -const _kOdd = "o"; -const _kEven = "e"; - -@JsonEnum() -enum TimetableWeekIndexType { - all(_kAll), - odd(_kOdd), - even(_kEven); - - final String indicator; - - const TimetableWeekIndexType(this.indicator); - - String l10nOf(String start, String end) => "timetable.weekIndexType.$name".tr(namedArgs: { - "start": start, - "end": end, - }); - - static String l10nOfSingle(String index) => "timetable.weekIndexType.single".tr(args: [index]); - - static TimetableWeekIndexType of(String indicator) { - return switch (indicator) { - _kOdd => TimetableWeekIndexType.odd, - _kEven => TimetableWeekIndexType.even, - _ => TimetableWeekIndexType.all, - }; - } -} - -@JsonSerializable() -class TimetableWeekIndex { - @JsonKey() - final TimetableWeekIndexType type; - - /// Both [start] and [end] are inclusive. - /// [start] will equal to [end] if it's not ranged. - @JsonKey() - final ({int start, int end}) range; - - const TimetableWeekIndex({ - required this.type, - required this.range, - }); - - const TimetableWeekIndex.all( - this.range, - ) : type = TimetableWeekIndexType.all; - - /// [start] will equal to [end]. - const TimetableWeekIndex.single( - int weekIndex, - ) : type = TimetableWeekIndexType.all, - range = (start: weekIndex, end: weekIndex); - - const TimetableWeekIndex.odd( - this.range, - ) : type = TimetableWeekIndexType.odd; - - const TimetableWeekIndex.even( - this.range, - ) : type = TimetableWeekIndexType.even; - - /// week number start by - bool match(int weekIndex) { - return range.start <= weekIndex && weekIndex <= range.end; - } - - bool get isSingle => range.start == range.end; - - /// convert the index to number. - /// e.g.: (start: 0, end: 8) => "1–9" - String l10n() { - if (isSingle) { - return TimetableWeekIndexType.l10nOfSingle("${range.start + 1}"); - } else { - return type.l10nOf("${range.start + 1}", "${range.end + 1}"); - } - } - - factory TimetableWeekIndex.fromJson(Map json) => _$TimetableWeekIndexFromJson(json); - - Map toJson() => _$TimetableWeekIndexToJson(this); -} - -@JsonSerializable() -class TimetableWeekIndices { - @JsonKey() - final List indices; - - const TimetableWeekIndices(this.indices); - - bool match(int weekIndex) { - for (final index in indices) { - if (index.match(weekIndex)) return true; - } - return false; - } - - /// Then the [indices] could be ["a1-5", "s14", "o8-10"] - /// The return value should be: - /// - `1-5 周, 14 周, 8-10 单周` in Chinese. - /// - `1-5 wk, 14 wk, 8-10 odd wk` - List l10n() { - return indices.map((index) => index.l10n()).toList(); - } - - /// The result, week index, which starts with 0. - /// e.g.: - /// ```dart - /// TimetableWeekIndices([ - /// TimetableWeekIndex.all( - /// (start: 0, end: 4), - /// ), - /// TimetableWeekIndex.single( - /// 13, - /// ), - /// TimetableWeekIndex.odd( - /// (start: 7, end: 9), - /// ), - /// ]) - /// ``` - /// return value is {0,1,2,3,4,13,7,9}. - Set getWeekIndices() { - final res = {}; - for (final TimetableWeekIndex(:type, :range) in indices) { - switch (type) { - case TimetableWeekIndexType.all: - for (var i = range.start; i <= range.end; i++) { - res.add(i); - } - break; - case TimetableWeekIndexType.odd: - for (var i = range.start; i <= range.end; i += 2) { - if ((i + 1).isOdd) res.add(i); - } - break; - case TimetableWeekIndexType.even: - for (var i = range.start; i <= range.end; i++) { - if ((i + 1).isEven) res.add(i); - } - break; - } - } - return res; - } - - factory TimetableWeekIndices.fromJson(Map json) => _$TimetableWeekIndicesFromJson(json); - - Map toJson() => _$TimetableWeekIndicesToJson(this); -} - -/// If [range] is "1-8", the output will be `(start:0, end: 7)`. -/// if [number2index] is true, the [range] will be considered as a number range, which starts with 1 instead of 0. -({int start, int end}) rangeFromString( - String range, { - bool number2index = false, -}) { - if (range.contains("-")) { -// in range of time slots - final rangeParts = range.split("-"); - final start = int.parse(rangeParts[0]); - final end = int.parse(rangeParts[1]); - if (number2index) { - return (start: start - 1, end: end - 1); - } else { - return (start: start, end: end); - } - } else { - final single = int.parse(range); - if (number2index) { - return (start: single - 1, end: single - 1); - } else { - return (start: single, end: single); - } - } -} - -String rangeToString(({int start, int end}) range) { - if (range.start == range.end) { - return "${range.start}"; - } else { - return "${range.start}-${range.end}"; - } -} - -class SitTimetableEntity { - final SitTimetable type; - - /// The Default number of weeks is 20. - final List weeks; - - final Map> _code2CoursesCache = {}; - - SitTimetableEntity({ - required this.type, - required this.weeks, - }); - - List findAndCacheCoursesByCourseCode(String courseCode) { - final found = _code2CoursesCache[courseCode]; - if (found != null) { - return found; - } else { - final res = []; - for (final course in type.courses.values) { - if (course.courseCode == courseCode) { - res.add(course); - } - } - _code2CoursesCache[courseCode] = res; - return res; - } - } - - String get name => type.name; - - DateTime get startDate => type.startDate; - - int get schoolYear => type.schoolYear; - - Semester get semester => type.semester; - - String get signature => type.signature; -} - -class SitTimetableWeek { - final int index; - - /// The 7 days in a week - final List days; - - SitTimetableWeek({ - required this.index, - required this.days, - }); - - factory SitTimetableWeek.$7days(int weekIndex) { - return SitTimetableWeek( - index: weekIndex, - days: List.generate(7, (index) => SitTimetableDay.$11slots(index)), - ); - } - - bool isFree() { - return days.every((day) => day.isFree()); - } - - @override - String toString() => "$days"; - - SitTimetableDay operator [](Weekday weekday) => days[weekday.index]; - - operator []=(Weekday weekday, SitTimetableDay day) => days[weekday.index] = day; -} - -/// Lessons in the same Timeslot. -class SitTimetableLessonSlot { - final List lessons; - - SitTimetableLessonSlot({required this.lessons}); -} - -class SitTimetableDay { - final int index; - - /// The Default number of lesson in one day is 11. But the length of lessons can be more. - /// When two lessons are overlapped, it can be 12+. - /// A Timeslot contain one or more lesson. - final List timeslot2LessonSlot; - - SitTimetableDay({ - required this.index, - required this.timeslot2LessonSlot, - }); - - factory SitTimetableDay.$11slots(int dayIndex) { - return SitTimetableDay( - index: dayIndex, - timeslot2LessonSlot: List.generate(11, (index) => SitTimetableLessonSlot(lessons: [])), - ); - } - - bool isFree() { - return timeslot2LessonSlot.every((lessonSlot) => lessonSlot.lessons.isEmpty); - } - - void add({required SitTimetableLessonPart lesson, required int at}) { - assert(0 <= at && at < timeslot2LessonSlot.length); - if (0 <= at && at < timeslot2LessonSlot.length) { - final lessonSlot = timeslot2LessonSlot[at]; - lessonSlot.lessons.add(lesson); - } - } - - /// At all lessons [layer] - Iterable browseLessonsAt({required int layer}) sync* { - for (final lessonSlot in timeslot2LessonSlot) { - if (0 <= layer && layer < lessonSlot.lessons.length) { - yield lessonSlot.lessons[layer]; - } - } - } - - bool hasAnyLesson() { - for (final lessonSlot in timeslot2LessonSlot) { - if (lessonSlot.lessons.isNotEmpty) { - return true; - } - } - return false; - } - - @override - String toString() => "$timeslot2LessonSlot"; -} - -class SitTimetableLesson { - /// The start index of this lesson in a [SitTimetableWeek] - final int startIndex; - - /// The end index of this lesson in a [SitTimetableWeek] - final int endIndex; - final DateTime startTime; - final DateTime endTime; - - /// A lesson may last two or more time slots. - /// If current [SitTimetableLessonPart] is a part of the whole lesson, they all have the same [courseKey]. - final SitCourse course; - - /// How many timeslots this lesson takes. - /// It's at least 1 timeslot. - int get timeslotDuration => endIndex - startIndex + 1; - - SitTimetableLesson({ - required this.course, - required this.startIndex, - required this.endIndex, - required this.startTime, - required this.endTime, - }); -} - -class SitTimetableLessonPart { - final SitTimetableLesson type; - - /// The start index of this lesson in a [SitTimetableWeek] - final int index; - - final DateTime startTime; - final DateTime endTime; - - SitCourse get course => type.course; - - const SitTimetableLessonPart({ - required this.type, - required this.index, - required this.startTime, - required this.endTime, - }); - - @override - String toString() => "$course at $index"; -} diff --git a/lib/timetable/entity/timetable.g.dart b/lib/timetable/entity/timetable.g.dart deleted file mode 100644 index 27ab5d950..000000000 --- a/lib/timetable/entity/timetable.g.dart +++ /dev/null @@ -1,208 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'timetable.dart'; - -// ************************************************************************** -// CopyWithGenerator -// ************************************************************************** - -abstract class _$SitTimetableCWProxy { - /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. - /// - /// Usage - /// ```dart - /// SitTimetable(...).copyWith(id: 12, name: "My name") - /// ```` - SitTimetable call({ - Map? courses, - int? lastCourseKey, - String? name, - DateTime? startDate, - int? schoolYear, - Semester? semester, - String? signature, - int? version, - }); -} - -/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfSitTimetable.copyWith(...)`. -class _$SitTimetableCWProxyImpl implements _$SitTimetableCWProxy { - const _$SitTimetableCWProxyImpl(this._value); - - final SitTimetable _value; - - @override - - /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. - /// - /// Usage - /// ```dart - /// SitTimetable(...).copyWith(id: 12, name: "My name") - /// ```` - SitTimetable call({ - Object? courses = const $CopyWithPlaceholder(), - Object? lastCourseKey = const $CopyWithPlaceholder(), - Object? name = const $CopyWithPlaceholder(), - Object? startDate = const $CopyWithPlaceholder(), - Object? schoolYear = const $CopyWithPlaceholder(), - Object? semester = const $CopyWithPlaceholder(), - Object? signature = const $CopyWithPlaceholder(), - Object? version = const $CopyWithPlaceholder(), - }) { - return SitTimetable( - courses: courses == const $CopyWithPlaceholder() || courses == null - ? _value.courses - // ignore: cast_nullable_to_non_nullable - : courses as Map, - lastCourseKey: lastCourseKey == const $CopyWithPlaceholder() || lastCourseKey == null - ? _value.lastCourseKey - // ignore: cast_nullable_to_non_nullable - : lastCourseKey as int, - name: name == const $CopyWithPlaceholder() || name == null - ? _value.name - // ignore: cast_nullable_to_non_nullable - : name as String, - startDate: startDate == const $CopyWithPlaceholder() || startDate == null - ? _value.startDate - // ignore: cast_nullable_to_non_nullable - : startDate as DateTime, - schoolYear: schoolYear == const $CopyWithPlaceholder() || schoolYear == null - ? _value.schoolYear - // ignore: cast_nullable_to_non_nullable - : schoolYear as int, - semester: semester == const $CopyWithPlaceholder() || semester == null - ? _value.semester - // ignore: cast_nullable_to_non_nullable - : semester as Semester, - signature: signature == const $CopyWithPlaceholder() || signature == null - ? _value.signature - // ignore: cast_nullable_to_non_nullable - : signature as String, - version: version == const $CopyWithPlaceholder() || version == null - ? _value.version - // ignore: cast_nullable_to_non_nullable - : version as int, - ); - } -} - -extension $SitTimetableCopyWith on SitTimetable { - /// Returns a callable class that can be used as follows: `instanceOfSitTimetable.copyWith(...)`. - // ignore: library_private_types_in_public_api - _$SitTimetableCWProxy get copyWith => _$SitTimetableCWProxyImpl(this); -} - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -SitTimetable _$SitTimetableFromJson(Map json) => SitTimetable( - courses: (json['courses'] as Map).map( - (k, e) => MapEntry(k, SitCourse.fromJson(e as Map)), - ), - lastCourseKey: json['lastCourseKey'] as int, - name: json['name'] as String, - startDate: DateTime.parse(json['startDate'] as String), - schoolYear: json['schoolYear'] as int, - semester: $enumDecode(_$SemesterEnumMap, json['semester']), - signature: json['signature'] as String? ?? "", - version: json['version'] as int? ?? 1, - ); - -Map _$SitTimetableToJson(SitTimetable instance) => { - 'name': instance.name, - 'startDate': instance.startDate.toIso8601String(), - 'schoolYear': instance.schoolYear, - 'semester': _$SemesterEnumMap[instance.semester]!, - 'lastCourseKey': instance.lastCourseKey, - 'signature': instance.signature, - 'courses': instance.courses, - 'version': instance.version, - }; - -const _$SemesterEnumMap = { - Semester.all: 'all', - Semester.term1: 'term1', - Semester.term2: 'term2', -}; - -SitCourse _$SitCourseFromJson(Map json) => SitCourse( - courseKey: json['courseKey'] as int, - courseName: json['courseName'] as String, - courseCode: json['courseCode'] as String, - classCode: json['classCode'] as String, - campus: $enumDecode(_$CampusEnumMap, json['campus'], unknownValue: Campus.fengxian), - place: json['place'] as String, - weekIndices: TimetableWeekIndices.fromJson(json['weekIndices'] as Map), - timeslots: _$recordConvert( - json['timeslots'], - ($jsonValue) => ( - end: $jsonValue['end'] as int, - start: $jsonValue['start'] as int, - ), - ), - courseCredit: (json['courseCredit'] as num).toDouble(), - dayIndex: json['dayIndex'] as int, - teachers: (json['teachers'] as List).map((e) => e as String).toList(), - ); - -Map _$SitCourseToJson(SitCourse instance) => { - 'courseKey': instance.courseKey, - 'courseName': instance.courseName, - 'courseCode': instance.courseCode, - 'classCode': instance.classCode, - 'campus': _$CampusEnumMap[instance.campus]!, - 'place': instance.place, - 'weekIndices': instance.weekIndices, - 'timeslots': { - 'end': instance.timeslots.end, - 'start': instance.timeslots.start, - }, - 'courseCredit': instance.courseCredit, - 'dayIndex': instance.dayIndex, - 'teachers': instance.teachers, - }; - -const _$CampusEnumMap = { - Campus.fengxian: 'fengxian', - Campus.xuhui: 'xuhui', -}; - -$Rec _$recordConvert<$Rec>( - Object? value, - $Rec Function(Map) convert, -) => - convert(value as Map); - -TimetableWeekIndex _$TimetableWeekIndexFromJson(Map json) => TimetableWeekIndex( - type: $enumDecode(_$TimetableWeekIndexTypeEnumMap, json['type']), - range: _$recordConvert( - json['range'], - ($jsonValue) => ( - end: $jsonValue['end'] as int, - start: $jsonValue['start'] as int, - ), - ), - ); - -Map _$TimetableWeekIndexToJson(TimetableWeekIndex instance) => { - 'type': _$TimetableWeekIndexTypeEnumMap[instance.type]!, - 'range': { - 'end': instance.range.end, - 'start': instance.range.start, - }, - }; - -const _$TimetableWeekIndexTypeEnumMap = { - TimetableWeekIndexType.all: 'all', - TimetableWeekIndexType.odd: 'odd', - TimetableWeekIndexType.even: 'even', -}; - -TimetableWeekIndices _$TimetableWeekIndicesFromJson(Map json) => TimetableWeekIndices( - (json['indices'] as List).map((e) => TimetableWeekIndex.fromJson(e as Map)).toList(), - ); - -Map _$TimetableWeekIndicesToJson(TimetableWeekIndices instance) => { - 'indices': instance.indices, - }; diff --git a/lib/timetable/events.dart b/lib/timetable/events.dart deleted file mode 100644 index 17e90eaca..000000000 --- a/lib/timetable/events.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:event_bus/event_bus.dart'; - -import 'entity/pos.dart'; - -final eventBus = EventBus(); - -class JumpToPosEvent { - final TimetablePos where; - - const JumpToPosEvent(this.where); -} diff --git a/lib/timetable/i18n.dart b/lib/timetable/i18n.dart deleted file mode 100644 index 3873b092e..000000000 --- a/lib/timetable/i18n.dart +++ /dev/null @@ -1,293 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:sit/l10n/common.dart'; -import 'package:sit/school/i18n.dart'; - -const i18n = _I18n(); - -class _I18n with CommonI18nMixin { - const _I18n(); - - static const ns = "timetable"; - final time = const TimeI18n(); - final mine = const _Mine(); - final p13n = const _P13n(); - final import = const _Import(); - final course = const CourseI18n(); - final export = const _Export(); - final screenshot = const _Screenshot(); - final editor = const _Editor(); - final freeTip = const _FreeTip(); - final campus = const CampusI10n(); - - String get navigation => "$ns.navigation".tr(); - - String weekOrderedName({required int number}) => "$ns.weekOrderedName".tr(args: [number.toString()]); - - String get startWith => "$ns.startWith".tr(); - - String get jump => "$ns.jump".tr(); - - String get findToday => "$ns.findToday".tr(); - - String get focusTimetable => "$ns.focusTimetable".tr(); - - String get signature => "$ns.signature".tr(); - - String get signaturePlaceholder => "$ns.signaturePlaceholder".tr(); -} - -class _Mine { - const _Mine(); - - static const ns = "${_I18n.ns}.mine"; - - String get title => "$ns.title".tr(); - - String get exportFile => "$ns.exportFile".tr(); - - String get exportCalendar => "$ns.exportCalendar".tr(); - - String get add2Calendar => "$ns.add2Calendar".tr(); - - String get deleteRequest => "$ns.deleteRequest".tr(); - - String get deleteRequestDesc => "$ns.deleteRequestDesc".tr(); - - String get emptyTip => "$ns.emptyTip".tr(); - - String get details => "$ns.details".tr(); -} - -class _P13n { - const _P13n(); - - static const ns = "${_I18n.ns}.p13n"; - final cell = const _CellStyle(); - final palette = const _Palette(); - final background = const _Background(); - - String get title => "$ns.title".tr(); - - ({String name, String place, List teachers}) livePreview(int index) { - return ( - name: "$ns.livePreview.$index.name".tr(), - place: "$ns.livePreview.$index.place".tr(), - teachers: "$ns.livePreview.$index.teachers".tr().split(","), - ); - } -} - -class _CellStyle { - const _CellStyle(); - - static const ns = "${_P13n.ns}.cellStyle"; - - String get title => "$ns.title".tr(); - - String get entrance => "$ns.entrance.title".tr(); - - String get entranceDesc => "$ns.entrance.desc".tr(); - - String get showTeachers => "$ns.showTeachers.title".tr(); - - String get showTeachersDesc => "$ns.showTeachers.desc".tr(); - - String get grayOut => "$ns.grayOut.title".tr(); - - String get grayOutDesc => "$ns.grayOut.desc".tr(); - - String get harmonize => "$ns.harmonize.title".tr(); - - String get harmonizeDesc => "$ns.harmonize.desc".tr(); - - String get alpha => "$ns.alpha".tr(); -} - -class _Palette { - const _Palette(); - - static const ns = "${_P13n.ns}.palette"; - - String get title => "$ns.title".tr(); - - String get fab => "$ns.fab".tr(); - - String get customTab => "$ns.tab.custom".tr(); - - String get builtinTab => "$ns.tab.builtin".tr(); - - String get infoTab => "$ns.tab.info".tr(); - - String get colorsTab => "$ns.tab.colors".tr(); - - String get shareQrCode => "$ns.shareQrCode".tr(); - - String get newPaletteName => "$ns.newPaletteName".tr(); - - String copyPaletteName(String old) => "$ns.copyPaletteName".tr(args: [old]); - - String get deleteRequest => "$ns.deleteRequest".tr(); - - String get deleteRequestDesc => "$ns.deleteRequestDesc".tr(); - - String get addFromQrCode => "$ns.addFromQrCode".tr(); - - String get addColor => "$ns.addColor".tr(); - - String get name => "$ns.name".tr(); - - String get namePlaceholder => "$ns.namePlaceholder".tr(); - - String get author => "$ns.author".tr(); - - String get authorPlaceholder => "$ns.authorPlaceholder".tr(); - - String get color => "$ns.color".tr(); - - String get details => "$ns.details".tr(); -} - -class _Background { - const _Background(); - - static const ns = "${_P13n.ns}.background"; - - String get title => "$ns.title".tr(); - - String get pickTip => "$ns.pickTip".tr(); - - String get opacity => "$ns.opacity".tr(); - - String get repeat => "$ns.repeat.title".tr(); - - String get repeatDesc => "$ns.repeat.desc".tr(); - - String get antialias => "$ns.antialias.title".tr(); - - String get antialiasDesc => "$ns.antialias.desc".tr(); -} - -class _Screenshot { - const _Screenshot(); - - static const ns = "${_I18n.ns}.screenshot"; - - String get title => "$ns.title".tr(); - - String get screenshot => "$ns.screenshot".tr(); - - String get take => "$ns.take".tr(); - - String get enableBackground => "$ns.enableBackground.title".tr(); - - String get enableBackgroundDesc => "$ns.enableBackground.desc".tr(); -} - -class _Import { - const _Import(); - - static const ns = "${_I18n.ns}.import"; - - String get title => "$ns.title".tr(); - - String get import => "$ns.import".tr(); - - String get fromFile => "$ns.fromFile".tr(); - - String get fromFileBtn => "$ns.fromFileBtn".tr(); - - String get connectivityCheckerDesc => "$ns.connectivityCheckerDesc".tr(); - - String get selectSemesterTip => "$ns.selectSemesterTip".tr(); - - String get endTip => "$ns.endTip".tr(); - - String get failed => "$ns.failed".tr(); - - String get failedDesc => "$ns.failedDesc".tr(); - - String get failedTip => "$ns.failedTip".tr(); - - String get tryImportBtn => "$ns.tryImportBtn".tr(); - - String get importing => "$ns.importing".tr(); - - String get timetableInfo => "$ns.timetableInfo".tr(); - - String defaultName( - String semester, - String yearStart, - String yearEnd, - ) => - "$ns.defaultName".tr(namedArgs: { - "semester": semester, - "yearStart": yearStart, - "yearEnd": yearEnd, - }); -} - -class _Editor { - const _Editor(); - - static const ns = "${_I18n.ns}.edit"; - - String get name => "$ns.name".tr(); -} - -class _Export { - const _Export(); - - static const ns = "${_I18n.ns}.export"; - - String get title => "$ns.title".tr(); - - String get export => "$ns.export".tr(); - - String get lessonMode => "$ns.lessonMode.title".tr(); - - String get lessonModeMerged => "$ns.lessonMode.merged".tr(); - - String get lessonModeMergedTip => "$ns.lessonMode.mergedTip".tr(); - - String get lessonModeSeparate => "$ns.lessonMode.separate".tr(); - - String get lessonModeSeparateTip => "$ns.lessonMode.separateTip".tr(); - - String get enableAlarm => "$ns.enableAlarm.title".tr(); - - String get enableAlarmDesc => "$ns.enableAlarm.desc".tr(); - - String get alarmMode => "$ns.alarmMode.title".tr(); - - String get alarmModeSound => "$ns.alarmMode.sound".tr(); - - String get alarmModeDisplay => "$ns.alarmMode.display".tr(); - - String get alarmDuration => "$ns.alarmDuration".tr(); - - String get alarmBeforeClassBegins => "$ns.alarmBeforeClassBegins.title".tr(); - - String alarmBeforeClassBeginsDesc(Duration duration) => "$ns.alarmBeforeClassBegins.desc".tr(namedArgs: { - "duration": i18n.time.minuteFormat(duration.inMinutes.toString()), - }); -} - -class _FreeTip { - const _FreeTip(); - - static const ns = "${_I18n.ns}.freeTip"; - - String get dayTip => "$ns.dayTip".tr(); - - String get isTodayTip => "$ns.isTodayTip".tr(); - - String get weekTip => "$ns.weekTip".tr(); - - String get isThisWeekTip => "$ns.isThisWeekTip".tr(); - - String get termTip => "$ns.termTip".tr(); - - String get findNearestWeekWithClass => "$ns.findNearestWeekWithClass".tr(); - - String get findNearestDayWithClass => "$ns.findNearestDayWithClass".tr(); -} diff --git a/lib/timetable/init.dart b/lib/timetable/init.dart deleted file mode 100644 index 831fb6290..000000000 --- a/lib/timetable/init.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'service/school.dart'; -import 'storage/timetable.dart'; - -class TimetableInit { - static late TimetableService service; - static late TimetableStorage storage; - - static void init() { - service = const TimetableService(); - storage = TimetableStorage(); - } -} diff --git a/lib/timetable/page/background.dart b/lib/timetable/page/background.dart deleted file mode 100644 index 976925e71..000000000 --- a/lib/timetable/page/background.dart +++ /dev/null @@ -1,193 +0,0 @@ -import 'dart:io'; - -import 'package:flutter/material.dart'; -import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; -import 'package:go_router/go_router.dart'; -import 'package:image_picker/image_picker.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:sit/design/adaptive/foundation.dart'; -import 'package:sit/design/widgets/card.dart'; -import 'package:sit/design/widgets/common.dart'; -import 'package:sit/files.dart'; -import 'package:sit/settings/settings.dart'; -import 'package:sit/timetable/entity/background.dart'; -import "../i18n.dart"; - -class TimetableBackgroundEditor extends StatefulWidget { - const TimetableBackgroundEditor({super.key}); - - @override - State createState() => _TimetableBackgroundEditorState(); -} - -class _TimetableBackgroundEditorState extends State with SingleTickerProviderStateMixin { - var background = Settings.timetable.backgroundImage ?? const BackgroundImage.disabled(); - late final AnimationController $opacity; - - @override - void initState() { - super.initState(); - $opacity = AnimationController(vsync: this, value: background.opacity); - } - - @override - void dispose() { - $opacity.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final old = Settings.timetable.backgroundImage ?? const BackgroundImage.disabled(); - final background = this.background; - return Scaffold( - body: CustomScrollView( - slivers: [ - SliverAppBar( - floating: true, - title: i18n.p13n.background.title.text(), - actions: [ - if (background.enabled) - PlatformTextButton( - onPressed: () async { - setState(() { - this.background = background.copyWith(path: ""); - }); - }, - child: i18n.delete.text(style: TextStyle(color: context.$red$)), - ), - if (background != old) - PlatformTextButton( - child: i18n.save.text(), - onPressed: () async { - if (background.enabled) { - final backgroundFi = await File(background.path).copy(Files.timetable.backgroundFile.path); - await FileImage(backgroundFi).evict(); - } - Settings.timetable.backgroundImage = background; - if (!mounted) return; - context.pop(background); - }, - ), - ], - ), - SliverList.list(children: [ - buildImage(background), - buildOpacity(background), - buildRepeat(background), - buildAntialias(background), - ]), - ], - ), - ); - } - - Widget buildImage(BackgroundImage bk) { - return OutlinedCard( - clip: Clip.hardEdge, - child: buildPreviewBoxContent(bk).inkWell( - onTap: pickImage, - ), - ).padH(10); - } - - Widget buildPreviewBoxContent(BackgroundImage bk) { - if (background.enabled) { - return InteractiveViewer( - child: Image.file( - File(bk.path), - opacity: $opacity, - height: context.mediaQuery.size.height / 3, - filterQuality: bk.antialias ? FilterQuality.low : FilterQuality.none, - ), - ); - } else { - return LeavingBlank( - icon: Icons.add_photo_alternate_outlined, - desc: i18n.p13n.background.pickTip, - ); - } - } - - Future pickImage() async { - final picker = ImagePicker(); - final XFile? fi = await picker.pickImage( - source: ImageSource.gallery, - requestFullMetadata: false, - ); - if (fi == null) return; - final newBk = background.copyWith(path: fi.path); - if (!mounted) return; - setState(() { - background = newBk; - }); - setOpacity(newBk.opacity); - } - - void setOpacity(double newValue) { - final background = this.background; - if ((background.opacity - newValue).abs() > 0.1) { - $opacity.animateTo( - newValue, - duration: const Duration(milliseconds: 300), - ); - } else { - $opacity.value = newValue; - } - } - - Widget buildOpacity(BackgroundImage bk) { - final value = bk.opacity; - return ListTile( - isThreeLine: true, - leading: const Icon(Icons.invert_colors), - title: i18n.p13n.background.opacity.text(), - trailing: "${(value * 100).toInt()}%".toString().text(), - subtitle: Slider( - min: 0.0, - max: 1.0, - divisions: 255, - label: (value * 255).toInt().toString(), - value: value, - onChanged: (double value) { - setState(() { - background = bk.copyWith(opacity: value); - }); - setOpacity(value); - }, - ), - ); - } - - Widget buildRepeat(BackgroundImage bk) { - return ListTile( - leading: const Icon(Icons.repeat), - title: i18n.p13n.background.repeat.text(), - subtitle: i18n.p13n.background.repeatDesc.text(), - trailing: Switch.adaptive( - value: bk.repeat, - onChanged: (newV) { - setState(() { - background = bk.copyWith(repeat: newV); - }); - }, - ), - ); - } - - Widget buildAntialias(BackgroundImage bk) { - return ListTile( - leading: const Icon(Icons.landscape), - title: i18n.p13n.background.antialias.text(), - subtitle: i18n.p13n.background.antialiasDesc.text(), - trailing: Switch.adaptive( - value: bk.antialias, - onChanged: (newV) { - setState(() { - background = bk.copyWith(antialias: newV); - }); - }, - ), - ); - } -} diff --git a/lib/timetable/page/cell_style.dart b/lib/timetable/page/cell_style.dart deleted file mode 100644 index 99a9b0475..000000000 --- a/lib/timetable/page/cell_style.dart +++ /dev/null @@ -1,138 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; -import 'package:go_router/go_router.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:sit/settings/settings.dart'; - -import '../widgets/style.dart'; -import '../i18n.dart'; -import 'p13n.dart'; - -class TimetableCellStyleEditor extends StatefulWidget { - const TimetableCellStyleEditor({super.key}); - - @override - State createState() => _TimetableCellStyleEditorState(); -} - -class _TimetableCellStyleEditorState extends State { - var showTeachers = Settings.timetable.cell.showTeachers; - var grayOutTakenLessons = Settings.timetable.cell.grayOutTakenLessons; - var harmonizeWithThemeColor = Settings.timetable.cell.harmonizeWithThemeColor; - var alpha = Settings.timetable.cell.alpha; - - @override - Widget build(BuildContext context) { - return Scaffold( - body: CustomScrollView( - slivers: [ - SliverAppBar( - floating: true, - title: i18n.p13n.cell.title.text(), - actions: [ - PlatformTextButton( - child: i18n.save.text(), - onPressed: () async { - final cellStyle = buildCellStyle(); - Settings.timetable.cell.cellStyle = cellStyle; - context.pop(cellStyle); - }, - ), - ], - ), - SliverToBoxAdapter( - child: TimetableStyleProv( - cellStyle: buildCellStyle(), - child: const TimetableP13nLivePreview(), - ), - ), - SliverList.list(children: [ - buildTeachersToggle(), - buildGrayOutPassedLesson(), - buildHarmonizeWithThemeColor(), - buildAlpha(), - ]), - ], - ), - ); - } - - CourseCellStyle buildCellStyle() { - return CourseCellStyle( - showTeachers: showTeachers, - grayOutTakenLessons: grayOutTakenLessons, - harmonizeWithThemeColor: harmonizeWithThemeColor, - alpha: alpha, - ); - } - - Widget buildTeachersToggle() { - return ListTile( - leading: const Icon(Icons.person_pin), - title: i18n.p13n.cell.showTeachers.text(), - subtitle: i18n.p13n.cell.showTeachersDesc.text(), - trailing: Switch.adaptive( - value: showTeachers, - onChanged: (newV) { - setState(() { - showTeachers = newV; - }); - }, - ), - ); - } - - Widget buildGrayOutPassedLesson() { - return ListTile( - leading: const Icon(Icons.timelapse), - title: i18n.p13n.cell.grayOut.text(), - subtitle: i18n.p13n.cell.grayOutDesc.text(), - trailing: Switch.adaptive( - value: grayOutTakenLessons, - onChanged: (newV) { - setState(() { - grayOutTakenLessons = newV; - }); - }, - ), - ); - } - - Widget buildHarmonizeWithThemeColor() { - return ListTile( - leading: const Icon(Icons.format_color_fill), - title: i18n.p13n.cell.harmonize.text(), - subtitle: i18n.p13n.cell.harmonizeDesc.text(), - trailing: Switch.adaptive( - value: harmonizeWithThemeColor, - onChanged: (newV) { - setState(() { - harmonizeWithThemeColor = newV; - }); - }, - ), - ); - } - - Widget buildAlpha() { - final value = alpha; - return ListTile( - isThreeLine: true, - leading: const Icon(Icons.invert_colors), - title: i18n.p13n.cell.alpha.text(), - trailing: "${(value * 100).toInt()}%".toString().text(), - subtitle: Slider( - min: 0.0, - max: 1.0, - divisions: 255, - label: (value * 255).toInt().toString(), - value: value, - onChanged: (double value) { - setState(() { - alpha = value; - }); - }, - ), - ); - } -} diff --git a/lib/timetable/page/details.dart b/lib/timetable/page/details.dart deleted file mode 100644 index 92b95499d..000000000 --- a/lib/timetable/page/details.dart +++ /dev/null @@ -1,103 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:sit/l10n/time.dart'; -import 'package:text_scroll/text_scroll.dart'; - -import '../i18n.dart'; -import '../entity/timetable.dart'; - -class TimetableCourseDetailsSheet extends StatelessWidget { - final String courseCode; - final SitTimetableEntity timetable; - - /// 一门课可能包括实践和理论课. 由于正方不支持这种设置, 实际教务系统在处理中会把这两部分拆开, 但是它们的课程名称和课程代码是一样的 - /// classes 中存放的就是对应的所有课程, 我们在这把它称为班级. - const TimetableCourseDetailsSheet({ - super.key, - required this.courseCode, - required this.timetable, - }); - - @override - Widget build(BuildContext context) { - final courses = timetable.findAndCacheCoursesByCourseCode(courseCode); - return Scaffold( - body: CustomScrollView( - slivers: [ - SliverAppBar( - floating: true, - title: TextScroll( - courses[0].courseName, - ), - ), - SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 12), - sliver: SliverToBoxAdapter( - child: buildTable(courses), - ), - ), - const SliverToBoxAdapter( - child: Divider(), - ), - SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 12), - sliver: SliverList.builder( - itemCount: courses.length, - itemBuilder: (ctx, i) { - final course = courses[i]; - return CourseDescCard(course); - }, - ), - ), - ], - ), - ); - } - - Widget buildTable(List courses) { - return Table( - columnWidths: const { - 0: FlexColumnWidth(1), - 1: FlexColumnWidth(2), - }, - children: [ - TableRow(children: [ - i18n.course.courseCode.text(), - courseCode.text(), - ]), - TableRow(children: [ - i18n.course.classCode.text(), - courses[0].classCode.text(), - ]), - TableRow(children: [ - i18n.course.teacher.text(), - courses.expand((course) => course.teachers).toSet().join(", ").text(), - ]), - ], - ); - } -} - -class CourseDescCard extends StatelessWidget { - final SitCourse course; - - const CourseDescCard( - this.course, { - super.key, - }); - - @override - Widget build(BuildContext context) { - final weekNumbers = course.weekIndices.l10n(); - final (:begin, :end) = course.calcBeginEndTimePoint(); - return ListTile( - title: course.place.text(), - subtitle: [ - "${Weekday.fromIndex(course.dayIndex).l10n()} ${begin.l10n(context)}–${end.l10n(context)}".text(), - if (course.teachers.length > 1) course.teachers.join(", ").text(), - ...weekNumbers.map((n) => n.text()), - ].column(mas: MainAxisSize.min, caa: CrossAxisAlignment.start), - trailing: course.teachers.length == 1 ? course.teachers.first.text() : null, - ); - } -} diff --git a/lib/timetable/page/editor.dart b/lib/timetable/page/editor.dart deleted file mode 100644 index 7683975c3..000000000 --- a/lib/timetable/page/editor.dart +++ /dev/null @@ -1,135 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; -import 'package:go_router/go_router.dart'; -import 'package:sit/l10n/extension.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:sit/settings/settings.dart'; - -import '../entity/timetable.dart'; -import '../i18n.dart'; - -class TimetableEditor extends StatefulWidget { - final SitTimetable timetable; - - const TimetableEditor({super.key, required this.timetable}); - - @override - State createState() => _TimetableEditorState(); -} - -class _TimetableEditorState extends State { - final _formKey = GlobalKey(); - late final $name = TextEditingController(text: widget.timetable.name); - late final $selectedDate = ValueNotifier(widget.timetable.startDate); - late final $signature = TextEditingController(text: widget.timetable.signature); - - @override - void dispose() { - $name.dispose(); - $selectedDate.dispose(); - $signature.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - resizeToAvoidBottomInset: false, - body: CustomScrollView( - slivers: [ - SliverAppBar( - floating: true, - title: i18n.import.timetableInfo.text(), - actions: [ - buildSaveAction(), - ], - ), - SliverList.list(children: [ - buildDescForm(), - buildStartDate(), - buildSignature(), - ]) - ], - ), - ); - } - - Widget buildStartDate() { - return ListTile( - leading: const Icon(Icons.alarm), - title: i18n.startWith.text(), - trailing: FilledButton( - child: $selectedDate >> (ctx, value) => ctx.formatYmdText(value).text(), - onPressed: () async { - final date = await _pickTimetableStartDate(context, initial: $selectedDate.value); - if (date != null) { - $selectedDate.value = DateTime(date.year, date.month, date.day); - } - }, - ), - ); - } - - Widget buildSignature() { - return ListTile( - isThreeLine: true, - leading: const Icon(Icons.drive_file_rename_outline), - title: i18n.signature.text(), - subtitle: TextField( - controller: $signature, - decoration: InputDecoration( - hintText: i18n.signaturePlaceholder, - ), - ), - ); - } - - Widget buildSaveAction() { - return PlatformTextButton( - onPressed: () { - final signature = $signature.text.trim(); - Settings.lastSignature = signature; - context.pop(widget.timetable.copyWith( - name: $name.text, - signature: signature, - startDate: $selectedDate.value, - )); - }, - child: i18n.save.text(), - ); - } - - Widget buildDescForm() { - return Form( - key: _formKey, - child: Column( - children: [ - TextFormField( - controller: $name, - maxLines: 1, - decoration: InputDecoration( - labelText: i18n.editor.name, - border: const OutlineInputBorder(), - ), - ).padAll(10), - ], - ), - ); - } -} - -Future _pickTimetableStartDate( - BuildContext ctx, { - required DateTime initial, -}) async { - final now = DateTime.now(); - return await showDatePicker( - context: ctx, - initialDate: initial, - currentDate: now, - firstDate: DateTime(now.year - 2), - lastDate: DateTime(now.year + 2), - selectableDayPredicate: (DateTime dataTime) => dataTime.weekday == DateTime.monday, - ); -} diff --git a/lib/timetable/page/export.dart b/lib/timetable/page/export.dart deleted file mode 100644 index 69bfc219d..000000000 --- a/lib/timetable/page/export.dart +++ /dev/null @@ -1,221 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; -import 'package:go_router/go_router.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:sit/design/widgets/duration_picker.dart'; -import '../entity/timetable.dart'; -import "../i18n.dart"; - -typedef TimetableExportCalendarAlarmConfig = ({ - Duration alarmBeforeClass, - Duration alarmDuration, - bool isSoundAlarm, -}); - -typedef TimetableExportCalendarConfig = ({ - TimetableExportCalendarAlarmConfig? alarm, - Locale? locale, - bool isLessonMerged, -}); - -class TimetableExportCalendarConfigEditor extends StatefulWidget { - final SitTimetable timetable; - - const TimetableExportCalendarConfigEditor({ - super.key, - required this.timetable, - }); - - @override - State createState() => _TimetableExportCalendarConfigEditorState(); -} - -class _TimetableExportCalendarConfigEditorState extends State { - final $enableAlarm = ValueNotifier(false); - final $alarmDuration = ValueNotifier(const Duration(minutes: 15)); - final $alarmBeforeClass = ValueNotifier(const Duration(minutes: 15)); - final $merged = ValueNotifier(true); - final $isSoundAlarm = ValueNotifier(false); - - @override - void dispose() { - $enableAlarm.dispose(); - $alarmDuration.dispose(); - $alarmBeforeClass.dispose(); - $merged.dispose(); - $isSoundAlarm.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - resizeToAvoidBottomInset: false, - body: CustomScrollView( - slivers: [ - SliverAppBar( - floating: true, - title: i18n.export.title.text(), - actions: [ - buildExportAction(), - ], - ), - SliverList.list(children: [ - buildModeSwitch(), - const Divider(), - buildAlarmToggle(), - buildAlarmModeSwitch(), - buildAlarmDuration(), - buildAlarmBeforeClassStart(), - ]), - ], - ), - ); - } - - Widget buildExportAction() { - return PlatformTextButton( - child: i18n.export.export.text(), - onPressed: () async { - context.pop(( - alarm: $enableAlarm.value - ? ( - alarmBeforeClass: $alarmBeforeClass.value, - alarmDuration: $alarmDuration.value, - isSoundAlarm: $isSoundAlarm.value, - ) - : null, - locale: context.locale, - isLessonMerged: $merged.value, - )); - }, - ); - } - - Widget buildModeSwitch() { - return $merged >> - (ctx, merged) => ListTile( - isThreeLine: true, - leading: const Icon(Icons.calendar_month), - title: i18n.export.lessonMode.text(), - subtitle: [ - ChoiceChip( - label: i18n.export.lessonModeMerged.text(), - selected: merged, - onSelected: (value) { - $merged.value = true; - }, - ), - ChoiceChip( - label: i18n.export.lessonModeSeparate.text(), - selected: !merged, - onSelected: (value) { - $merged.value = false; - }, - ), - ].wrap(spacing: 4), - trailing: Tooltip( - triggerMode: TooltipTriggerMode.tap, - message: merged ? i18n.export.lessonModeMergedTip : i18n.export.lessonModeSeparateTip, - child: const Icon(Icons.info_outline), - ).padAll(8), - ); - } - - Widget buildAlarmToggle() { - return ListTile( - leading: const Icon(Icons.alarm), - title: i18n.export.enableAlarm.text(), - subtitle: i18n.export.enableAlarmDesc.text(), - trailing: $enableAlarm >> - (ctx, value) => Switch.adaptive( - value: value, - onChanged: (newV) { - $enableAlarm.value = newV; - }, - ), - ); - } - - Widget buildAlarmModeSwitch() { - return $enableAlarm >> - (ctx, enabled) => - $isSoundAlarm >> - (ctx, soundAlarm) => ListTile( - isThreeLine: true, - enabled: enabled, - title: i18n.export.alarmMode.text(), - subtitle: [ - ChoiceChip( - label: i18n.export.alarmModeSound.text(), - selected: soundAlarm, - onSelected: !enabled - ? null - : (value) { - $isSoundAlarm.value = true; - }, - ), - ChoiceChip( - label: i18n.export.alarmModeDisplay.text(), - selected: !soundAlarm, - onSelected: !enabled - ? null - : (value) { - $isSoundAlarm.value = false; - }, - ), - ].wrap(spacing: 4), - ); - } - - Widget buildAlarmDuration() { - return $enableAlarm >> - (ctx, enabled) => - $alarmDuration >> - (ctx, duration) => ListTile( - enabled: enabled, - title: i18n.export.alarmDuration.text(), - subtitle: i18n.time.minuteFormat(duration.inMinutes.toString()).text(), - trailing: IconButton( - icon: const Icon(Icons.edit), - onPressed: !enabled - ? null - : () async { - final newDuration = await showDurationPicker( - context: ctx, - initialTime: duration, - ); - if (newDuration != null) { - $alarmDuration.value = newDuration; - } - }, - ), - ); - } - - Widget buildAlarmBeforeClassStart() { - return $enableAlarm >> - (ctx, enabled) => - $alarmBeforeClass >> - (ctx, duration) => ListTile( - enabled: enabled, - title: i18n.export.alarmBeforeClassBegins.text(), - subtitle: i18n.export.alarmBeforeClassBeginsDesc(duration).text(), - trailing: IconButton( - icon: const Icon(Icons.edit), - onPressed: !enabled - ? null - : () async { - final newDuration = await showDurationPicker( - context: ctx, - initialTime: duration, - ); - if (newDuration != null) { - $alarmBeforeClass.value = newDuration; - } - }, - ), - ); - } -} diff --git a/lib/timetable/page/import.dart b/lib/timetable/page/import.dart deleted file mode 100644 index 6924e0c3c..000000000 --- a/lib/timetable/page/import.dart +++ /dev/null @@ -1,248 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; -import 'package:go_router/go_router.dart'; -import 'package:sit/credentials/entity/user_type.dart'; -import 'package:sit/credentials/widgets/oa_scope.dart'; -import 'package:sit/design/adaptive/foundation.dart'; -import 'package:sit/design/animation/animated.dart'; -import 'package:sit/init.dart'; -import 'package:sit/network/checker.dart'; -import 'package:sit/design/adaptive/dialog.dart'; -import 'package:sit/school/entity/school.dart'; -import 'package:sit/school/utils.dart'; -import 'package:sit/school/widgets/semester.dart'; -import 'package:sit/settings/settings.dart'; -import 'package:sit/timetable/utils.dart'; -import 'package:rettulf/rettulf.dart'; - -import '../i18n.dart'; -import '../entity/timetable.dart'; -import '../init.dart'; -import 'editor.dart'; - -enum ImportStatus { - none, - importing, - end, - failed; -} - -class ImportTimetablePage extends StatefulWidget { - const ImportTimetablePage({super.key}); - - @override - State createState() => _ImportTimetablePageState(); -} - -class _ImportTimetablePageState extends State { - bool canImport = false; - var _status = ImportStatus.none; - late SemesterInfo initial = estimateCurrentSemester(); - late SemesterInfo selected = initial; - - @override - void initState() { - super.initState(); - } - - @override - Widget build(BuildContext context) { - final isImporting = _status == ImportStatus.importing; - return Scaffold( - appBar: AppBar( - title: i18n.import.title.text(), - actions: [ - PlatformTextButton(onPressed: importFromFile, child: i18n.import.fromFileBtn.text()), - ], - bottom: !isImporting - ? null - : const PreferredSize( - preferredSize: Size.fromHeight(4), - child: LinearProgressIndicator(), - ), - ), - body: (canImport - ? buildImportPage(key: const ValueKey("Import Timetable")) - : buildConnectivityChecker(context, const ValueKey("Connectivity Checker"))) - .animatedSwitched(), - ); - } - - Future importFromFile() async { - try { - final id2timetable = await importTimetableFromFile(); - if (id2timetable == null) return; - if (!mounted) return; - context.pop(id2timetable); - } catch (err, stackTrace) { - // TODO: Handle permission error - debugPrint(err.toString()); - debugPrintStack(stackTrace: stackTrace); - if (!mounted) return; - context.showSnackBar(content: "Format Error. Please select a timetable file.".text()); - } - } - - Widget buildConnectivityChecker(BuildContext ctx, Key? key) { - return ConnectivityChecker( - key: key, - iconSize: ctx.isPortrait ? 180 : 120, - initialDesc: i18n.import.connectivityCheckerDesc, - check: Init.ssoSession.checkConnectivity, - onConnected: () { - if (!mounted) return; - setState(() { - canImport = true; - }); - }, - ); - } - - Widget buildTip(BuildContext ctx) { - final tip = switch (_status) { - ImportStatus.none => i18n.import.selectSemesterTip, - ImportStatus.importing => i18n.import.importing, - ImportStatus.end => i18n.import.endTip, - ImportStatus.failed => i18n.import.failedTip, - }; - return tip - .text( - key: ValueKey(_status), - style: ctx.textTheme.titleLarge, - ) - .animatedSwitched(); - } - - Widget buildImportPage({Key? key}) { - return [ - buildTip(context).padSymmetric(v: 30), - SemesterSelector( - baseYear: getAdmissionYearFromStudentId(context.auth.credentials?.account), - initial: initial, - onSelected: (newSelection) { - setState(() { - selected = newSelection; - }); - }, - ).padSymmetric(v: 30), - buildImportButton(context).padAll(24), - ].column(key: key, maa: MainAxisAlignment.center, caa: CrossAxisAlignment.center); - } - - Future<({int id, SitTimetable timetable})?> handleTimetableData( - SitTimetable timetable, - SemesterInfo info, - ) async { - final SemesterInfo(:exactYear, :semester) = info; - final defaultName = i18n.import.defaultName(semester.l10n(), exactYear.toString(), (exactYear + 1).toString()); - DateTime defaultStartDate = estimateStartDate(exactYear, semester); - if (context.auth.userType == OaUserType.undergraduate) { - final current = estimateCurrentSemester(); - if (info == current) { - final span = await TimetableInit.service.getUgSemesterSpan(); - if (span != null) { - defaultStartDate = span.start; - } - } - } - if (!mounted) return null; - final newTimetable = await context.show$Sheet$( - (ctx) => TimetableEditor( - timetable: timetable.copyWith( - name: defaultName, - semester: semester, - startDate: defaultStartDate, - schoolYear: exactYear, - signature: Settings.lastSignature, - ), - ), - dismissible: false, - ); - if (newTimetable != null) { - final id = TimetableInit.storage.timetable.add(newTimetable); - return (id: id, timetable: timetable); - } - return null; - } - - DateTime estimateStartDate(int year, Semester semester) { - if (semester == Semester.term1) { - return findFirstWeekdayInCurrentMonth(DateTime(year, 9), DateTime.monday); - } else { - return findFirstWeekdayInCurrentMonth(DateTime(year + 1, 2), DateTime.monday); - } - } - - Widget buildImportButton(BuildContext ctx) { - return FilledButton( - onPressed: _status == ImportStatus.importing ? null : _onImport, - child: i18n.import.tryImportBtn - .text( - style: TextStyle(fontSize: ctx.textTheme.titleLarge?.fontSize), - ) - .padAll(12), - ); - } - - Future getTimetable(SemesterInfo info) async { - return switch (context.auth.userType) { - OaUserType.undergraduate => TimetableInit.service.getUgTimetable(info), - OaUserType.postgraduate => TimetableInit.service.getPgTimetable(info), - OaUserType.other => throw Exception("Timetable importing not supported"), - }; - } - - void _onImport() async { - setState(() { - _status = ImportStatus.importing; - }); - try { - final selected = this.selected; - final timetable = await getTimetable(selected); - if (!mounted) return; - setState(() { - _status = ImportStatus.end; - }); - final id2timetable = await handleTimetableData(timetable, selected); - if (id2timetable == null) return; - if (!mounted) return; - context.pop(id2timetable); - } catch (e, stackTrace) { - if (e is ParallelWaitError) { - final inner = e.errors.$1 as AsyncError; - debugPrint(inner.toString()); - debugPrintStack(stackTrace: inner.stackTrace); - } else { - debugPrint(e.toString()); - debugPrintStack(stackTrace: stackTrace); - } - setState(() { - _status = ImportStatus.failed; - }); - if (!mounted) return; - await context.showTip(title: i18n.import.failed, desc: i18n.import.failedDesc, ok: i18n.ok); - } finally { - if (_status == ImportStatus.importing) { - setState(() { - _status = ImportStatus.end; - }); - } - } - } -} - -DateTime findFirstWeekdayInCurrentMonth(DateTime current, int weekday) { - // Calculate the first day of the current month while keeping the same year. - DateTime firstDayOfMonth = DateTime(current.year, current.month, 1); - - // Calculate the difference in days between the first day of the current month - // and the desired weekday. - int daysUntilWeekday = (weekday - firstDayOfMonth.weekday + 7) % 7; - - // Calculate the date of the first occurrence of the desired weekday in the current month. - DateTime firstWeekdayInMonth = firstDayOfMonth.add(Duration(days: daysUntilWeekday)); - - return firstWeekdayInMonth; -} diff --git a/lib/timetable/page/index.dart b/lib/timetable/page/index.dart deleted file mode 100644 index a979bab9e..000000000 --- a/lib/timetable/page/index.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../entity/timetable.dart'; -import '../init.dart'; -import '../widgets/style.dart'; -import 'mine.dart'; -import 'timetable.dart'; - -class TimetablePage extends StatefulWidget { - const TimetablePage({super.key}); - - @override - State createState() => _TimetablePageState(); -} - -class _TimetablePageState extends State { - late SitTimetableEntity? _selected; - final $selected = TimetableInit.storage.timetable.$selected; - - @override - void initState() { - super.initState(); - _selected = TimetableInit.storage.timetable.selectedRow?.resolve(); - $selected.addListener(onSelectChange); - } - - @override - void dispose() { - $selected.removeListener(onSelectChange); - super.dispose(); - } - - void onSelectChange() { - final current = TimetableInit.storage.timetable.selectedRow; - if (!mounted) return; - setState(() { - _selected = current?.resolve(); - }); - } - - @override - Widget build(BuildContext context) { - final selected = _selected; - if (selected == null) { - // If no timetable selected, navigate to Mine page to select/import one. - return const MyTimetableListPage(); - } else { - return TimetableStyleProv( - child: TimetableBoardPage( - timetable: selected, - ), - ); - } - } -} diff --git a/lib/timetable/page/mine.dart b/lib/timetable/page/mine.dart deleted file mode 100644 index ea69b2f7a..000000000 --- a/lib/timetable/page/mine.dart +++ /dev/null @@ -1,359 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:sit/design/adaptive/foundation.dart'; -import 'package:sit/design/widgets/common.dart'; -import 'package:sit/design/adaptive/dialog.dart'; -import 'package:sit/design/widgets/entry_card.dart'; -import 'package:sit/design/widgets/fab.dart'; -import 'package:sit/l10n/extension.dart'; -import 'package:sit/route.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:sit/settings/settings.dart'; -import 'package:sit/timetable/page/export.dart'; -import 'package:sit/timetable/platte.dart'; -import 'package:sit/timetable/widgets/course.dart'; -import 'package:text_scroll/text_scroll.dart'; - -import '../i18n.dart'; -import '../entity/timetable.dart'; -import '../init.dart'; -import '../utils.dart'; -import '../widgets/style.dart'; -import 'editor.dart'; -import 'preview.dart'; - -class MyTimetableListPage extends StatefulWidget { - const MyTimetableListPage({super.key}); - - @override - State createState() => _MyTimetableListPageState(); -} - -class _MyTimetableListPageState extends State { - final $timetableList = TimetableInit.storage.timetable.$any; - final scrollController = ScrollController(); - - @override - void initState() { - super.initState(); - $timetableList.addListener(refresh); - } - - @override - void dispose() { - $timetableList.removeListener(refresh); - super.dispose(); - } - - void refresh() { - setState(() {}); - } - - /// Import a new timetable. - /// Updates the selected timetable id. - /// If [TimetableSettings.autoUseImported] is enabled, the newly-imported will be used. - Future goImport() async { - final ({int id, SitTimetable timetable})? result; - if (isLoginGuarded(context)) { - result = await importFromFile(); - } else { - result = await importFromSchoolServer(); - } - - if (result != null) { - if (Settings.timetable.autoUseImported) { - TimetableInit.storage.timetable.selectedId = result.id; - } else { - // use this timetable if no one else - TimetableInit.storage.timetable.selectedId ??= result.id; - } - } - } - - Future<({int id, SitTimetable timetable})?> importFromSchoolServer() async { - return await context.push<({int id, SitTimetable timetable})>("/timetable/import"); - } - - Future<({int id, SitTimetable timetable})?> importFromFile() async { - try { - return await importTimetableFromFile(); - } catch (err, stackTrace) { - // TODO: Handle permission error - debugPrint(err.toString()); - debugPrintStack(stackTrace: stackTrace); - if (!mounted) return null; - context.showSnackBar(content: "Format Error. Please select a timetable file.".text()); - return null; - } - } - - @override - Widget build(BuildContext context) { - final timetables = TimetableInit.storage.timetable.getRows(); - final selectedId = TimetableInit.storage.timetable.selectedId; - return Scaffold( - body: CustomScrollView( - controller: scrollController, - slivers: [ - SliverAppBar( - floating: true, - title: i18n.mine.title.text(), - actions: [ - if (!Settings.focusTimetable) - IconButton( - icon: const Icon(Icons.color_lens_outlined), - onPressed: () { - context.push("/timetable/p13n"); - }, - ), - ], - ), - if (timetables.isEmpty) - SliverFillRemaining( - child: LeavingBlank( - icon: Icons.calendar_month_rounded, - desc: i18n.mine.emptyTip, - onIconTap: goImport, - ), - ) - else - SliverList.builder( - itemCount: timetables.length, - itemBuilder: (ctx, i) { - final (:id, row: timetable) = timetables[i]; - return TimetableCard( - id: id, - timetable: timetable, - selected: selectedId == id, - ).padH(6); - }, - ), - const SliverFillRemaining(), - ], - ), - floatingActionButton: AutoHideFAB.extended( - controller: scrollController, - onPressed: goImport, - label: Text(isLoginGuarded(context) ? i18n.import.fromFile : i18n.import.import), - icon: const Icon(Icons.add_outlined), - ), - ); - } -} - -class TimetableCard extends StatelessWidget { - final SitTimetable timetable; - final int id; - final bool selected; - - const TimetableCard({ - super.key, - required this.timetable, - required this.id, - required this.selected, - }); - - @override - Widget build(BuildContext context) { - final year = '${timetable.schoolYear}–${timetable.schoolYear + 1}'; - final semester = timetable.semester.l10n(); - final textTheme = context.textTheme; - - return EntryCard( - title: timetable.name, - selected: selected, - selectAction: (ctx) => EntrySelectAction( - selectLabel: i18n.use, - selectedLabel: i18n.used, - action: () async { - TimetableInit.storage.timetable.selectedId = id; - }, - ), - deleteAction: (ctx) => EntryDeleteAction( - label: i18n.delete, - action: () async { - final confirm = await ctx.showRequest( - title: i18n.mine.deleteRequest, - desc: i18n.mine.deleteRequestDesc, - yes: i18n.delete, - no: i18n.cancel, - highlight: true, - ); - if (confirm != true) return; - TimetableInit.storage.timetable.delete(id); - if (TimetableInit.storage.timetable.isEmpty) { - if (!ctx.mounted) return; - ctx.pop(); - } - }, - ), - actions: (ctx) => [ - if (!selected) - EntryAction( - main: true, - label: i18n.preview, - icon: Icons.preview, - cupertinoIcon: CupertinoIcons.eye, - action: () async { - if (!ctx.mounted) return; - await context.show$Sheet$( - (context) => TimetableStyleProv( - child: TimetablePreviewPage( - timetable: timetable, - ), - ), - ); - }, - ), - EntryAction( - label: i18n.edit, - icon: Icons.edit, - cupertinoIcon: CupertinoIcons.pencil, - type: EntryActionType.edit, - action: () async { - // don't use outside `palette`. because it wouldn't updated after the palette was changed. - // TODO: better solution - final timetable = TimetableInit.storage.timetable[id]; - if (timetable == null) return; - final newTimetable = await ctx.show$Sheet$( - (ctx) => TimetableEditor(timetable: timetable), - ); - if (newTimetable != null) { - TimetableInit.storage.timetable[id] = newTimetable; - } - }, - ), - EntryAction( - label: i18n.share, - icon: Icons.output_outlined, - cupertinoIcon: CupertinoIcons.share, - type: EntryActionType.share, - action: () async { - await exportTimetableFileAndShare(timetable, context: ctx); - }, - ), - EntryAction( - label: i18n.mine.exportCalendar, - icon: Icons.calendar_month, - cupertinoIcon: CupertinoIcons.calendar_badge_plus, - action: () async { - await onExportCalendar(ctx, timetable); - }, - ), - ], - detailsBuilder: (ctx, actions) { - return TimetableDetailsPage(id: id, timetable: timetable, actions: actions?.call(ctx)); - }, - itemBuilder: (ctx, animation) => [ - timetable.name.text(style: textTheme.titleLarge), - "$year, $semester".text(style: textTheme.titleMedium), - if (timetable.signature.isNotEmpty) timetable.signature.text(style: textTheme.bodyMedium), - "${i18n.startWith} ${context.formatYmdText(timetable.startDate)}".text(style: textTheme.bodyMedium), - ], - ); - } - - Future onExportCalendar(BuildContext context, SitTimetable timetable) async { - final config = await context.show$Sheet$( - (context) => TimetableExportCalendarConfigEditor(timetable: timetable)); - if (config == null) return; - if (!context.mounted) return; - await exportTimetableAsICalendarAndOpen( - context, - timetable: timetable.resolve(), - config: config, - ); - } -} - -class TimetableDetailsPage extends StatefulWidget { - final int id; - final SitTimetable timetable; - final List? actions; - - const TimetableDetailsPage({ - super.key, - required this.id, - required this.timetable, - this.actions, - }); - - @override - State createState() => _TimetableDetailsPageState(); -} - -class _TimetableDetailsPageState extends State { - late final $row = TimetableInit.storage.timetable.listenRowChange(widget.id); - late SitTimetable timetable = widget.timetable; - - @override - void initState() { - super.initState(); - $row.addListener(refresh); - } - - @override - void dispose() { - $row.removeListener(refresh); - super.dispose(); - } - - void refresh() { - final timetable = TimetableInit.storage.timetable[widget.id]; - if (timetable == null) { - context.pop(); - return; - } else { - setState(() { - this.timetable = timetable; - }); - } - } - - @override - Widget build(BuildContext context) { - final timetable = this.timetable; - final actions = widget.actions; - final palette = TimetableInit.storage.palette.selectedRow ?? BuiltinTimetablePalettes.classic; - final courses = timetable.courses.values.toList(); - return Scaffold( - body: CustomScrollView( - slivers: [ - SliverAppBar( - title: TextScroll(timetable.name), - floating: true, - actions: actions, - ), - SliverList.list(children: [ - ListTile( - leading: const Icon(Icons.drive_file_rename_outline), - title: i18n.editor.name.text(), - subtitle: timetable.name.text(), - ), - ListTile( - leading: const Icon(Icons.date_range), - title: i18n.startWith.text(), - subtitle: context.formatYmdText(timetable.startDate).text(), - ), - ListTile( - leading: const Icon(Icons.drive_file_rename_outline), - title: i18n.signature.text(), - subtitle: timetable.signature.text(), - ), - const Divider(), - ]), - SliverList.builder( - itemCount: courses.length, - itemBuilder: (ctx, i) { - return TimetableCourseCard( - courses[i], - palette: palette, - ); - }, - ) - ], - ), - ); - } -} diff --git a/lib/timetable/page/p13n.dart b/lib/timetable/page/p13n.dart deleted file mode 100644 index 1c6adec11..000000000 --- a/lib/timetable/page/p13n.dart +++ /dev/null @@ -1,553 +0,0 @@ -import 'package:dynamic_color/dynamic_color.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:go_router/go_router.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:sit/design/adaptive/dialog.dart'; -import 'package:sit/design/adaptive/foundation.dart'; -import 'package:sit/design/widgets/card.dart'; -import 'package:sit/design/widgets/entry_card.dart'; -import 'package:sit/l10n/extension.dart'; -import 'package:sit/qrcode/page/view.dart'; -import 'package:sit/qrcode/protocol.dart'; -import 'package:sit/timetable/entity/platte.dart'; -import 'package:sit/timetable/entity/timetable.dart'; -import 'package:sit/timetable/init.dart'; -import 'package:sit/timetable/platte.dart'; -import 'package:sit/utils/color.dart'; -import 'package:sit/utils/format.dart'; -import 'package:text_scroll/text_scroll.dart'; - -import '../i18n.dart'; -import '../widgets/style.dart'; -import '../widgets/timetable/weekly.dart'; -import 'palette.dart'; -import 'preview.dart'; - -class TimetableP13nPage extends StatefulWidget { - final int? tab; - - const TimetableP13nPage({ - super.key, - this.tab, - }) : assert(tab == null || (0 <= tab && tab < TimetableP13nTab.length), "#$tab tab not found"); - - @override - State createState() => _TimetableP13nPageState(); -} - -class TimetableP13nTab { - static const length = 2; - static const custom = 0; - static const builtin = 1; -} - -class _TimetableP13nPageState extends State with SingleTickerProviderStateMixin { - final $paletteList = TimetableInit.storage.palette.$any; - late final TabController tabController; - final $selected = TimetableInit.storage.timetable.$selected; - var selectedTimetable = TimetableInit.storage.timetable.selectedRow; - - @override - void initState() { - super.initState(); - $selected.addListener(refresh); - $paletteList.addListener(refresh); - tabController = TabController(vsync: this, length: TimetableP13nTab.length); - final selectedId = TimetableInit.storage.palette.selectedId; - final forceTab = widget.tab; - if (forceTab != null) { - tabController.index = forceTab.clamp(TimetableP13nTab.custom, TimetableP13nTab.builtin); - } else if (selectedId == null || BuiltinTimetablePalettes.all.any((palette) => palette.id == selectedId)) { - tabController.index = TimetableP13nTab.builtin; - } - } - - @override - void dispose() { - $paletteList.removeListener(refresh); - tabController.dispose(); - $selected.removeListener(refresh); - super.dispose(); - } - - void refresh() { - setState(() { - selectedTimetable = TimetableInit.storage.timetable.selectedRow; - }); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - floatingActionButton: FloatingActionButton.extended( - label: i18n.p13n.palette.fab.text(), - icon: const Icon(Icons.add), - onPressed: () async { - final palette = TimetablePalette( - name: i18n.p13n.palette.newPaletteName, - author: "", - colors: [], - lastModified: DateTime.now(), - ); - TimetableInit.storage.palette.add(palette); - tabController.index = TimetableP13nTab.custom; - }, - ), - body: NestedScrollView( - floatHeaderSlivers: true, - headerSliverBuilder: (context, innerBoxIsScrolled) { - // These are the slivers that show up in the "outer" scroll view. - return [ - SliverOverlapAbsorber( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), - sliver: SliverAppBar( - floating: true, - title: i18n.p13n.palette.title.text(), - forceElevated: innerBoxIsScrolled, - bottom: TabBar( - controller: tabController, - isScrollable: true, - tabs: [ - Tab(child: i18n.p13n.palette.customTab.text()), - Tab(child: i18n.p13n.palette.builtinTab.text()), - ], - ), - ), - ), - ]; - }, - body: TabBarView( - controller: tabController, - children: [ - buildPaletteList(TimetableInit.storage.palette.getRows()), - buildPaletteList(BuiltinTimetablePalettes.all.map((e) => (id: e.id, row: e)).toList()), - ], - ), - ), - ); - } - - Widget buildPaletteList(List<({int id, TimetablePalette row})> palettes) { - final selectedId = TimetableInit.storage.palette.selectedId ?? BuiltinTimetablePalettes.classic.id; - palettes.sort((a, b) { - final $a = a.row.lastModified; - final $b = b.row.lastModified; - if ($a == $b) return 0; - if ($a == null) { - return 1; - } else if ($b == null) { - return -1; - } - return $b.compareTo($a); - }); - return CustomScrollView( - slivers: [ - SliverList.builder( - itemCount: palettes.length, - itemBuilder: (ctx, i) { - final (:id, row: palette) = palettes[i]; - return PaletteCard( - id: id, - palette: palette, - timetable: selectedTimetable, - selected: selectedId == id, - onDuplicate: () { - tabController.index = TimetableP13nTab.custom; - }, - ).padH(6); - }, - ), - ], - ); - } -} - -class PaletteCard extends StatelessWidget { - final int id; - final TimetablePalette palette; - final bool selected; - final SitTimetable? timetable; - final VoidCallback? onDuplicate; - - const PaletteCard({ - super.key, - required this.id, - required this.palette, - required this.selected, - this.timetable, - this.onDuplicate, - }); - - @override - Widget build(BuildContext context) { - final theme = context.theme; - final timetable = this.timetable; - return EntryCard( - title: palette.name, - selected: selected, - selectAction: (ctx) => EntrySelectAction( - selectLabel: i18n.use, - selectedLabel: i18n.used, - action: palette.colors.isEmpty - ? null - : () async { - TimetableInit.storage.palette.selectedId = id; - }, - ), - deleteAction: palette is BuiltinTimetablePalette - ? null - : (ctx) => EntryDeleteAction( - label: i18n.delete, - action: () async { - final confirm = await ctx.showRequest( - title: i18n.p13n.palette.deleteRequest, - desc: i18n.p13n.palette.deleteRequestDesc, - yes: i18n.delete, - no: i18n.cancel, - highlight: true, - ); - if (confirm == true) { - TimetableInit.storage.palette.delete(id); - } - }, - ), - actions: (ctx) => [ - if (palette is! BuiltinTimetablePalette) - EntryAction( - main: true, - label: i18n.edit, - icon: Icons.edit, - cupertinoIcon: CupertinoIcons.pencil, - type: EntryActionType.edit, - oneShot: true, - action: () async { - // don't use outside `palette`. because it wouldn't updated after the palette was changed. - // TODO: better solution - final palette = TimetableInit.storage.palette[id]; - if (palette == null) return; - await context.show$Sheet$( - (context) => TimetablePaletteEditor(id: id, palette: palette.copyWith()), - ); - }, - ), - if (timetable != null && palette.colors.isNotEmpty) - EntryAction( - label: i18n.preview, - icon: Icons.preview, - cupertinoIcon: CupertinoIcons.eye, - action: () async { - await context.show$Sheet$( - (context) => TimetableStyleProv( - palette: palette, - child: TimetablePreviewPage( - timetable: timetable, - ), - ), - ); - }, - ), - EntryAction( - label: i18n.duplicate, - icon: Icons.copy, - oneShot: true, - cupertinoIcon: CupertinoIcons.plus_square_on_square, - action: () async { - final duplicate = palette.copyWith( - name: getDuplicateFileName(palette.name), - author: palette.author, - lastModified: DateTime.now(), - ); - TimetableInit.storage.palette.add(duplicate); - onDuplicate?.call(); - }, - ), - EntryAction( - label: i18n.p13n.palette.shareQrCode, - icon: Icons.qr_code, - cupertinoIcon: CupertinoIcons.qrcode, - action: () async { - final qrCodeData = const TimetablePaletteDeepLink().encode(palette); - await ctx.show$Sheet$( - (context) => QrCodePage( - title: palette.name.text(), - data: qrCodeData.toString(), - ), - ); - }, - ), - if (kDebugMode) - EntryAction( - label: "Copy Dart code", - action: () async { - final code = palette.colors.toString(); - await Clipboard.setData(ClipboardData(text: code)); - }, - ), - ], - detailsBuilder: (ctx, actions) { - return PaletteDetailsPage(id: id, palette: palette, actions: actions?.call(ctx)); - }, - itemBuilder: (ctx, animation) => [ - palette.name.text(style: theme.textTheme.titleLarge), - if (palette.author.isNotEmpty) - palette.author.text( - style: const TextStyle( - fontStyle: FontStyle.italic, - ), - ), - PaletteColorsPreview(palette.colors), - ], - ); - } -} - -class PaletteDetailsPage extends StatefulWidget { - final int id; - final TimetablePalette palette; - final List? actions; - - const PaletteDetailsPage({ - super.key, - required this.id, - required this.palette, - this.actions, - }); - - @override - State createState() => _PaletteDetailsPageState(); -} - -class _PaletteDetailsPageState extends State { - late final $row = TimetableInit.storage.palette.listenRowChange(widget.id); - late TimetablePalette palette = widget.palette; - - @override - void initState() { - super.initState(); - $row.addListener(refresh); - } - - @override - void dispose() { - $row.removeListener(refresh); - super.dispose(); - } - - void refresh() { - final palette = TimetableInit.storage.palette[widget.id]; - if (palette == null) { - context.pop(); - return; - } else { - setState(() { - this.palette = palette; - }); - } - } - - @override - Widget build(BuildContext context) { - final palette = this.palette; - final actions = widget.actions; - return Scaffold( - body: CustomScrollView( - slivers: [ - SliverAppBar( - title: TextScroll(palette.name), - floating: true, - actions: actions, - ), - SliverList.list(children: [ - ListTile( - leading: const Icon(Icons.drive_file_rename_outline), - title: i18n.p13n.palette.name.text(), - subtitle: palette.name.text(), - ), - if (palette.author.isNotEmpty) - ListTile( - leading: const Icon(Icons.person), - title: i18n.p13n.palette.author.text(), - subtitle: palette.author.text(), - ), - if (palette.colors.isNotEmpty) const Divider(), - if (palette.colors.isNotEmpty) - TimetableStyleProv( - palette: palette, - child: const TimetableP13nLivePreview(), - ), - const Divider(), - const LightDarkColorsHeaderTitle(), - ]), - SliverList.builder( - itemCount: palette.colors.length, - itemBuilder: (ctx, i) { - return PaletteColorTile(colors: palette.colors[i]); - }, - ) - ], - ), - ); - } -} - -class PaletteColorsPreview extends StatelessWidget { - final List colors; - - const PaletteColorsPreview(this.colors, {super.key}); - - @override - Widget build(BuildContext context) { - final brightness = context.theme.brightness; - return colors - .map((c) { - final color = c.byBrightness(brightness); - return OutlinedCard( - color: brightness == Brightness.light ? Colors.black : Colors.white, - margin: EdgeInsets.zero, - child: TweenAnimationBuilder( - tween: ColorTween(begin: color, end: color), - duration: const Duration(milliseconds: 300), - builder: (ctx, value, child) => FilledCard( - margin: EdgeInsets.zero, - color: value, - child: const SizedBox( - width: 32, - height: 32, - ), - ), - ), - ); - }) - .toList() - .wrap(spacing: 4, runSpacing: 4) - .padV(4); - } -} - -class TimetableP13nLivePreview extends StatelessWidget { - const TimetableP13nLivePreview({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return LayoutBuilder(builder: (ctx, box) { - final height = box.maxHeight.isFinite ? box.maxHeight : context.mediaQuery.size.height / 2; - return buildLivePreview(context, fullSize: Size(box.maxWidth, height)); - }); - } - - Widget buildLivePreview( - BuildContext context, { - required Size fullSize, - }) { - final style = TimetableStyle.of(context); - final cellStyle = style.cellStyle; - final palette = style.platte; - final cellSize = Size(fullSize.width / 5, fullSize.height / 3); - final themeColor = context.colorScheme.primary; - Widget buildCell({ - required int colorId, - required String name, - required String place, - required List teachers, - bool grayOut = false, - }) { - var color = palette.safeGetColor(colorId).byTheme(context.theme); - if (cellStyle.harmonizeWithThemeColor) { - color = color.harmonizeWith(themeColor); - } - if (grayOut) { - color = color.monochrome(); - } - final alpha = cellStyle.alpha; - if (alpha < 1.0) { - color = color.withOpacity(alpha); - } - return SizedBox.fromSize( - size: cellSize, - child: TweenAnimationBuilder( - tween: ColorTween(begin: color, end: color), - duration: const Duration(milliseconds: 300), - builder: (ctx, value, child) => CourseCell( - courseName: name, - color: value!, - place: place, - teachers: cellStyle.showTeachers ? teachers : null, - ), - ), - ); - } - - Widget livePreview( - int index, { - required int colorId, - bool grayOut = false, - }) { - final data = i18n.p13n.livePreview(index); - return buildCell( - colorId: colorId, - name: data.name, - place: data.place, - teachers: data.teachers, - grayOut: grayOut, - ); - } - - final grayOut = cellStyle.grayOutTakenLessons; - return ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: palette.colors.length, - itemBuilder: (ctx, i) { - return livePreview(i % 4, colorId: i, grayOut: grayOut && i % 4 < 2).padH(8); - }, - ).sized(h: cellSize.height); - } -} - -class BrightnessSwitch extends StatelessWidget { - final ValueNotifier $brightness; - - const BrightnessSwitch( - this.$brightness, { - super.key, - }); - - @override - Widget build(BuildContext context) { - return $brightness >> - (ctx, brightness) => SegmentedButton( - segments: [ - ButtonSegment( - value: Brightness.light, - label: Brightness.light.l10n().text(), - icon: const Icon(Icons.light_mode), - ), - ButtonSegment( - value: Brightness.dark, - label: Brightness.dark.l10n().text(), - icon: const Icon(Icons.dark_mode), - ), - ], - selected: {brightness}, - onSelectionChanged: (newSelection) async { - $brightness.value = newSelection.first; - await HapticFeedback.selectionClick(); - }, - ); - } -} - -Future onTimetablePaletteFromQrCode({ - required BuildContext context, - required TimetablePalette palette, -}) async { - TimetableInit.storage.palette.add(palette); - await HapticFeedback.mediumImpact(); - if (!context.mounted) return; - context.showSnackBar(content: i18n.p13n.palette.addFromQrCode.text()); - context.push("/timetable/p13n/custom"); -} diff --git a/lib/timetable/page/palette.dart b/lib/timetable/page/palette.dart deleted file mode 100644 index bd2b9f823..000000000 --- a/lib/timetable/page/palette.dart +++ /dev/null @@ -1,333 +0,0 @@ -import 'package:flex_color_picker/flex_color_picker.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_platform_widgets/flutter_platform_widgets.dart' hide isCupertino; -import 'package:rettulf/rettulf.dart'; -import 'package:sit/design/adaptive/dialog.dart'; -import 'package:sit/design/adaptive/foundation.dart'; -import 'package:sit/design/adaptive/swipe.dart'; -import 'package:sit/design/widgets/card.dart'; -import 'package:sit/l10n/extension.dart'; -import 'package:sit/timetable/page/preview.dart'; - -import '../entity/platte.dart'; -import '../i18n.dart'; -import '../init.dart'; -import '../widgets/style.dart'; - -class TimetablePaletteEditor extends StatefulWidget { - final int id; - final TimetablePalette palette; - - const TimetablePaletteEditor({ - super.key, - required this.id, - required this.palette, - }); - - @override - State createState() => _TimetablePaletteEditorState(); -} - -class _Tab { - static const length = 2; - static const info = 0; - static const colors = 1; -} - -class _TimetablePaletteEditorState extends State { - late final $name = TextEditingController(text: widget.palette.name); - late final $author = TextEditingController(text: widget.palette.author); - late var colors = widget.palette.colors; - final $selected = TimetableInit.storage.timetable.$selected; - var selectedTimetable = TimetableInit.storage.timetable.selectedRow; - - @override - void initState() { - super.initState(); - $selected.addListener(refresh); - } - - @override - void dispose() { - $name.dispose(); - $author.dispose(); - $selected.removeListener(refresh); - super.dispose(); - } - - void refresh() { - setState(() { - selectedTimetable = TimetableInit.storage.timetable.selectedRow; - }); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - body: DefaultTabController( - length: _Tab.length, - child: NestedScrollView( - floatHeaderSlivers: true, - headerSliverBuilder: (context, innerBoxIsScrolled) { - // These are the slivers that show up in the "outer" scroll view. - final selectedTimetable = TimetableInit.storage.timetable.selectedRow; - return [ - SliverOverlapAbsorber( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), - sliver: SliverAppBar( - floating: true, - title: i18n.p13n.palette.title.text(), - actions: [ - if (selectedTimetable != null && colors.isNotEmpty) - PlatformTextButton( - child: i18n.preview.text(), - onPressed: () async { - await context.show$Sheet$( - (context) => TimetableStyleProv( - palette: buildPalette(), - child: TimetablePreviewPage( - timetable: selectedTimetable, - ), - ), - ); - }, - ), - PlatformTextButton( - child: i18n.save.text(), - onPressed: () { - final palette = buildPalette(); - TimetableInit.storage.palette[widget.id] = palette; - context.navigator.pop(palette); - }, - ), - ], - forceElevated: innerBoxIsScrolled, - bottom: TabBar( - isScrollable: true, - tabs: [ - Tab(child: i18n.p13n.palette.infoTab.text()), - Tab(child: i18n.p13n.palette.colorsTab.text()), - ], - ), - ), - ), - ]; - }, - body: TabBarView( - children: [ - CustomScrollView( - slivers: [ - SliverList.list(children: [ - buildName(), - buildAuthor(), - ]), - ], - ), - CustomScrollView( - slivers: [ - const SliverToBoxAdapter( - child: LightDarkColorsHeaderTitle(), - ), - SliverList.builder( - itemCount: colors.length, - itemBuilder: buildColorTile, - ), - SliverList.list(children: [ - const Divider(indent: 12, endIndent: 12), - ListTile( - leading: const Icon(Icons.add), - title: i18n.p13n.palette.addColor.text(), - onTap: () { - setState(() { - colors.add((light: Colors.white30, dark: Colors.black12)); - }); - }, - ), - ]), - ], - ) - ], - ), - ), - ), - ); - } - - TimetablePalette buildPalette() { - return TimetablePalette( - name: $name.text, - author: $author.text, - colors: colors, - lastModified: DateTime.now(), - ); - } - - Widget buildColorTile(BuildContext ctx, int index) { - Future changeColor(Color old, Brightness brightness) async { - final newColor = await showColorPickerDialog( - ctx, - old, - enableOpacity: true, - enableShadesSelection: true, - enableTonalPalette: true, - showColorCode: true, - pickersEnabled: const { - ColorPickerType.both: true, - ColorPickerType.primary: false, - ColorPickerType.accent: false, - ColorPickerType.custom: true, - ColorPickerType.wheel: true, - }, - ); - if (newColor != old) { - await HapticFeedback.mediumImpact(); - setState(() { - if (brightness == Brightness.light) { - colors[index] = (light: newColor, dark: colors[index].dark); - } else { - colors[index] = (light: colors[index].light, dark: newColor); - } - }); - } - } - - final current = colors[index]; - return SwipeToDismiss( - childKey: ObjectKey(current), - right: SwipeToDismissAction( - label: i18n.delete, - action: () async { - setState(() { - colors.removeAt(index); - }); - }, - ), - child: PaletteColorTile( - colors: current, - onEdit: (old, brightness) async { - await changeColor(old, brightness); - }, - ), - ); - } - - Widget buildName() { - return ListTile( - isThreeLine: true, - title: i18n.p13n.palette.name.text(), - subtitle: TextField( - controller: $name, - decoration: InputDecoration( - hintText: i18n.p13n.palette.namePlaceholder, - ), - ), - ); - } - - Widget buildAuthor() { - return ListTile( - isThreeLine: true, - title: i18n.p13n.palette.author.text(), - subtitle: TextField( - controller: $author, - decoration: InputDecoration( - hintText: i18n.p13n.palette.authorPlaceholder, - ), - ), - ); - } -} - -class LightDarkColorsHeaderTitle extends StatelessWidget { - const LightDarkColorsHeaderTitle({super.key}); - - @override - Widget build(BuildContext context) { - return ListTile( - title: [ - [const Icon(Icons.light_mode), Brightness.light.l10n().text()].row(mas: MainAxisSize.min), - [const Icon(Icons.dark_mode), Brightness.dark.l10n().text()].row(mas: MainAxisSize.min), - ].row(maa: MainAxisAlignment.spaceBetween), - ); - } -} - -class PaletteColorTile extends StatelessWidget { - final Color2Mode colors; - final void Function(Color old, Brightness brightness)? onEdit; - - const PaletteColorTile({ - super.key, - required this.colors, - this.onEdit, - }); - - @override - Widget build(BuildContext context) { - final (:light, :dark) = colors; - return ListTile( - isThreeLine: true, - visualDensity: VisualDensity.compact, - title: [ - "#${light.hexAlpha}".text(), - "#${dark.hexAlpha}".text(), - ].row(maa: MainAxisAlignment.spaceBetween), - subtitle: [ - PaletteColorBar(color: light, brightness: Brightness.light, onEdit: onEdit).expanded(), - const SizedBox(width: 5), - PaletteColorBar(color: dark, brightness: Brightness.dark, onEdit: onEdit).expanded(), - ].row(mas: MainAxisSize.min, maa: MainAxisAlignment.spaceEvenly), - ); - } -} - -class PaletteColorBar extends StatelessWidget { - final Color color; - final Brightness brightness; - final void Function(Color old, Brightness brightness)? onEdit; - - const PaletteColorBar({ - super.key, - required this.color, - required this.brightness, - this.onEdit, - }); - - @override - Widget build(BuildContext context) { - final onEdit = this.onEdit; - final inverseColor = brightness == Brightness.light ? Colors.black : Colors.white; - return OutlinedCard( - color: inverseColor, - margin: EdgeInsets.zero, - child: FilledCard( - color: color, - clip: Clip.hardEdge, - margin: EdgeInsets.zero, - child: InkWell( - onTap: onEdit == null - ? null - : () { - onEdit.call(color, brightness); - }, - onLongPress: () async { - await Clipboard.setData(ClipboardData(text: "#${color.hexAlpha}")); - if (!context.mounted) return; - context.showSnackBar(content: i18n.copyTipOf(i18n.p13n.palette.color).text()); - }, - child: SizedBox( - height: 35, - child: brightness - .l10n() - .text( - style: context.textTheme.bodyLarge?.copyWith( - color: inverseColor, - )) - .center(), - ), - ), - ), - ); - } -} diff --git a/lib/timetable/page/preview.dart b/lib/timetable/page/preview.dart deleted file mode 100644 index 1ffe6fb14..000000000 --- a/lib/timetable/page/preview.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:text_scroll/text_scroll.dart'; - -import '../entity/display.dart'; -import '../entity/timetable.dart'; -import '../entity/pos.dart'; -import '../widgets/timetable/board.dart'; - -class TimetablePreviewPage extends StatefulWidget { - final SitTimetable timetable; - - const TimetablePreviewPage({ - super.key, - required this.timetable, - }); - - @override - State createState() => _TimetablePreviewPageState(); -} - -class _TimetablePreviewPageState extends State { - final $displayMode = ValueNotifier(DisplayMode.weekly); - late final $currentPos = ValueNotifier(widget.timetable.locate(DateTime.now())); - final scrollController = ScrollController(); - late SitTimetableEntity timetable; - - @override - void dispose() { - $displayMode.dispose(); - $currentPos.dispose(); - super.dispose(); - } - - @override - void didChangeDependencies() { - timetable = widget.timetable.resolve(); - super.didChangeDependencies(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: TextScroll(widget.timetable.name), - actions: [ - IconButton( - icon: const Icon(Icons.swap_horiz), - onPressed: () { - $displayMode.value = $displayMode.value.toggle(); - }, - ) - ], - ), - body: TimetableBoard( - timetable: timetable, - $displayMode: $displayMode, - $currentPos: $currentPos, - ), - ); - } -} diff --git a/lib/timetable/page/screenshot.dart b/lib/timetable/page/screenshot.dart deleted file mode 100644 index e2e2bb3ac..000000000 --- a/lib/timetable/page/screenshot.dart +++ /dev/null @@ -1,227 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; -import 'package:go_router/go_router.dart'; -import 'package:open_file/open_file.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:screenshot/screenshot.dart'; -import 'package:sit/design/adaptive/foundation.dart'; -import 'package:sit/files.dart'; -import 'package:sit/settings/settings.dart'; -import 'package:sit/timetable/entity/timetable.dart'; -import "../i18n.dart"; -import '../widgets/style.dart'; -import '../widgets/timetable/board.dart'; -import '../widgets/timetable/weekly.dart'; - -typedef TimetableScreenshotConfig = ({ - String signature, - bool grayOutTakenLessons, - bool enableBackground, -}); - -class TimetableScreenshotConfigEditor extends StatefulWidget { - final SitTimetableEntity timetable; - final bool initialGrayOut; - - const TimetableScreenshotConfigEditor({ - super.key, - required this.timetable, - this.initialGrayOut = false, - }); - - @override - State createState() => _TimetableScreenshotConfigEditorState(); -} - -class _TimetableScreenshotConfigEditorState extends State { - late final $signature = TextEditingController(text: widget.timetable.signature); - late bool grayOutTakenLessons = widget.initialGrayOut; - var enableBackground = true; - - @override - void dispose() { - $signature.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - body: CustomScrollView( - slivers: [ - SliverAppBar( - floating: true, - title: i18n.screenshot.title.text(), - actions: [ - buildScreenshotAction(), - ], - ), - SliverList.list(children: [ - buildSignatureInput(), - buildGrayOut(), - buildEnableBackground(), - ]), - ], - ), - ); - } - - Widget buildScreenshotAction() { - return PlatformTextButton( - child: i18n.screenshot.take.text(), - onPressed: () async { - Settings.lastSignature = $signature.text; - context.pop(( - signature: $signature.text.trim(), - grayOutTakenLessons: grayOutTakenLessons == true, - enableBackground: enableBackground, - )); - }, - ); - } - - Widget buildSignatureInput() { - return ListTile( - isThreeLine: true, - leading: const Icon(Icons.drive_file_rename_outline), - title: i18n.signature.text(), - subtitle: TextField( - controller: $signature, - decoration: InputDecoration( - hintText: i18n.signaturePlaceholder, - ), - ), - ); - } - - Widget buildGrayOut() { - return ListTile( - leading: const Icon(Icons.timelapse), - title: i18n.p13n.cell.grayOut.text(), - subtitle: i18n.p13n.cell.grayOutDesc.text(), - trailing: Switch.adaptive( - value: grayOutTakenLessons == true, - onChanged: (newV) { - setState(() { - grayOutTakenLessons = newV; - }); - }, - ), - ); - } - - Widget buildEnableBackground() { - return ListTile( - leading: const Icon(Icons.image_outlined), - title: i18n.screenshot.enableBackground.text(), - subtitle: i18n.screenshot.enableBackgroundDesc.text(), - trailing: Switch.adaptive( - value: enableBackground, - onChanged: (newV) { - setState(() { - enableBackground = newV; - }); - }, - ), - ); - } -} - -class TimetableWeeklyScreenshotFilm extends StatelessWidget { - final TimetableScreenshotConfig config; - final SitTimetableEntity timetable; - final int weekIndex; - final Size fullSize; - - const TimetableWeeklyScreenshotFilm({ - super.key, - required this.timetable, - required this.weekIndex, - required this.fullSize, - required this.config, - }); - - @override - Widget build(BuildContext context) { - final style = TimetableStyle.of(context); - final background = style.background; - if (config.enableBackground && background.enabled) { - return [ - Positioned.fill( - child: TimetableBackground(background: background), - ), - buildBody(context, style), - ].stack(); - } - return buildBody(context, style); - } - - Widget buildBody(BuildContext context, TimetableStyleData style) { - final today = DateTime.now(); - return [ - buildTitle().text(style: context.textTheme.titleLarge).padSymmetric(v: 10), - TimetableOneWeek( - fullSize: fullSize, - timetable: timetable, - weekIndex: weekIndex, - cellBuilder: ({required context, required lesson, required timetable}) { - return StyledCourseCell( - style: style, - course: lesson.course, - grayOut: config.grayOutTakenLessons ? lesson.type.endTime.isBefore(today) : false, - ); - }, - ), - ].column(); - } - - String buildTitle() { - final week = i18n.weekOrderedName(number: weekIndex + 1); - final signature = config.signature; - if (signature.isNotEmpty) { - return "$signature $week"; - } - return week; - } -} - -Future takeTimetableScreenshot({ - required BuildContext context, - required SitTimetableEntity timetable, - required int weekIndex, -}) async { - final config = await context.show$Sheet$((ctx) => TimetableScreenshotConfigEditor( - timetable: timetable, - initialGrayOut: TimetableStyle.of(context).cellStyle.grayOutTakenLessons, - )); - if (config == null) return; - if (!context.mounted) return; - var fullSize = context.mediaQuery.size; - fullSize = Size(fullSize.width, fullSize.height); - final screenshotController = ScreenshotController(); - final screenshot = await screenshotController.captureFromLongWidget( - InheritedTheme.captureAll( - context, - MediaQuery( - data: MediaQueryData(size: fullSize), - child: Material( - child: TimetableStyleProv( - child: TimetableWeeklyScreenshotFilm( - config: config, - timetable: timetable, - weekIndex: weekIndex, - fullSize: fullSize, - ), - ), - ), - ), - ), - delay: const Duration(milliseconds: 100), - context: context, - pixelRatio: View.of(context).devicePixelRatio, - ); - final imgFi = Files.timetable.screenshotFile; - await imgFi.writeAsBytes(screenshot); - - await OpenFile.open(imgFi.path, type: "image/png"); -} diff --git a/lib/timetable/page/timetable.dart b/lib/timetable/page/timetable.dart deleted file mode 100644 index 27fa2f03f..000000000 --- a/lib/timetable/page/timetable.dart +++ /dev/null @@ -1,298 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; -import 'package:go_router/go_router.dart'; -import 'package:sit/design/adaptive/dialog.dart'; -import 'package:sit/design/adaptive/foundation.dart'; -import 'package:sit/design/widgets/fab.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:sit/l10n/time.dart'; -import 'package:sit/settings/settings.dart'; -import 'package:sit/timetable/page/p13n.dart'; -import 'package:sit/timetable/page/screenshot.dart'; -import 'package:sit/school/i18n.dart' as $school; -import 'package:sit/life/i18n.dart' as $life; -import '../entity/display.dart'; -import '../events.dart'; -import '../i18n.dart'; -import '../entity/timetable.dart'; -import '../init.dart'; -import '../entity/pos.dart'; -import '../widgets/timetable/board.dart'; -import 'background.dart'; -import 'cell_style.dart'; - -class TimetableBoardPage extends StatefulWidget { - final SitTimetableEntity timetable; - - const TimetableBoardPage({super.key, required this.timetable}); - - @override - State createState() => _TimetableBoardPageState(); -} - -class _TimetableBoardPageState extends State { - final scrollController = ScrollController(); - final $displayMode = ValueNotifier(TimetableInit.storage.lastDisplayMode ?? DisplayMode.weekly); - late final ValueNotifier $currentPos; - - SitTimetableEntity get timetable => widget.timetable; - - @override - void initState() { - super.initState(); - $displayMode.addListener(() { - TimetableInit.storage.lastDisplayMode = $displayMode.value; - }); - $currentPos = ValueNotifier(timetable.type.locate(DateTime.now())); - } - - @override - void dispose() { - scrollController.dispose(); - $displayMode.dispose(); - $currentPos.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - resizeToAvoidBottomInset: false, - appBar: AppBar( - title: $currentPos >> (ctx, pos) => i18n.weekOrderedName(number: pos.weekIndex + 1).text(), - actions: [ - buildSwitchViewButton(), - buildMoreActionsButton(), - buildMyTimetablesButton(), - ], - ), - floatingActionButton: InkWell( - onLongPress: () async { - if ($displayMode.value == DisplayMode.weekly) { - await selectWeeklyTimetablePageToJump(); - } else { - await selectDailyTimetablePageToJump(); - } - }, - child: AutoHideFAB( - controller: scrollController, - child: const Icon(Icons.undo_rounded), - onPressed: () async { - final today = timetable.type.locate(DateTime.now()); - if ($currentPos.value != today) { - eventBus.fire(JumpToPosEvent(today)); - } - }, - ), - ), - body: TimetableBoard( - timetable: timetable, - $displayMode: $displayMode, - $currentPos: $currentPos, - ), - ); - } - - Widget buildSwitchViewButton() { - return $displayMode >> - (ctx, mode) => SegmentedButton( - showSelectedIcon: false, - style: ButtonStyle( - padding: MaterialStateProperty.all(const EdgeInsets.symmetric(horizontal: 4)), - visualDensity: VisualDensity.compact, - ), - segments: DisplayMode.values - .map((e) => ButtonSegment( - value: e, - label: e.l10n().text(), - )) - .toList(), - selected: {mode}, - onSelectionChanged: (newSelection) { - $displayMode.value = mode.toggle(); - }, - ); - } - - Widget buildMyTimetablesButton() { - return IconButton( - icon: const Icon(Icons.person_rounded), - onPressed: () async { - final focusMode = Settings.focusTimetable; - if (focusMode) { - await context.push("/me"); - } else { - await context.push("/timetable/mine"); - } - }, - ); - } - - Widget buildMoreActionsButton() { - final focusMode = Settings.focusTimetable; - return PopupMenuButton( - position: PopupMenuPosition.under, - padding: EdgeInsets.zero, - itemBuilder: (ctx) => [ - PopupMenuItem( - child: ListTile( - leading: const Icon(Icons.screenshot), - title: i18n.screenshot.screenshot.text(), - ), - onTap: () async { - await takeTimetableScreenshot( - context: context, - timetable: timetable, - weekIndex: $currentPos.value.weekIndex, - ); - }, - ), - if (focusMode) - PopupMenuItem( - child: ListTile( - leading: const Icon(Icons.calendar_month_outlined), - title: i18n.mine.title.text(), - ), - onTap: () async { - await context.push("/timetable/mine"); - }, - ), - PopupMenuItem( - child: ListTile( - leading: const Icon(Icons.palette_outlined), - title: i18n.p13n.palette.title.text(), - ), - onTap: () async { - await context.show$Sheet$((ctx) => const TimetableP13nPage()); - }, - ), - PopupMenuItem( - child: ListTile( - leading: const Icon(Icons.view_comfortable_outlined), - title: i18n.p13n.cell.title.text(), - ), - onTap: () async { - await context.show$Sheet$((ctx) => const TimetableCellStyleEditor()); - }, - ), - PopupMenuItem( - child: ListTile( - leading: const Icon(Icons.image_outlined), - title: i18n.p13n.background.title.text(), - ), - onTap: () async { - await context.show$Sheet$((ctx) => const TimetableBackgroundEditor()); - }, - ), - if (focusMode) ...buildFocusPopupActions(), - const PopupMenuDivider(), - CheckedPopupMenuItem( - checked: focusMode, - child: ListTile( - title: i18n.focusTimetable.text(), - ), - onTap: () async { - Settings.focusTimetable = !focusMode; - }, - ), - ], - ); - } - - List buildFocusPopupActions() { - return [ - const PopupMenuDivider(), - PopupMenuItem( - child: ListTile( - leading: const Icon(Icons.school_outlined), - title: $school.i18n.navigation.text(), - ), - onTap: () async { - await context.push("/school"); - }, - ), - PopupMenuItem( - child: ListTile( - leading: const Icon(Icons.spa_outlined), - title: $life.i18n.navigation.text(), - ), - onTap: () async { - await context.push("/life"); - }, - ), - ]; - } - - Future selectWeeklyTimetablePageToJump() async { - final initialIndex = $currentPos.value.weekIndex; - final controller = FixedExtentScrollController(initialItem: initialIndex); - final todayPos = timetable.type.locate(DateTime.now()); - final todayIndex = todayPos.weekIndex; - final week2Go = await context.showPicker( - count: 20, - controller: controller, - ok: i18n.jump, - okEnabled: (curSelected) => curSelected != initialIndex, - actions: [ - (ctx, curSelected) => PlatformTextButton( - onPressed: (curSelected == todayIndex) - ? null - : () { - controller.animateToItem(todayIndex, - duration: const Duration(milliseconds: 500), curve: Curves.fastEaseInToSlowEaseOut); - }, - child: i18n.findToday.text(), - ) - ], - make: (ctx, i) { - return Text(i18n.weekOrderedName(number: i + 1)); - }, - ) ?? - initialIndex; - controller.dispose(); - if (week2Go != initialIndex) { - eventBus.fire(JumpToPosEvent($currentPos.value.copyWith(weekIndex: week2Go))); - } - } - - Future selectDailyTimetablePageToJump() async { - final currentPos = $currentPos.value; - final initialWeekIndex = currentPos.weekIndex; - final initialDayIndex = currentPos.weekday.index; - final $week = FixedExtentScrollController(initialItem: initialWeekIndex); - final $day = FixedExtentScrollController(initialItem: initialDayIndex); - final todayPos = timetable.type.locate(DateTime.now()); - final todayWeekIndex = todayPos.weekIndex; - final todayDayIndex = todayPos.weekday.index; - final (week2Go, day2Go) = await context.showDualPicker( - countA: 20, - countB: 7, - controllerA: $week, - controllerB: $day, - ok: i18n.jump, - okEnabled: (weekSelected, daySelected) => weekSelected != initialWeekIndex || daySelected != initialDayIndex, - actions: [ - (ctx, week, day) => PlatformTextButton( - onPressed: (week == todayWeekIndex && day == todayDayIndex) - ? null - : () { - $week.animateToItem(todayWeekIndex, - duration: const Duration(milliseconds: 500), curve: Curves.fastEaseInToSlowEaseOut); - - $day.animateToItem(todayDayIndex, - duration: const Duration(milliseconds: 500), curve: Curves.fastEaseInToSlowEaseOut); - }, - child: i18n.findToday.text(), - ) - ], - makeA: (ctx, i) => i18n.weekOrderedName(number: i + 1).text(), - makeB: (ctx, i) => Weekday.fromIndex(i).l10n().text(), - ) ?? - (initialWeekIndex, initialDayIndex); - $week.dispose(); - $day.dispose(); - if (week2Go != initialWeekIndex || day2Go != initialDayIndex) { - eventBus.fire(JumpToPosEvent(TimetablePos(weekIndex: week2Go, weekday: Weekday.fromIndex(day2Go)))); - } - } -} diff --git a/lib/timetable/platte.dart b/lib/timetable/platte.dart deleted file mode 100644 index c465a5ed4..000000000 --- a/lib/timetable/platte.dart +++ /dev/null @@ -1,161 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:sit/timetable/entity/platte.dart'; - -import 'entity/timetable.dart'; - -/// https://m3.material.io/theme-builder#/custom -class BuiltinTimetablePalettes { - static const classic = BuiltinTimetablePalette( - id: -1, - key: "classic", - author: "Li_plum@outlook.com", - colors: [ - (light: Color(0xD285e779), dark: Color(0xDF21520f)), // green - (light: Color(0xD2c3e8ff), dark: Color(0xDF004c68)), // sky - (light: Color(0xD2ffa6bb), dark: Color(0xDF8e2f56)), // pink - (light: Color(0xD2ad9bd7), dark: Color(0xDF50378a)), // violet - (light: Color(0xD2ff9d6b), dark: Color(0xDF7f2b00)), // orange - (light: Color(0xD2ffa2d2), dark: Color(0xDF8e0032)), // rose - (light: Color(0xD2ffd200), dark: Color(0xDF523900)), // lemon - (light: Color(0xD275f8e2), dark: Color(0xDF005047)), // cyan - (light: Color(0xD2b4ebff), dark: Color(0xDF004e5f)), // ice - (light: Color(0xD2b4ebff), dark: Color(0xDF004e5f)), // cyan - (light: Color(0xD2ffd7f5), dark: Color(0xDF7c157a)), // mauve - (light: Color(0xD2eaf141), dark: Color(0xDF4b4c00)), // toxic - ], - ); - static const americano = BuiltinTimetablePalette( - id: -2, - key: "americano", - author: "Gracie", - colors: [ - (dark: Color(0xff4d5a3f), light: Color(0xe67a8d62)), - (dark: Color(0xff837a69), light: Color(0xffedddbe)), - (dark: Color(0xff6a3634), light: Color(0xe6c86563)), - (dark: Color(0xff98814c), light: Color(0xffe7c574)), - (dark: Color(0xff14506a), light: Color(0xe60080ad)), - ], - ); - static const candy = BuiltinTimetablePalette( - id: -3, - key: "candy", - author: "Gracie", - colors: [ - (dark: Color(0xff877878), light: Color(0xfff1dada)), - (dark: Color(0xff668076), light: Color(0xffb9e8d7)), - (dark: Color(0xff6e7760), light: Color(0xffc9d8af)), - (dark: Color(0xffa36665), light: Color(0xfff2a09e)), - (dark: Color(0xff676f7f), light: Color(0xffbbc9e6)), - (dark: Color(0xff786e7a), light: Color(0xffdac8dd)), - (dark: Color(0xff87786e), light: Color(0xfff2d8c8)), - ], - ); - static const spring = BuiltinTimetablePalette( - id: -4, - key: "sprint", - author: "Gracie", - colors: [ - (dark: Color(0xff6c8081), light: Color(0xffa5c2c4)), - (dark: Color(0xff88877c), light: Color(0xfff3f1e1)), - (dark: Color(0xff715252), light: Color(0xffae7e7e)), - (dark: Color(0xff799995), light: Color(0xffbae5df)), - (dark: Color(0xff799279), light: Color(0xffb9dbb9)), - (dark: Color(0xff907f6d), light: Color(0xffffe4c8)), - ], - ); - static const summary = BuiltinTimetablePalette( - id: -5, - key: "summary", - author: "Gracie", - colors: [ - (dark: Color(0xffaca88b), light: Color(0xfffffad3)), - (dark: Color(0xff976455), light: Color(0xffe69782)), - (dark: Color(0xff7ca07f), light: Color(0xffbeedc3)), - (dark: Color(0xff3c5b51), light: Color(0xff5e8f7f)), - (dark: Color(0xff93997b), light: Color(0xffdce4bd)), - (dark: Color(0xffa78044), light: Color(0xffffc367)), - ], - ); - static const fall = BuiltinTimetablePalette( - id: -6, - key: "fall", - author: "Gracie", - colors: [ - (dark: Color(0xff9e9c7e), light: Color(0xffece9c1)), - (dark: Color(0xff977955), light: Color(0xffe6b982)), - (dark: Color(0xff8e8471), light: Color(0xffd5c6af)), - (dark: Color(0xff626a48), light: Color(0xff97a470)), - (dark: Color(0xff6e5c46), light: Color(0xffaa8f6c)), - (dark: Color(0xff96563a), light: Color(0xffe68358)), - ], - ); - static const winter = BuiltinTimetablePalette( - id: -7, - key: "winter", - author: "Gracie", - colors: [ - (dark: Color(0xff7c8787), light: Color(0xffc3dede)), - (dark: Color(0xff7e7f6d), light: Color(0xffe5e6c4)), - (dark: Color(0xff4d6067), light: Color(0xff90b2c0)), - (dark: Color(0xff4e6f6d), light: Color(0xff8fcdca)), - (dark: Color(0xff5e6d5e), light: Color(0xffabc8ad)), - (dark: Color(0xff4c5253), light: Color(0xffb9c6c9)), - ], - ); - static const thicket = BuiltinTimetablePalette( - id: -8, - key: "thicket", - author: "Gracie", - colors: [ - (dark: Color(0xff506952), light: Color(0xff7da37f)), - (dark: Color(0xff547b65), light: Color(0xff81bc95)), - (dark: Color(0xff465753), light: Color(0xff6e8882)), - (dark: Color(0xff7b978d), light: Color(0xffbce2d4)), - (dark: Color(0xff9e948a), light: Color(0xffecdfd0)), - ], - ); - static const creeksideBreeze = BuiltinTimetablePalette( - id: -9, - key: "creeksideBreeze", - author: "Gracie", - colors: [ - (dark: Color(0xff6c8080), light: Color(0xffc4e7e7)), - (dark: Color(0xff748e87), light: Color(0xffdcf4f1)), - (dark: Color(0xff3e5657), light: Color(0xff77a3a5)), - (dark: Color(0xff7c726f), light: Color(0xffbcada9)), - (dark: Color(0xff5d5a5a), light: Color(0xfff4ecec)), - ], - ); - - static const all = [ - classic, - americano, - candy, - spring, - summary, - fall, - winter, - thicket, - creeksideBreeze, - ]; -} - -extension TimetablePlatteX on TimetablePalette { - Color2Mode resolveColor(SitCourse course) { - assert(colors.isNotEmpty, "Colors can't be empty"); - if (colors.isEmpty) return (light: Colors.white, dark: Colors.black); - return colors[course.courseCode.hashCode.abs() % colors.length]; - } - - Color2Mode safeGetColor(int index) { - assert(colors.isNotEmpty, "Colors can't be empty"); - if (colors.isEmpty) return (light: Colors.white, dark: Colors.black); - return colors[index % colors.length]; - } -} - -extension Color2ModeX on Color2Mode { - Color byTheme(ThemeData theme) => theme.brightness == Brightness.dark ? dark : light; - - Color byBrightness(Brightness brightness) => brightness == Brightness.dark ? dark : light; -} diff --git a/lib/timetable/service/school.dart b/lib/timetable/service/school.dart deleted file mode 100644 index 8803bf4be..000000000 --- a/lib/timetable/service/school.dart +++ /dev/null @@ -1,105 +0,0 @@ -import 'package:beautiful_soup_dart/beautiful_soup.dart'; -import 'package:dio/dio.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:sit/init.dart'; - -import 'package:sit/school/entity/school.dart'; -import 'package:sit/school/exam_result/init.dart'; -import 'package:sit/session/gms.dart'; -import 'package:sit/session/jwxt.dart'; -import 'package:sit/settings/settings.dart'; - -import '../entity/course.dart'; -import '../entity/timetable.dart'; -import '../utils.dart'; - -class TimetableService { - static const _undergraduateTimetableUrl = 'http://jwxt.sit.edu.cn/jwglxt/kbcx/xskbcx_cxXsgrkb.html'; - static const _postgraduateTimetableUrl = - 'http://gms.sit.edu.cn/epstar/yjs/T_PYGL_KWGL_WSXK/T_PYGL_KWGL_WSXK_XSKB_NEW.jsp'; - - JwxtSession get jwxtSession => Init.jwxtSession; - - GmsSession get gmsSession => Init.gmsSession; - - const TimetableService(); - - /// 获取本科生课表 - Future getUgTimetable(SemesterInfo info) async { - final response = await jwxtSession.request( - _undergraduateTimetableUrl, - options: Options( - method: "POST", - ), - para: {'gnmkdm': 'N253508'}, - data: { - // 学年名 - 'xnm': info.exactYear.toString(), - // 学期名 - 'xqm': semesterToFormField(info.semester) - }, - ); - final json = response.data; - final List courseList = json['kbList']; - final rawCourses = courseList.map((e) => UndergraduateCourseRaw.fromJson(e)).toList(); - final timetableEntity = parseUndergraduateTimetableFromCourseRaw(rawCourses); - return timetableEntity; - } - - /// 获取研究生课表 - Future getPgTimetable(SemesterInfo info) async { - final timetableRes = await gmsSession.request( - _postgraduateTimetableUrl, - options: Options( - method: "POST", - ), - data: { - "excel": "true", - "XQDM": _toPgSemesterText(info), - }, - ); - final scoreList = await ExamResultInit.pgService.fetchResultRawList(); - final courseList = parsePostgraduateCourseRawsFromHtml(timetableRes.data); - completePostgraduateCourseRawsFromPostgraduateScoreRaws(courseList, scoreList); - final timetableEntity = parsePostgraduateTimetableFromCourseRaw( - courseList, - campus: Settings.campus, - ); - return timetableEntity; - } - - String _toPgSemesterText(SemesterInfo info) { - assert(info.semester != Semester.all); - if (info.semester == Semester.term1) { - return "${info.exactYear}09"; - } else { - return "${info.exactYear + 1}02"; - } - } - - Future<({DateTime start, DateTime end})?> getUgSemesterSpan() async { - final res = await jwxtSession.request( - "http://jwxt.sit.edu.cn/jwglxt/xtgl/index_cxAreaFive.html", - options: Options( - method: "POST", - ), - ); - return _parseSemesterSpan(res.data); - } - - static final _semesterSpanRe = RegExp(r"\((\S+)至(\S+)\)"); - static final _semesterSpanDateFormat = DateFormat("yyyy-MM-dd"); - - ({DateTime start, DateTime end})? _parseSemesterSpan(String content) { - final html = BeautifulSoup(content); - final element = html.find("th", attrs: {"style": "text-align: center"}); - if (element == null) return null; - final text = element.text; - final match = _semesterSpanRe.firstMatch(text); - if (match == null) return null; - final start = _semesterSpanDateFormat.tryParse(match.group(1) ?? ""); - final end = _semesterSpanDateFormat.tryParse(match.group(2) ?? ""); - if (start == null || end == null) return null; - return (start: start, end: end); - } -} diff --git a/lib/timetable/settings.dart b/lib/timetable/settings.dart deleted file mode 100644 index a529ae993..000000000 --- a/lib/timetable/settings.dart +++ /dev/null @@ -1,94 +0,0 @@ -import 'dart:convert'; - -import 'package:flutter/foundation.dart'; -import 'package:hive_flutter/hive_flutter.dart'; -import 'package:sit/timetable/entity/background.dart'; - -import 'widgets/style.dart'; - -const _kAutoUseImported = true; -const _kShowTeachers = true; -const _kGrayOutTakenLessons = false; -const _kHarmonizeWithThemeColor = true; -const _kAlpha = 1.0; - -class _K { - static const ns = "/timetable"; - static const autoUseImported = "$ns/autoUseImported"; - static const backgroundImage = "$ns/backgroundImage"; -} - -class TimetableSettings { - final Box box; - - TimetableSettings(this.box); - - late final cell = _Cell(box); - - bool get autoUseImported => box.get(_K.autoUseImported) ?? _kAutoUseImported; - - set autoUseImported(bool newV) => box.put(_K.autoUseImported, newV); - - BackgroundImage? get backgroundImage { - final json = box.get(_K.backgroundImage); - if (json == null) return null; - return BackgroundImage.fromJson(jsonDecode(json)); - } - - set backgroundImage(BackgroundImage? newV) { - box.put(_K.backgroundImage, newV == null ? null : jsonEncode(newV.toJson())); - } - - ValueListenable listenBackgroundImage() => box.listenable(keys: [_K.backgroundImage]); -} - -class _CellK { - static const ns = "${_K.ns}/cell"; - static const showTeachers = "$ns/showTeachers"; - static const grayOutTakenLessons = "$ns/grayOutTakenLessons"; - static const harmonizeWithThemeColor = "$ns/harmonizeWithThemeColor"; - static const alpha = "$ns/alpha"; -} - -class _Cell { - final Box box; - - const _Cell(this.box); - - bool get showTeachers => box.get(_CellK.showTeachers) ?? _kShowTeachers; - - set showTeachers(bool newV) => box.put(_CellK.showTeachers, newV); - - bool get grayOutTakenLessons => box.get(_CellK.grayOutTakenLessons) ?? _kGrayOutTakenLessons; - - set grayOutTakenLessons(bool newV) => box.put(_CellK.grayOutTakenLessons, newV); - - bool get harmonizeWithThemeColor => box.get(_CellK.harmonizeWithThemeColor) ?? _kHarmonizeWithThemeColor; - - set harmonizeWithThemeColor(bool newV) => box.put(_CellK.harmonizeWithThemeColor, newV); - - double get alpha => box.get(_CellK.alpha) ?? _kAlpha; - - set alpha(double newV) => box.put(_CellK.alpha, newV); - - ValueListenable listenStyle() => box.listenable(keys: [ - _CellK.showTeachers, - _CellK.grayOutTakenLessons, - _CellK.harmonizeWithThemeColor, - _CellK.alpha, - ]); - - CourseCellStyle get cellStyle => CourseCellStyle( - showTeachers: showTeachers, - grayOutTakenLessons: grayOutTakenLessons, - harmonizeWithThemeColor: harmonizeWithThemeColor, - alpha: alpha, - ); - - set cellStyle(CourseCellStyle newV) { - showTeachers = newV.showTeachers; - grayOutTakenLessons = newV.grayOutTakenLessons; - harmonizeWithThemeColor = newV.harmonizeWithThemeColor; - alpha = newV.alpha; - } -} diff --git a/lib/timetable/storage/timetable.dart b/lib/timetable/storage/timetable.dart deleted file mode 100644 index 7bab7c169..000000000 --- a/lib/timetable/storage/timetable.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:hive_flutter/hive_flutter.dart'; -import 'package:sit/storage/hive/init.dart'; -import 'package:sit/storage/hive/table.dart'; -import 'package:sit/timetable/entity/timetable.dart'; - -import '../entity/display.dart'; -import '../entity/platte.dart'; -import '../platte.dart'; - -class _K { - static const timetable = "/timetable"; - static const lastDisplayMode = "/lastDisplayMode"; - static const palette = "/palette"; -} - -class TimetableStorage { - Box get box => HiveInit.timetable; - - final HiveTable timetable; - final HiveTable palette; - - TimetableStorage() - : timetable = HiveTable( - base: _K.timetable, - box: HiveInit.timetable, - useJson: (fromJson: SitTimetable.fromJson, toJson: (timetable) => timetable.toJson()), - ), - palette = HiveTable( - base: _K.palette, - box: HiveInit.timetable, - useJson: (fromJson: TimetablePalette.fromJson, toJson: (palette) => palette.toJson()), - get: (id, builtin) { - // intercept builtin timetable - for (final timetable in BuiltinTimetablePalettes.all) { - if (timetable.id == id) return timetable; - } - return builtin(id); - }, - set: (id, newV, builtin) { - // skip builtin timetable - for (final timetable in BuiltinTimetablePalettes.all) { - if (timetable.id == id) return; - } - builtin(id, newV); - }, - ); - - DisplayMode? get lastDisplayMode => DisplayMode.at(box.get(_K.lastDisplayMode)); - - set lastDisplayMode(DisplayMode? newValue) => box.put(_K.lastDisplayMode, newValue?.index); -} diff --git a/lib/timetable/utils.dart b/lib/timetable/utils.dart deleted file mode 100644 index 7c2e87170..000000000 --- a/lib/timetable/utils.dart +++ /dev/null @@ -1,485 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:file_picker/file_picker.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; -import 'package:ical/serializer.dart'; -import 'package:open_file/open_file.dart'; -import 'package:sit/design/adaptive/multiplatform.dart'; -import 'package:sit/entity/campus.dart'; -import 'package:sit/files.dart'; -import 'package:sit/l10n/extension.dart'; -import 'package:sit/l10n/time.dart'; -import 'package:sit/school/entity/school.dart'; -import 'package:sanitize_filename/sanitize_filename.dart'; -import 'package:share_plus/share_plus.dart'; -import 'package:sit/school/utils.dart'; -import 'package:sit/school/entity/timetable.dart'; -import 'package:sit/utils/strings.dart'; -import 'package:universal_platform/universal_platform.dart'; -import '../school/exam_result/entity/result.pg.dart'; -import 'entity/timetable.dart'; - -import 'entity/course.dart'; -import 'dart:math'; - -import 'init.dart'; - -import 'page/export.dart'; -import 'package:html/parser.dart'; - -const maxWeekLength = 20; - -final Map _weekday2Index = { - '星期一': 0, - '星期二': 1, - '星期三': 2, - '星期四': 3, - '星期五': 4, - '星期六': 5, - '星期日': 6, -}; - -/// Then the [weekText] could be `1-5周,14周,8-10周(单)` -/// The return value should be -/// ```dart -/// TimetableWeekIndices([ -/// TimetableWeekIndex.all( -/// (start: 0, end: 4) -/// ), -/// TimetableWeekIndex.single( -/// 13, -/// ), -/// TimetableWeekIndex.odd( -/// (start: 7, end: 9), -/// ), -/// ]) -/// ``` -TimetableWeekIndices _parseWeekText2RangedNumbers( - String weekText, { - required String allSuffix, - required String oddSuffix, - required String evenSuffix, -}) { - final weeks = weekText.split(','); -// Then the weeks should be ["1-5周","14周","8-10周(单)"] - final indices = []; - for (final week in weeks) { - // odd week - if (week.endsWith(oddSuffix)) { - final rangeText = week.removeSuffix(oddSuffix); - final range = rangeFromString(rangeText, number2index: true); - indices.add(TimetableWeekIndex.odd(range)); - } else if (week.endsWith(evenSuffix)) { - final rangeText = week.removeSuffix(evenSuffix); - final range = rangeFromString(rangeText, number2index: true); - indices.add(TimetableWeekIndex.even(range)); - } else if (week.endsWith(allSuffix)) { - final numberText = week.removeSuffix(allSuffix); - final range = rangeFromString(numberText, number2index: true); - indices.add(TimetableWeekIndex.all(range)); - } - } - return TimetableWeekIndices(indices); -} - -Campus _parseCampus(String campus) { - if (campus.contains("徐汇")) { - return Campus.xuhui; - } else { - return Campus.fengxian; - } -} - -SitTimetable parseUndergraduateTimetableFromCourseRaw(List all) { - final courseKey2Entity = {}; - var counter = 0; - for (final raw in all) { - final courseKey = counter++; - final weekIndices = _parseWeekText2RangedNumbers( - mapChinesePunctuations(raw.weekText), - allSuffix: "周", - oddSuffix: "周(单)", - evenSuffix: "周(双)", - ); - final dayIndex = _weekday2Index[raw.weekDayText]; - assert(dayIndex != null && 0 <= dayIndex && dayIndex < 7, "dayIndex isn't in range [0,6] but $dayIndex"); - if (dayIndex == null || !(0 <= dayIndex && dayIndex < 7)) continue; - final timeslots = rangeFromString(raw.timeslotsText, number2index: true); - assert(timeslots.start <= timeslots.end, "${timeslots.start} > ${timeslots.end} actually. ${raw.courseName}"); - final course = SitCourse( - courseKey: courseKey, - courseName: mapChinesePunctuations(raw.courseName).trim(), - courseCode: raw.courseCode.trim(), - classCode: raw.classCode.trim(), - campus: _parseCampus(raw.campus), - place: reformatPlace(mapChinesePunctuations(raw.place)), - weekIndices: weekIndices, - timeslots: timeslots, - courseCredit: double.tryParse(raw.courseCredit) ?? 0.0, - dayIndex: dayIndex, - teachers: raw.teachers.split(","), - ); - courseKey2Entity["$courseKey"] = course; - } - final res = SitTimetable( - courses: courseKey2Entity, - lastCourseKey: counter, - name: "", - startDate: DateTime.utc(0), - schoolYear: 0, - semester: Semester.term1, - ); - return res; -} - -SitTimetableEntity resolveTimetableEntity(SitTimetable timetable) { - final weeks = List.generate(20, (index) => SitTimetableWeek.$7days(index)); - - for (final course in timetable.courses.values) { - final timeslots = course.timeslots; - for (final weekIndex in course.weekIndices.getWeekIndices()) { - assert( - 0 <= weekIndex && weekIndex < maxWeekLength, - "Week index is more out of range [0,$maxWeekLength) but $weekIndex.", - ); - if (0 <= weekIndex && weekIndex < maxWeekLength) { - final week = weeks[weekIndex]; - final day = week.days[course.dayIndex]; - final thatDay = reflectWeekDayIndexToDate( - weekIndex: week.index, - weekday: Weekday.fromIndex(day.index), - startDate: timetable.startDate, - ); - final fullClassTime = course.calcBeginEndTimePoint(); - final lesson = SitTimetableLesson( - course: course, - startIndex: timeslots.start, - endIndex: timeslots.end, - startTime: thatDay.addTimePoint(fullClassTime.begin), - endTime: thatDay.addTimePoint(fullClassTime.end), - ); - for (int slot = timeslots.start; slot <= timeslots.end; slot++) { - final classTime = course.calcBeginEndTimePointOfLesson(slot); - day.add( - at: slot, - lesson: SitTimetableLessonPart( - type: lesson, - index: slot, - startTime: thatDay.addTimePoint(classTime.begin), - endTime: thatDay.addTimePoint(classTime.end), - ), - ); - } - } - } - } - return SitTimetableEntity( - type: timetable, - weeks: weeks, - ); -} - -Duration calcuSwitchAnimationDuration(num distance) { - final time = sqrt(max(1, distance) * 100000); - return Duration(milliseconds: time.toInt()); -} - -Future<({int id, SitTimetable timetable})?> importTimetableFromFile() async { - final result = await FilePicker.platform.pickFiles( - // Cannot limit the extensions. My RedMi phone just reject all files. - // type: FileType.custom, - // allowedExtensions: const ["timetable", "json"], - ); - if (result == null) return null; - final content = await _readTimetableFi(result.files.single); - if (content == null) return null; - final json = jsonDecode(content); - final timetable = SitTimetable.fromJson(json); - final id = TimetableInit.storage.timetable.add(timetable); - return (id: id, timetable: timetable); -} - -Future _readTimetableFi(PlatformFile fi) async { - if (kIsWeb) { - final bytes = fi.bytes; - return bytes == null ? null : String.fromCharCodes(bytes); - } else { - final path = fi.path; - if (path == null) return null; - final file = File(path); - return await file.readAsString(); - } -} - -Future exportTimetableFileAndShare( - SitTimetable timetable, { - required BuildContext context, -}) async { - final content = jsonEncode(timetable.toJson()); - var fileName = "${timetable.name}.timetable"; - if (timetable.signature.isNotEmpty) { - fileName = "${timetable.signature} $fileName"; - } - fileName = sanitizeFilename(fileName, replacement: "-"); - final timetableFi = Files.temp.subFile(fileName); - final sharePositionOrigin = context.getSharePositionOrigin(); - await timetableFi.writeAsString(content); - await Share.shareXFiles( - [XFile(timetableFi.path)], - sharePositionOrigin: sharePositionOrigin, - ); -} - -Future exportTimetableAsICalendarAndOpen( - BuildContext context, { - required SitTimetableEntity timetable, - required TimetableExportCalendarConfig config, -}) async { - final name = "${timetable.type.name}, ${context.formatYmdNum(timetable.type.startDate)}"; - final fileName = sanitizeFilename( - UniversalPlatform.isAndroid ? "$name #${DateTime.now().millisecondsSinceEpoch ~/ 1000}.ics" : "$name.ics", - replacement: "-", - ); - final calendarFi = Files.timetable.calendarDir.subFile(fileName); - final data = convertTimetable2ICal(timetable: timetable, config: config); - await calendarFi.writeAsString(data); - await OpenFile.open(calendarFi.path, type: "text/calendar"); -} - -String convertTimetable2ICal({ - required SitTimetableEntity timetable, - required TimetableExportCalendarConfig config, -}) { - final calendar = ICalendar( - company: 'mysit.life', - product: 'SIT Life', - lang: config.locale?.toLanguageTag() ?? "EN", - ); - final alarm = config.alarm; - final merged = config.isLessonMerged; - for (final week in timetable.weeks) { - for (final day in week.days) { - for (final lessonSlot in day.timeslot2LessonSlot) { - for (final part in lessonSlot.lessons) { - final course = part.course; - final teachers = course.teachers.join(', '); - final lesson = part.type; - final startTime = (merged ? lesson.startTime : part.startTime).toUtc(); - final endTime = (merged ? lesson.endTime : part.endTime).toUtc(); - final uid = merged - ? "${R.appId}.${course.courseCode}.${week.index}.${day.index}.${lesson.startIndex}-${lesson.endIndex}" - : "${R.appId}.${course.courseCode}.${week.index}.${day.index}.${part.index}"; - // Use UTC - final event = IEvent( - uid: uid, - summary: course.courseName, - location: course.place, - description: teachers, - start: startTime, - end: endTime, - // DON'T USE duration, that is broken on iOS. - // duration: part.calcuClassDuration().toDuration(), - alarm: alarm == null - ? null - : alarm.isSoundAlarm - ? IAlarm.audio( - trigger: startTime.subtract(alarm.alarmBeforeClass).toUtc(), - duration: alarm.alarmDuration, - ) - : IAlarm.display( - trigger: startTime.subtract(alarm.alarmBeforeClass).toUtc(), - description: "${course.courseName} ${course.place} $teachers", - duration: alarm.alarmDuration, - ), - ); - calendar.addElement(event); - if (merged) { - // skip the `lessonParts` loop - break; - } - } - } - } - } - return calendar.serialize(); -} - -List parsePostgraduateCourseRawsFromHtml(String timetableHtmlContent) { - List> generateTimetable() { - List> timetable = []; - for (int i = 0; i < 9; i++) { - List timeslots = List.generate(14, (index) => -1); - timetable.add(timeslots); - } - return timetable; - } - - List courseList = []; - List> timetable = generateTimetable(); - const mapOfWeekday = ['星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日']; - final courseCodeRegExp = RegExp(r"(.*?)(学硕\d+班|专硕\d+班|\d+班)$"); - final weekTextRegExp = RegExp(r"([\d-]+周(\([^)]*\))?)([\d-]+节)"); - - int parseWeekdayCodeFromIndex(int index, int row, {bool isFirst = false, int rowspan = 1}) { - if (!isFirst) { - index = index + 1; - } - for (int i = 0; i <= index; i++) { - if (timetable[i][row] != -1 && timetable[i][row] != row) { - index++; - } - } - for (int r = 0; r < rowspan; r++) { - timetable[index][row + r] = row; - } - return index - 2; - } - - void processNodes(List nodes, String weekday) { - if (nodes.length < 5) { - // 如果节点数量小于 5,不足以构成一个完整的 Course,忽略 - return; - } - - final locationWithTeacherStr = mapChinesePunctuations(nodes[4].text); - final locationWithTeacherList = locationWithTeacherStr.split(" "); - final location = locationWithTeacherList[0]; - final teacher = locationWithTeacherList[1]; - - var courseNameWithClassCode = mapChinesePunctuations(nodes[0].text); - final String courseName; - final String classCode; - RegExpMatch? courseNameWithClassCodeMatch = courseCodeRegExp.firstMatch(courseNameWithClassCode); - if (courseNameWithClassCodeMatch != null) { - courseName = courseNameWithClassCodeMatch.group(1) ?? ""; - classCode = courseNameWithClassCodeMatch.group(2) ?? ""; - } else { - courseName = courseNameWithClassCode; - classCode = ""; - } - - var weekTextWithTimeslotsText = mapChinesePunctuations(nodes[2].text); - final String weekText; - final String timeslotsText; - RegExpMatch? weekTextWithTimeslotsTextMatch = weekTextRegExp.firstMatch(weekTextWithTimeslotsText); - if (weekTextWithTimeslotsTextMatch != null) { - weekText = weekTextWithTimeslotsTextMatch.group(1) ?? ""; - timeslotsText = weekTextWithTimeslotsTextMatch.group(3) ?? ""; - } else { - weekText = ""; - timeslotsText = ""; - } - - final course = PostgraduateCourseRaw( - courseName: courseName, - weekDayText: weekday, - weekText: weekText, - timeslotsText: timeslotsText, - teachers: teacher, - place: location, - classCode: classCode, - courseCode: "", - courseCredit: "", - creditHour: "", - ); - - courseList.add(course); - - // 移除处理过的节点,继续处理剩余的节点 - nodes.removeRange(0, 7); - - if (nodes.isNotEmpty) { - processNodes(nodes, weekday); - } - } - - final document = parse(timetableHtmlContent); - final table = document.querySelector('table'); - final trList = table!.querySelectorAll('tr'); - for (var tr in trList) { - final row = trList.indexOf(tr); - final tdList = tr.querySelectorAll('td'); - for (var td in tdList) { - String firstTdContent = tdList[0].text; - bool isFirst = const ["上午", "下午", "晚上"].contains(firstTdContent); - if (td.innerHtml.contains("br")) { - final index = tdList.indexOf(td); - final rowspan = int.parse(td.attributes["rowspan"] ?? "1"); - int weekdayCode = parseWeekdayCodeFromIndex(index, row, isFirst: isFirst, rowspan: rowspan); - String weekday = mapOfWeekday[weekdayCode]; - final nodes = td.nodes; - processNodes(nodes, weekday); - } - } - } - return courseList; -} - -void completePostgraduateCourseRawsFromPostgraduateScoreRaws( - List courseList, List scoreList) { - var name2Score = {}; - - for (var score in scoreList) { - var key = score.courseName.replaceAll(" ", ""); - name2Score[key] = score; - } - - for (var course in courseList) { - var key = course.courseName.replaceAll(" ", ""); - var score = name2Score[key]; - if (score != null) { - course.courseCode = score.courseCode; - course.courseCredit = score.credit; - } - } -} - -SitTimetable parsePostgraduateTimetableFromCourseRaw( - List all, { - required Campus campus, -}) { - final courseKey2Entity = {}; - var counter = 0; - for (final raw in all) { - final courseKey = counter++; - final weekIndices = _parseWeekText2RangedNumbers( - mapChinesePunctuations(raw.weekText), - allSuffix: "周", - oddSuffix: "周(单周)", - evenSuffix: "周(双周)", - ); - final dayIndex = _weekday2Index[raw.weekDayText]; - assert(dayIndex != null && 0 <= dayIndex && dayIndex < 7, "dayIndex isn't in range [0,6] but $dayIndex"); - if (dayIndex == null || !(0 <= dayIndex && dayIndex < 7)) continue; - final timeslotsText = raw.timeslotsText.endsWith("节") - ? raw.timeslotsText.substring(0, raw.timeslotsText.length - 1) - : raw.timeslotsText; - final timeslots = rangeFromString(timeslotsText, number2index: true); - assert(timeslots.start <= timeslots.end, "${timeslots.start} > ${timeslots.end} actually. ${raw.courseName}"); - final course = SitCourse( - courseKey: courseKey, - courseName: mapChinesePunctuations(raw.courseName).trim(), - courseCode: raw.courseCode.trim(), - classCode: raw.classCode.trim(), - campus: campus, - place: reformatPlace(mapChinesePunctuations(raw.place)), - weekIndices: weekIndices, - timeslots: timeslots, - courseCredit: double.tryParse(raw.courseCredit) ?? 0.0, - dayIndex: dayIndex, - teachers: raw.teachers.split(","), - ); - courseKey2Entity["$courseKey"] = course; - } - final res = SitTimetable( - courses: courseKey2Entity, - lastCourseKey: counter, - name: "", - startDate: DateTime.utc(0), - schoolYear: 0, - semester: Semester.term1, - ); - return res; -} diff --git a/lib/timetable/widgets/course.dart b/lib/timetable/widgets/course.dart deleted file mode 100644 index 144b1bcc4..000000000 --- a/lib/timetable/widgets/course.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:sit/design/widgets/card.dart'; -import 'package:sit/school/widgets/course.dart'; -import 'package:sit/timetable/entity/platte.dart'; -import 'package:sit/timetable/entity/timetable.dart'; -import 'package:sit/timetable/platte.dart'; - -class TimetableCourseCard extends StatelessWidget { - final SitCourse course; - final TimetablePalette? palette; - - const TimetableCourseCard( - this.course, { - super.key, - this.palette, - }); - - @override - Widget build(BuildContext context) { - return FilledCard( - child: ListTile( - isThreeLine: true, - leading: CourseIcon(courseName: course.courseName), - title: course.courseName.text(), - subtitle: [ - if (course.place.isNotEmpty) course.place.text(), - if (course.teachers.isNotEmpty) course.teachers.join(", ").text(), - ].column(caa: CrossAxisAlignment.start), - trailing: FilledCard( - color: palette?.resolveColor(course).byTheme(context.theme), - child: const SizedBox(width: 32, height: 32), - ), - ), - ); - } -} diff --git a/lib/timetable/widgets/free.dart b/lib/timetable/widgets/free.dart deleted file mode 100644 index a7817ae05..000000000 --- a/lib/timetable/widgets/free.dart +++ /dev/null @@ -1,149 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; -import 'package:sit/design/adaptive/dialog.dart'; -import 'package:sit/design/widgets/common.dart'; -import 'package:sit/l10n/time.dart'; -import 'package:sit/timetable/entity/timetable.dart'; -import 'package:rettulf/rettulf.dart'; -import '../entity/pos.dart'; -import '../events.dart'; -import '../i18n.dart'; - -class FreeDayTip extends StatelessWidget { - final SitTimetableEntity timetable; - final int weekIndex; - final Weekday weekday; - - const FreeDayTip({ - super.key, - required this.timetable, - required this.weekIndex, - required this.weekday, - }); - - @override - Widget build(BuildContext context) { - final todayPos = timetable.type.locate(DateTime.now()); - final isToday = todayPos.weekIndex == weekIndex && todayPos.weekday == weekday; - final String desc; - if (isToday) { - desc = i18n.freeTip.isTodayTip; - } else { - desc = i18n.freeTip.dayTip; - } - return LeavingBlank( - icon: Icons.free_cancellation_rounded, - desc: desc, - subtitle: PlatformTextButton( - onPressed: () async { - await jumpToNearestDayWithClass(context, weekIndex, weekday.index); - }, - child: i18n.freeTip.findNearestDayWithClass.text(), - ), - ); - } - - /// Find the nearest day with class forward. - /// No need to look back to passed days, unless there's no day after [weekIndex] and [dayIndex] that has any class. - Future jumpToNearestDayWithClass( - BuildContext ctx, - int weekIndex, - int dayIndex, - ) async { - for (int i = weekIndex; i < timetable.weeks.length; i++) { - final week = timetable.weeks[i]; - if (!week.isFree()) { - final dayIndexStart = weekIndex == i ? dayIndex : 0; - for (int j = dayIndexStart; j < week.days.length; j++) { - final day = week.days[j]; - if (day.hasAnyLesson()) { - eventBus.fire(JumpToPosEvent(TimetablePos(weekIndex: i, weekday: Weekday.fromIndex(j)))); - return; - } - } - } - } - // Now there's no class forward, so let's search backward. - for (int i = weekIndex; 0 <= i; i--) { - final week = timetable.weeks[i]; - if (!week.isFree()) { - final dayIndexStart = weekIndex == i ? dayIndex : week.days.length - 1; - for (int j = dayIndexStart; 0 <= j; j--) { - final day = week.days[j]; - if (day.hasAnyLesson()) { - eventBus.fire(JumpToPosEvent(TimetablePos(weekIndex: i, weekday: Weekday.fromIndex(j)))); - return; - } - } - } - } - // WHAT? NO CLASS IN THE WHOLE TERM? - // Alright, let's congratulate them! - if (!ctx.mounted) return; - await ctx.showTip(title: i18n.congratulations, desc: i18n.freeTip.termTip, ok: i18n.ok); - } -} - -class FreeWeekTip extends StatelessWidget { - final SitTimetableEntity timetable; - final int weekIndex; - - const FreeWeekTip({ - super.key, - required this.timetable, - required this.weekIndex, - }); - - @override - Widget build(BuildContext context) { - final String desc; - final todayPos = timetable.type.locate(DateTime.now()); - if (todayPos.weekIndex == weekIndex) { - desc = i18n.freeTip.isThisWeekTip; - } else { - desc = i18n.freeTip.weekTip; - } - return LeavingBlank( - icon: Icons.free_cancellation_rounded, - desc: desc, - subtitle: PlatformTextButton( - onPressed: () async { - await jumpToNearestWeekWithClass( - context, - weekIndex, - defaultWeekday: Weekday.monday, - ); - }, - child: i18n.freeTip.findNearestWeekWithClass.text(), - ), - ); - } - - /// Find the nearest week with class forward. - /// No need to look back to passed weeks, unless there's no week after [weekIndex] that has any class. - Future jumpToNearestWeekWithClass( - BuildContext ctx, - int weekIndex, { - required Weekday defaultWeekday, - }) async { - for (int i = weekIndex; i < timetable.weeks.length; i++) { - final week = timetable.weeks[i]; - if (!week.isFree()) { - eventBus.fire(JumpToPosEvent(TimetablePos(weekIndex: i, weekday: defaultWeekday))); - return; - } - } - // Now there's no class forward, so let's search backward. - for (int i = weekIndex; 0 <= i; i--) { - final week = timetable.weeks[i]; - if (!week.isFree()) { - eventBus.fire(JumpToPosEvent(TimetablePos(weekIndex: i, weekday: defaultWeekday))); - return; - } - } - // WHAT? NO CLASS IN THE WHOLE TERM? - // Alright, let's congratulate them! - if (!ctx.mounted) return; - await ctx.showTip(title: i18n.congratulations, desc: i18n.freeTip.termTip, ok: i18n.ok); - } -} diff --git a/lib/timetable/widgets/style.dart b/lib/timetable/widgets/style.dart deleted file mode 100644 index 97a8f8846..000000000 --- a/lib/timetable/widgets/style.dart +++ /dev/null @@ -1,167 +0,0 @@ -import 'package:copy_with_extension/copy_with_extension.dart'; -import 'package:flutter/material.dart'; -import 'package:sit/settings/settings.dart'; -import 'package:sit/timetable/entity/background.dart'; -import 'package:sit/timetable/entity/platte.dart'; -import 'package:sit/timetable/platte.dart'; - -import '../init.dart'; - -part "style.g.dart"; - -@CopyWith(skipFields: true) -class CourseCellStyle { - final bool showTeachers; - final bool grayOutTakenLessons; - final bool harmonizeWithThemeColor; - final double alpha; - - const CourseCellStyle({ - this.showTeachers = true, - this.grayOutTakenLessons = false, - this.harmonizeWithThemeColor = true, - this.alpha = 1.0, - }); -} - -@CopyWith(skipFields: true) -class TimetableStyleData { - final TimetablePalette platte; - final CourseCellStyle cellStyle; - final BackgroundImage background; - - const TimetableStyleData({ - this.platte = BuiltinTimetablePalettes.classic, - this.cellStyle = const CourseCellStyle(), - this.background = const BackgroundImage.disabled(), - }); - - @override - // ignore: hash_and_equals - bool operator ==(Object other) { - return other is TimetableStyleData && - runtimeType == other.runtimeType && - platte == other.platte && - background == other.background && - cellStyle == other.cellStyle; - } -} - -class TimetableStyle extends InheritedWidget { - final TimetableStyleData data; - - const TimetableStyle({ - super.key, - required this.data, - required super.child, - }); - - static TimetableStyleData of(BuildContext context) { - final TimetableStyle? result = context.dependOnInheritedWidgetOfExactType(); - assert(result != null, 'No TimetableStyle found in context'); - return result!.data; - } - - static TimetableStyleData? maybeOf(BuildContext context) { - final TimetableStyle? result = context.dependOnInheritedWidgetOfExactType(); - return result?.data; - } - - @override - bool updateShouldNotify(TimetableStyle oldWidget) { - return data != oldWidget.data; - } -} - -class TimetableStyleProv extends StatefulWidget { - final Widget? child; - final TimetablePalette? palette; - final CourseCellStyle? cellStyle; - final BackgroundImage? background; - - final Widget Function(BuildContext context, TimetableStyleData style)? builder; - - const TimetableStyleProv({ - super.key, - this.child, - this.builder, - this.palette, - this.cellStyle, - this.background, - }) : assert(builder != null || child != null, "TimetableStyleProv should have at least one child."); - - @override - TimetableStyleProvState createState() => TimetableStyleProvState(); -} - -class TimetableStyleProvState extends State { - final $palette = TimetableInit.storage.palette.$selected; - final $cellStyle = Settings.timetable.cell.listenStyle(); - final $background = Settings.timetable.listenBackgroundImage(); - var palette = TimetableInit.storage.palette.selectedRow ?? BuiltinTimetablePalettes.classic; - var cellStyle = Settings.timetable.cell.cellStyle; - var background = Settings.timetable.backgroundImage ?? const BackgroundImage.disabled(); - - @override - void initState() { - super.initState(); - $palette.addListener(refreshPalette); - $cellStyle.addListener(refreshCellStyle); - $background.addListener(refreshBackground); - } - - @override - void dispose() { - $palette.removeListener(refreshPalette); - $cellStyle.removeListener(refreshCellStyle); - $background.removeListener(refreshBackground); - super.dispose(); - } - - void refreshPalette() { - setState(() { - palette = TimetableInit.storage.palette.selectedRow ?? BuiltinTimetablePalettes.classic; - }); - } - - void refreshCellStyle() { - setState(() { - cellStyle = Settings.timetable.cell.cellStyle; - }); - } - - void refreshBackground() { - setState(() { - background = Settings.timetable.backgroundImage ?? const BackgroundImage.disabled(); - }); - } - - @override - Widget build(BuildContext context) { - final data = TimetableStyleData( - platte: palette, - cellStyle: cellStyle, - background: background, - ).copyWith( - platte: widget.palette, - cellStyle: widget.cellStyle, - background: widget.background, - ); - return TimetableStyle( - data: data, - child: buildChild(data), - ); - } - - Widget buildChild(TimetableStyleData data) { - final child = widget.child; - if (child != null) { - return child; - } - final builder = widget.builder; - if (builder != null) { - return Builder(builder: (ctx) => builder(ctx, data)); - } - return const SizedBox(); - } -} diff --git a/lib/timetable/widgets/style.g.dart b/lib/timetable/widgets/style.g.dart deleted file mode 100644 index 6842c1ce5..000000000 --- a/lib/timetable/widgets/style.g.dart +++ /dev/null @@ -1,126 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'style.dart'; - -// ************************************************************************** -// CopyWithGenerator -// ************************************************************************** - -abstract class _$CourseCellStyleCWProxy { - /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. - /// - /// Usage - /// ```dart - /// CourseCellStyle(...).copyWith(id: 12, name: "My name") - /// ```` - CourseCellStyle call({ - bool? showTeachers, - bool? grayOutTakenLessons, - bool? harmonizeWithThemeColor, - double? alpha, - }); -} - -/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfCourseCellStyle.copyWith(...)`. -class _$CourseCellStyleCWProxyImpl implements _$CourseCellStyleCWProxy { - const _$CourseCellStyleCWProxyImpl(this._value); - - final CourseCellStyle _value; - - @override - - /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. - /// - /// Usage - /// ```dart - /// CourseCellStyle(...).copyWith(id: 12, name: "My name") - /// ```` - CourseCellStyle call({ - Object? showTeachers = const $CopyWithPlaceholder(), - Object? grayOutTakenLessons = const $CopyWithPlaceholder(), - Object? harmonizeWithThemeColor = const $CopyWithPlaceholder(), - Object? alpha = const $CopyWithPlaceholder(), - }) { - return CourseCellStyle( - showTeachers: showTeachers == const $CopyWithPlaceholder() || showTeachers == null - ? _value.showTeachers - // ignore: cast_nullable_to_non_nullable - : showTeachers as bool, - grayOutTakenLessons: grayOutTakenLessons == const $CopyWithPlaceholder() || grayOutTakenLessons == null - ? _value.grayOutTakenLessons - // ignore: cast_nullable_to_non_nullable - : grayOutTakenLessons as bool, - harmonizeWithThemeColor: - harmonizeWithThemeColor == const $CopyWithPlaceholder() || harmonizeWithThemeColor == null - ? _value.harmonizeWithThemeColor - // ignore: cast_nullable_to_non_nullable - : harmonizeWithThemeColor as bool, - alpha: alpha == const $CopyWithPlaceholder() || alpha == null - ? _value.alpha - // ignore: cast_nullable_to_non_nullable - : alpha as double, - ); - } -} - -extension $CourseCellStyleCopyWith on CourseCellStyle { - /// Returns a callable class that can be used as follows: `instanceOfCourseCellStyle.copyWith(...)`. - // ignore: library_private_types_in_public_api - _$CourseCellStyleCWProxy get copyWith => _$CourseCellStyleCWProxyImpl(this); -} - -abstract class _$TimetableStyleDataCWProxy { - /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. - /// - /// Usage - /// ```dart - /// TimetableStyleData(...).copyWith(id: 12, name: "My name") - /// ```` - TimetableStyleData call({ - TimetablePalette? platte, - CourseCellStyle? cellStyle, - BackgroundImage? background, - }); -} - -/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfTimetableStyleData.copyWith(...)`. -class _$TimetableStyleDataCWProxyImpl implements _$TimetableStyleDataCWProxy { - const _$TimetableStyleDataCWProxyImpl(this._value); - - final TimetableStyleData _value; - - @override - - /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. - /// - /// Usage - /// ```dart - /// TimetableStyleData(...).copyWith(id: 12, name: "My name") - /// ```` - TimetableStyleData call({ - Object? platte = const $CopyWithPlaceholder(), - Object? cellStyle = const $CopyWithPlaceholder(), - Object? background = const $CopyWithPlaceholder(), - }) { - return TimetableStyleData( - platte: platte == const $CopyWithPlaceholder() || platte == null - ? _value.platte - // ignore: cast_nullable_to_non_nullable - : platte as TimetablePalette, - cellStyle: cellStyle == const $CopyWithPlaceholder() || cellStyle == null - ? _value.cellStyle - // ignore: cast_nullable_to_non_nullable - : cellStyle as CourseCellStyle, - background: background == const $CopyWithPlaceholder() || background == null - ? _value.background - // ignore: cast_nullable_to_non_nullable - : background as BackgroundImage, - ); - } -} - -extension $TimetableStyleDataCopyWith on TimetableStyleData { - /// Returns a callable class that can be used as follows: `instanceOfTimetableStyleData.copyWith(...)`. - // ignore: library_private_types_in_public_api - _$TimetableStyleDataCWProxy get copyWith => _$TimetableStyleDataCWProxyImpl(this); -} diff --git a/lib/timetable/widgets/timetable/board.dart b/lib/timetable/widgets/timetable/board.dart deleted file mode 100644 index afc634b84..000000000 --- a/lib/timetable/widgets/timetable/board.dart +++ /dev/null @@ -1,106 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:sit/files.dart'; -import 'package:sit/timetable/entity/background.dart'; -import 'package:sit/timetable/widgets/style.dart'; - -import '../../entity/display.dart'; -import '../../entity/pos.dart'; -import '../../entity/timetable.dart'; -import 'daily.dart'; -import 'weekly.dart'; - -class TimetableBoard extends StatelessWidget { - final SitTimetableEntity timetable; - - final ValueNotifier $displayMode; - - final ValueNotifier $currentPos; - - const TimetableBoard({ - super.key, - required this.timetable, - required this.$displayMode, - required this.$currentPos, - }); - - @override - Widget build(BuildContext context) { - final style = TimetableStyle.of(context); - final background = style.background; - if (background.enabled) { - return [ - Positioned.fill( - child: TimetableBackground(background: background), - ), - buildBoard(), - ].stack(); - } - return buildBoard(); - } - - Widget buildBoard() { - return $displayMode >> - (ctx, mode) => AnimatedSwitcher( - duration: Durations.short4, - child: mode == DisplayMode.daily - ? DailyTimetable( - $currentPos: $currentPos, - timetable: timetable, - ) - : WeeklyTimetable( - $currentPos: $currentPos, - timetable: timetable, - ), - ); - } -} - -class TimetableBackground extends StatefulWidget { - final BackgroundImage background; - - const TimetableBackground({ - super.key, - required this.background, - }); - - @override - State createState() => _TimetableBackgroundState(); -} - -class _TimetableBackgroundState extends State with SingleTickerProviderStateMixin { - late final AnimationController $opacity; - - @override - void initState() { - super.initState(); - $opacity = AnimationController(vsync: this, value: widget.background.opacity); - } - - @override - void dispose() { - $opacity.dispose(); - super.dispose(); - } - - @override - void didUpdateWidget(covariant TimetableBackground oldWidget) { - super.didUpdateWidget(oldWidget); - $opacity.animateTo( - widget.background.opacity, - duration: const Duration(milliseconds: 100), - ); - } - - @override - Widget build(BuildContext context) { - final bk = widget.background; - return Image.file( - key: ValueKey(bk.path), - Files.timetable.backgroundFile, - opacity: $opacity, - filterQuality: bk.antialias ? FilterQuality.low : FilterQuality.none, - repeat: bk.repeat ? ImageRepeat.repeat : ImageRepeat.noRepeat, - ); - } -} diff --git a/lib/timetable/widgets/timetable/daily.dart b/lib/timetable/widgets/timetable/daily.dart deleted file mode 100644 index d747e2715..000000000 --- a/lib/timetable/widgets/timetable/daily.dart +++ /dev/null @@ -1,423 +0,0 @@ -import 'dart:async'; - -import 'package:auto_size_text/auto_size_text.dart'; -import 'package:dynamic_color/dynamic_color.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:sit/design/adaptive/foundation.dart'; -import 'package:sit/design/widgets/card.dart'; -import 'package:sit/l10n/time.dart'; -import 'package:sit/school/utils.dart'; -import 'package:sit/school/entity/timetable.dart'; -import 'package:sit/school/widgets/course.dart'; -import 'package:sit/timetable/page/details.dart'; -import 'package:sit/timetable/platte.dart'; -import 'package:sit/timetable/widgets/free.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:sit/utils/color.dart'; - -import '../../entity/timetable.dart'; -import '../../events.dart'; -import '../../utils.dart'; -import '../style.dart'; -import '../../entity/pos.dart'; -import 'header.dart'; - -class DailyTimetable extends StatefulWidget { - final SitTimetableEntity timetable; - - final ValueNotifier $currentPos; - - @override - State createState() => DailyTimetableState(); - - const DailyTimetable({ - super.key, - required this.timetable, - required this.$currentPos, - }); -} - -class DailyTimetableState extends State { - SitTimetableEntity get timetable => widget.timetable; - - TimetablePos get currentPos => widget.$currentPos.value; - - set currentPos(TimetablePos newValue) => widget.$currentPos.value = newValue; - - /// 翻页控制 - late PageController _pageController; - - int pos2PageOffset(TimetablePos pos) => pos.weekIndex * 7 + pos.weekday.index; - - TimetablePos page2Pos(int page) => TimetablePos(weekIndex: page ~/ 7, weekday: Weekday.fromIndex(page % 7)); - - late StreamSubscription $jumpToPos; - - @override - void initState() { - super.initState(); - _pageController = PageController(initialPage: pos2PageOffset(widget.$currentPos.value)) - ..addListener(() { - setState(() { - final page = (_pageController.page ?? 0).round(); - final newPos = page2Pos(page); - if (currentPos != newPos) { - currentPos = newPos; - } - }); - }); - $jumpToPos = eventBus.on().listen((event) { - jumpTo(event.where); - }); - } - - @override - void dispose() { - $jumpToPos.cancel(); - _pageController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return [ - widget.$currentPos >> - (ctx, cur) => TimetableHeader( - selectedWeekday: cur.weekday, - weekIndex: cur.weekIndex, - startDate: timetable.type.startDate, - onDayTap: (dayIndex) { - eventBus.fire(JumpToPosEvent(TimetablePos(weekIndex: cur.weekIndex, weekday: dayIndex))); - }, - ), - PageView.builder( - controller: _pageController, - scrollDirection: Axis.horizontal, - itemCount: 20 * 7, - itemBuilder: (_, int index) { - int weekIndex = index ~/ 7; - int dayIndex = index % 7; - final todayPos = timetable.type.locate(DateTime.now()); - return TimetableOneDayPage( - timetable: timetable, - todayPos: todayPos, - weekIndex: weekIndex, - weekday: Weekday.fromIndex(dayIndex), - ); - }, - ).expanded(), - ].column(); - } - - void jumpTo(TimetablePos pos) { - if (_pageController.hasClients) { - final targetOffset = pos2PageOffset(pos); - final currentPos = _pageController.page ?? targetOffset; - final distance = (targetOffset - currentPos).abs(); - _pageController.animateToPage( - targetOffset, - duration: calcuSwitchAnimationDuration(distance), - curve: Curves.fastEaseInToSlowEaseOut, - ); - } - } -} - -class TimetableOneDayPage extends StatefulWidget { - final SitTimetableEntity timetable; - final TimetablePos todayPos; - final int weekIndex; - final Weekday weekday; - - const TimetableOneDayPage({ - super.key, - required this.timetable, - required this.todayPos, - required this.weekIndex, - required this.weekday, - }); - - @override - State createState() => _TimetableOneDayPageState(); -} - -class _TimetableOneDayPageState extends State with AutomaticKeepAliveClientMixin { - Widget? _cached; - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - _cached = null; - } - - @override - Widget build(BuildContext context) { - super.build(context); - final cache = _cached; - if (cache != null) { - return cache; - } else { - final res = buildPage(context); - _cached = res; - return res; - } - } - - Widget buildPage(BuildContext ctx) { - int weekIndex = widget.weekIndex; - final week = widget.timetable.weeks[weekIndex]; - final day = week[widget.weekday]; - if (!day.hasAnyLesson()) { - return FreeDayTip( - timetable: widget.timetable, - weekIndex: weekIndex, - weekday: widget.weekday, - ).scrolled().center(); - } else { - final slotCount = day.timeslot2LessonSlot.length; - final builder = _RowBuilder(); - for (int timeslot = 0; timeslot < slotCount; timeslot++) { - builder.add( - timeslot, - buildLessonsInTimeslot( - ctx, - day.timeslot2LessonSlot[timeslot].lessons, - timeslot, - ), - ); - } - // Since the course list is small, no need to use [ListView.builder]. - return ListView( - children: builder.build(), - ); - } - } - - Widget? buildLessonsInTimeslot( - BuildContext ctx, - List lessonsInSlot, - int timeslot, - ) { - if (lessonsInSlot.isEmpty) { - return null; - } else if (lessonsInSlot.length == 1) { - final lesson = lessonsInSlot[0]; - return buildSingleLesson( - ctx, - timetable: widget.timetable, - lesson: lesson, - timeslot: timeslot, - ).padH(6); - } else { - return LessonOverlapGroup(lessonsInSlot, timeslot, widget.timetable).padH(6); - } - } - - Widget buildSingleLesson( - BuildContext context, { - required SitTimetableEntity timetable, - required SitTimetableLessonPart lesson, - required int timeslot, - }) { - final course = lesson.course; - final style = TimetableStyle.of(context); - - var color = style.platte.resolveColor(course).byTheme(context.theme); - if (style.cellStyle.harmonizeWithThemeColor) { - color = color.harmonizeWith(context.colorScheme.primary); - } - if (style.cellStyle.grayOutTakenLessons && lesson.endTime.isBefore(DateTime.now())) { - color = color.monochrome(); - } - final alpha = style.cellStyle.alpha; - if (alpha < 1.0) { - color = color.withOpacity(color.opacity * alpha); - } - final classTime = course.buildingTimetable[timeslot]; - return [ - ClassTimeCard( - color: color, - classTime: classTime, - ), - LessonCard( - lesson: lesson, - timetable: timetable, - course: course, - color: color, - ).expanded() - ].row(); - } - - @override - bool get wantKeepAlive => true; -} - -class LessonCard extends StatelessWidget { - final SitTimetableLessonPart lesson; - final SitCourse course; - final SitTimetableEntity timetable; - final Color color; - - const LessonCard({ - super.key, - required this.lesson, - required this.course, - required this.timetable, - required this.color, - }); - - static const iconSize = 45.0; - - @override - Widget build(BuildContext context) { - return FilledCard( - margin: const EdgeInsets.all(8), - color: color, - clip: Clip.hardEdge, - child: ListTile( - leading: CourseIcon(courseName: course.courseName), - onTap: () async { - if (!context.mounted) return; - await context.show$Sheet$( - (ctx) => TimetableCourseDetailsSheet(courseCode: course.courseCode, timetable: timetable), - ); - }, - title: AutoSizeText( - course.courseName, - maxLines: 1, - ), - subtitle: [ - if (course.place.isNotEmpty) - Text(beautifyPlace(course.place), softWrap: true, overflow: TextOverflow.ellipsis), - course.teachers.join(', ').text(), - ].column(caa: CrossAxisAlignment.start), - ), - ); - } -} - -class ClassTimeCard extends StatelessWidget { - final Color color; - final ClassTime classTime; - - const ClassTimeCard({ - super.key, - required this.color, - required this.classTime, - }); - - @override - Widget build(BuildContext context) { - return ElevatedText( - color: color, - margin: 10, - child: [ - classTime.begin.l10n(context).text(style: const TextStyle(fontWeight: FontWeight.bold)), - SizedBox(height: 5.h), - classTime.end.l10n(context).text(), - ].column(), - ); - } -} - -class LessonOverlapGroup extends StatelessWidget { - final List lessonsInSlot; - final int timeslot; - final SitTimetableEntity timetable; - - const LessonOverlapGroup( - this.lessonsInSlot, - this.timeslot, - this.timetable, { - super.key, - }); - - @override - Widget build(BuildContext context) { - if (lessonsInSlot.isEmpty) return const SizedBox(); - final List all = []; - ClassTime? classTime; - for (int lessonIndex = 0; lessonIndex < lessonsInSlot.length; lessonIndex++) { - final lesson = lessonsInSlot[lessonIndex]; - final course = lesson.course; - final color = TimetableStyle.of(context).platte.resolveColor(course).byTheme(context.theme); - classTime = course.buildingTimetable[timeslot]; - final row = LessonCard( - lesson: lesson, - course: course, - timetable: timetable, - color: color, - ); - all.add(row); - } - // [classTime] must be nonnull. - // TODO: Color for class overlap. - return OutlinedCard( - child: [ - ClassTimeCard( - color: TimetableStyle.of(context).platte.colors[0].byTheme(context.theme), - classTime: classTime!, - ), - all.column().expanded(), - ].row().padAll(3), - ); - } -} - -enum _RowBuilderState { - row, - divider, - none; -} - -class _RowBuilder { - final List _rows = []; - _RowBuilderState lastAdded = _RowBuilderState.none; - - void add(int index, Widget? row) { - // WOW! MEAL TIME! - // For each four classes, there's a meal. - if (index != 0 && index % 4 == 0 && lastAdded != _RowBuilderState.divider) { - _rows.add(const Divider(thickness: 2)); - lastAdded = _RowBuilderState.divider; - } - if (row != null) { - _rows.add(row); - lastAdded = _RowBuilderState.row; - } - } - - List build() { - // Remove surplus dividers. - for (int i = _rows.length - 1; 0 <= i; i--) { - if (_rows[i] is Divider) { - _rows.removeLast(); - } else { - break; - } - } - return _rows; - } -} - -class ElevatedText extends StatelessWidget { - final Widget child; - final Color color; - final double margin; - - const ElevatedText({ - super.key, - required this.color, - required this.margin, - required this.child, - }); - - @override - Widget build(BuildContext context) { - return FilledCard( - color: color, - child: child.padAll(margin), - ); - } -} diff --git a/lib/timetable/widgets/timetable/header.dart b/lib/timetable/widgets/timetable/header.dart deleted file mode 100644 index 3e4040882..000000000 --- a/lib/timetable/widgets/timetable/header.dart +++ /dev/null @@ -1,134 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:sit/l10n/time.dart'; -import 'package:sit/school/utils.dart'; -import 'package:rettulf/rettulf.dart'; - -BorderSide getTimetableBorderSide(BuildContext ctx) => - BorderSide(color: ctx.colorScheme.primary.withOpacity(0.4), width: 0.8); - -class TimetableHeader extends StatelessWidget { - final int weekIndex; - final Weekday? selectedWeekday; - final Function(Weekday dayIndex)? onDayTap; - final DateTime startDate; - - const TimetableHeader({ - super.key, - required this.weekIndex, - required this.startDate, - this.selectedWeekday, - this.onDayTap, - }); - - @override - Widget build(BuildContext context) { - final onDayTap = this.onDayTap; - - return Weekday.monday - .genSequenceStartWithThis() - .map((weekday) { - return Expanded( - child: InkWell( - onTap: onDayTap != null - ? () { - onDayTap.call(weekday); - } - : null, - child: HeaderCell( - weekIndex: weekIndex, - weekday: weekday, - startDate: startDate, - backgroundColor: weekday == selectedWeekday ? context.colorScheme.secondaryContainer : null, - ), - ), - ); - }) - .toList() - .row(maa: MainAxisAlignment.spaceEvenly); - } -} - -class HeaderCell extends StatelessWidget { - final int weekIndex; - final Weekday weekday; - final DateTime startDate; - final Color? backgroundColor; - - const HeaderCell({ - super.key, - required this.weekIndex, - required this.weekday, - required this.startDate, - this.backgroundColor, - }); - - @override - Widget build(BuildContext context) { - final side = getTimetableBorderSide(context); - return AnimatedContainer( - duration: Durations.short2, - curve: Curves.easeOut, - decoration: BoxDecoration( - color: backgroundColor, - border: Border(bottom: side), - ), - child: HeaderCellTextBox( - weekIndex: weekIndex, - weekday: weekday, - startDate: startDate, - ), - ); - } -} - -class HeaderCellTextBox extends StatelessWidget { - final int weekIndex; - final Weekday weekday; - final DateTime startDate; - - const HeaderCellTextBox({ - super.key, - required this.weekIndex, - required this.weekday, - required this.startDate, - }); - - @override - Widget build(BuildContext context) { - final date = reflectWeekDayIndexToDate( - weekIndex: weekIndex, - weekday: weekday, - startDate: startDate, - ); - return [ - weekday.l10nShort().text( - textAlign: TextAlign.center, - style: context.textTheme.titleSmall, - ), - '${date.month}/${date.day}'.text( - textAlign: TextAlign.center, - style: context.textTheme.labelSmall, - ), - ].column().padOnly(t: 5, b: 5); - } -} - -class EmptyHeaderCellTextBox extends StatelessWidget { - const EmptyHeaderCellTextBox({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return [ - "".text( - textAlign: TextAlign.center, - style: context.textTheme.titleSmall, - ), - "".text( - textAlign: TextAlign.center, - style: context.textTheme.labelSmall, - ), - ].column().padOnly(t: 5, b: 5); - } -} diff --git a/lib/timetable/widgets/timetable/weekly.dart b/lib/timetable/widgets/timetable/weekly.dart deleted file mode 100644 index 625a4567d..000000000 --- a/lib/timetable/widgets/timetable/weekly.dart +++ /dev/null @@ -1,539 +0,0 @@ -import 'dart:async'; - -import 'package:auto_size_text/auto_size_text.dart'; -import 'package:dynamic_color/dynamic_color.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:sit/design/adaptive/foundation.dart'; -import 'package:sit/design/dash_decoration.dart'; -import 'package:sit/design/widgets/card.dart'; -import 'package:sit/l10n/time.dart'; -import 'package:sit/school/utils.dart'; -import 'package:sit/timetable/platte.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:sit/utils/color.dart'; -import 'package:universal_platform/universal_platform.dart'; - -import '../../events.dart'; -import '../../entity/timetable.dart'; -import '../../page/details.dart'; -import '../../utils.dart'; -import '../free.dart'; -import 'header.dart'; -import '../style.dart'; -import '../../entity/pos.dart'; -import '../../i18n.dart'; - -class WeeklyTimetable extends StatefulWidget { - final SitTimetableEntity timetable; - - final ValueNotifier $currentPos; - - @override - State createState() => WeeklyTimetableState(); - - const WeeklyTimetable({ - super.key, - required this.timetable, - required this.$currentPos, - }); -} - -class WeeklyTimetableState extends State { - late PageController pageController; - final $cellSize = ValueNotifier(Size.zero); - - SitTimetableEntity get timetable => widget.timetable; - - TimetablePos get currentPos => widget.$currentPos.value; - - set currentPos(TimetablePos newValue) => widget.$currentPos.value = newValue; - late StreamSubscription $jumpToPos; - - @override - void initState() { - super.initState(); - pageController = PageController(initialPage: currentPos.weekIndex) - ..addListener(() { - setState(() { - final newWeek = (pageController.page ?? 0).round(); - if (newWeek != currentPos.weekIndex) { - currentPos = currentPos.copyWith(weekIndex: newWeek); - } - }); - }); - $jumpToPos = eventBus.on().listen((event) { - jumpTo(event.where); - }); - } - - @override - void dispose() { - $jumpToPos.cancel(); - pageController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return PageView.builder( - controller: pageController, - scrollDirection: Axis.horizontal, - itemCount: 20, - itemBuilder: (ctx, weekIndex) { - return TimetableOneWeekCached( - timetable: timetable, - weekIndex: weekIndex, - ); - }, - ); - } - - /// 跳到某一周 - void jumpTo(TimetablePos pos) { - if (pageController.hasClients) { - final targetOffset = pos.weekIndex; - final currentPos = pageController.page ?? targetOffset; - final distance = (targetOffset - currentPos).abs(); - pageController.animateToPage( - targetOffset, - duration: calcuSwitchAnimationDuration(distance), - curve: Curves.fastEaseInToSlowEaseOut, - ); - } - } -} - -class TimetableOneWeekCached extends StatefulWidget { - final SitTimetableEntity timetable; - final int weekIndex; - - const TimetableOneWeekCached({ - super.key, - required this.timetable, - required this.weekIndex, - }); - - @override - State createState() => _TimetableOneWeekCachedState(); -} - -class _TimetableOneWeekCachedState extends State with AutomaticKeepAliveClientMixin { - /// Cache the entire page to avoid expensive rebuilding. - Widget? _cached; - - @override - bool get wantKeepAlive => true; - - @override - void didChangeDependencies() { - _cached = null; - super.didChangeDependencies(); - } - - @override - void didUpdateWidget(covariant TimetableOneWeekCached oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.timetable != oldWidget.timetable) { - _cached = null; - } - } - - @override - Widget build(BuildContext context) { - super.build(context); - final cache = _cached; - if (cache != null) { - return cache; - } else { - final style = TimetableStyle.of(context); - final today = DateTime.now(); - Widget buildCell({ - required BuildContext context, - required SitTimetableLessonPart lesson, - required SitTimetableEntity timetable, - }) { - return InteractiveCourseCell( - lesson: lesson, - style: style, - timetable: timetable, - grayOut: style.cellStyle.grayOutTakenLessons ? lesson.type.endTime.isBefore(today) : false, - ); - } - - final res = LayoutBuilder( - builder: (context, box) { - return TimetableOneWeek( - fullSize: Size(box.maxWidth, box.maxHeight * 1.2), - timetable: widget.timetable, - weekIndex: widget.weekIndex, - showFreeTip: true, - cellBuilder: buildCell, - ).scrolled(); - }, - ); - _cached = res; - return res; - } - } -} - -class TimetableOneWeek extends StatelessWidget { - final SitTimetableEntity timetable; - final int weekIndex; - final Size fullSize; - final bool showFreeTip; - final Widget Function({ - required BuildContext context, - required SitTimetableLessonPart lesson, - required SitTimetableEntity timetable, - }) cellBuilder; - - const TimetableOneWeek({ - super.key, - required this.timetable, - required this.weekIndex, - required this.fullSize, - required this.cellBuilder, - this.showFreeTip = false, - }); - - @override - Widget build(BuildContext context) { - final todayPos = timetable.type.locate(DateTime.now()); - final cellSize = Size(fullSize.width / 7.62, fullSize.height / 11); - final timetableWeek = timetable.weeks[weekIndex]; - - final view = buildSingleWeekView( - timetableWeek, - context: context, - cellSize: cellSize, - fullSize: fullSize, - todayPos: todayPos, - ); - if (showFreeTip && timetableWeek.isFree()) { - // free week - return [ - view, - FreeWeekTip( - timetable: timetable, - weekIndex: weekIndex, - ).padOnly(t: fullSize.height * 0.2), - ].stack(); - } else { - return view; - } - } - - /// 布局左侧边栏, 显示节次 - Widget buildLeftColumn(BuildContext ctx, Size cellSize) { - final textStyle = ctx.textTheme.bodyMedium; - final side = getTimetableBorderSide(ctx); - final cells = []; - cells.add(SizedBox( - width: cellSize.width * 0.6, - child: const EmptyHeaderCellTextBox(), - )); - for (var i = 0; i < 11; i++) { - cells.add(Container( - decoration: BoxDecoration( - border: Border(right: side), - ), - child: SizedBox.fromSize( - size: Size(cellSize.width * 0.6, cellSize.height), - child: (i + 1).toString().text(style: textStyle).center(), - ), - )); - } - return cells.column(); - } - - Widget buildSingleWeekView( - SitTimetableWeek timetableWeek, { - required BuildContext context, - required Size cellSize, - required Size fullSize, - required TimetablePos todayPos, - }) { - return List.generate(8, (index) { - if (index == 0) { - return buildLeftColumn(context, cellSize); - } else { - return _buildCellsByDay( - context, - timetableWeek.days[index - 1], - cellSize, - todayPos: todayPos, - ); - } - }).row(); - } - - /// 构建某一天的那一列格子. - Widget _buildCellsByDay( - BuildContext context, - SitTimetableDay day, - Size cellSize, { - required TimetablePos todayPos, - }) { - final cells = []; - final weekday = Weekday.fromIndex(day.index); - cells.add(Container( - width: cellSize.width, - decoration: BoxDecoration( - color: todayPos.weekIndex == weekIndex && todayPos.weekday == weekday - ? context.colorScheme.secondaryContainer - : null, - border: Border(bottom: getTimetableBorderSide(context)), - ), - child: HeaderCellTextBox( - weekIndex: weekIndex, - weekday: weekday, - startDate: timetable.type.startDate, - ), - )); - for (int timeslot = 0; timeslot < day.timeslot2LessonSlot.length; timeslot++) { - final lessonSlot = day.timeslot2LessonSlot[timeslot]; - if (lessonSlot.lessons.isEmpty) { - cells.add(DashLined( - top: timeslot != 0, - bottom: timeslot != day.timeslot2LessonSlot.length - 1, - child: SizedBox(width: cellSize.width, height: cellSize.height), - )); - } else { - /// TODO: Multi-layer lessonSlot - final firstLayerLesson = lessonSlot.lessons[0]; - - /// TODO: Range checking - final course = firstLayerLesson.course; - cells.add(SizedBox( - width: cellSize.width, - height: cellSize.height * firstLayerLesson.type.timeslotDuration, - child: cellBuilder( - context: context, - lesson: firstLayerLesson, - timetable: timetable, - ), - )); - - /// Skip to the end - timeslot = firstLayerLesson.type.endIndex; - } - } - - return cells.column(); - } -} - -class InteractiveCourseCell extends StatefulWidget { - final SitTimetableLessonPart lesson; - final SitTimetableEntity timetable; - final bool grayOut; - final TimetableStyleData style; - - const InteractiveCourseCell({ - super.key, - required this.lesson, - required this.timetable, - this.grayOut = false, - required this.style, - }); - - @override - State createState() => _InteractiveCourseCellState(); -} - -class _InteractiveCourseCellState extends State { - final $tooltip = GlobalKey(debugLabel: "tooltip"); - - @override - Widget build(BuildContext context) { - return StyledCourseCell( - course: widget.lesson.course, - grayOut: widget.grayOut, - style: widget.style, - innerBuilder: (ctx, child) => Tooltip( - key: $tooltip, - preferBelow: false, - triggerMode: UniversalPlatform.isDesktop ? TooltipTriggerMode.tap : TooltipTriggerMode.manual, - message: buildTooltipMessage(), - textAlign: TextAlign.center, - child: InkWell( - onTap: UniversalPlatform.isDesktop - ? null - : () async { - $tooltip.currentState?.ensureTooltipVisible(); - }, - onLongPress: () async { - await context.show$Sheet$( - (ctx) => TimetableCourseDetailsSheet( - courseCode: widget.lesson.course.courseCode, - timetable: widget.timetable, - ), - ); - }, - child: child, - ), - ), - ); - } - - String buildTooltipMessage() { - final lessons = widget.lesson.course.calcBeginEndTimePointForEachLesson(); - final lessonTimeTip = lessons.map((time) => "${time.begin.l10n(context)}–${time.end.l10n(context)}").join("\n"); - final course = widget.lesson.course; - var tooltip = "${i18n.course.courseCode} ${course.courseCode}"; - if (course.classCode.isNotEmpty) { - tooltip += "\n${i18n.course.classCode} ${course.classCode}"; - } - tooltip += "\n$lessonTimeTip"; - return tooltip; - } -} - -class CourseCell extends StatelessWidget { - final String courseName; - final String place; - final List? teachers; - final Widget Function(BuildContext context, Widget child)? innerBuilder; - final Color color; - - const CourseCell({ - super.key, - required this.courseName, - required this.color, - required this.place, - this.teachers, - this.innerBuilder, - }); - - @override - Widget build(BuildContext context) { - final innerBuilder = this.innerBuilder; - final info = TimetableSlotInfo( - courseName: courseName, - maxLines: context.isPortrait ? 8 : 5, - place: place, - teachers: teachers, - ).center(); - return FilledCard( - clip: Clip.hardEdge, - color: color, - margin: EdgeInsets.all(0.5.w), - child: innerBuilder != null ? innerBuilder(context, info) : info, - ); - } -} - -class StyledCourseCell extends StatelessWidget { - final SitCourse course; - final bool grayOut; - final Widget Function(BuildContext context, Widget child)? innerBuilder; - final TimetableStyleData style; - - const StyledCourseCell({ - super.key, - required this.course, - required this.grayOut, - required this.style, - this.innerBuilder, - }); - - @override - Widget build(BuildContext context) { - var color = style.platte.resolveColor(course).byTheme(context.theme); - if (style.cellStyle.harmonizeWithThemeColor) { - color = color.harmonizeWith(context.colorScheme.primary); - } - if (grayOut) { - color = color.monochrome(); - } - final alpha = style.cellStyle.alpha; - if (alpha < 1.0) { - color = color.withOpacity(color.opacity * alpha); - } - return CourseCell( - courseName: course.courseName, - color: color, - place: course.place, - teachers: style.cellStyle.showTeachers ? course.teachers : null, - innerBuilder: innerBuilder, - ); - } -} - -class DashLined extends StatelessWidget { - final Widget? child; - final bool top; - final bool bottom; - final bool left; - final bool right; - - const DashLined({ - super.key, - this.child, - this.top = false, - this.bottom = false, - this.left = false, - this.right = false, - }); - - @override - Widget build(BuildContext context) { - return Container( - decoration: DashDecoration( - color: context.colorScheme.onBackground.withOpacity(0.3), - strokeWidth: 0.5, - borders: { - if (right) LinePosition.right, - if (bottom) LinePosition.bottom, - if (left) LinePosition.left, - if (top) LinePosition.top, - }, - ), - child: child, - ); - } -} - -class TimetableSlotInfo extends StatelessWidget { - final String courseName; - final String place; - final List? teachers; - final int maxLines; - - const TimetableSlotInfo({ - super.key, - required this.maxLines, - required this.courseName, - required this.place, - this.teachers, - }); - - @override - Widget build(BuildContext context) { - final teachers = this.teachers; - return AutoSizeText.rich( - TextSpan(children: [ - TextSpan( - text: courseName, - style: context.textTheme.bodyMedium, - ), - if (place.isNotEmpty) - TextSpan( - text: "\n${beautifyPlace(place)}", - style: context.textTheme.bodySmall, - ), - if (teachers != null) - TextSpan( - text: "\n${teachers.join(',')}", - style: context.textTheme.bodySmall, - ), - ]), - minFontSize: 0, - stepGranularity: 0.1, - maxLines: maxLines, - textAlign: TextAlign.center, - ); - } -} diff --git a/lib/utils/async_event.dart b/lib/utils/async_event.dart deleted file mode 100644 index 5be466220..000000000 --- a/lib/utils/async_event.dart +++ /dev/null @@ -1,352 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/foundation.dart'; - -const String _thisLibrary = 'package:sit/utils/event_bus.dart'; - -class AsyncEventEmitter implements Listenable { - int _count = 0; - - // The _listeners is intentionally set to a fixed-length _GrowableList instead - // of const []. - // - // The const [] creates an instance of _ImmutableList which would be - // different from fixed-length _GrowableList used elsewhere in this class. - // keeping runtime type the same during the lifetime of this class lets the - // compiler to infer concrete type for this property, and thus improves - // performance. - static final List Function()?> _emptyListeners = List Function()?>.filled(0, null); - List Function()?> _listeners = _emptyListeners; - int _notificationCallStackDepth = 0; - int _reentrantlyRemovedListeners = 0; - bool _debugDisposed = false; - - /// If true, the event [ObjectCreated] for this instance was dispatched to - /// [MemoryAllocations]. - /// - /// As [ChangedNotifier] is used as mixin, it does not have constructor, - /// so we use [addListener] to dispatch the event. - bool _creationDispatched = false; - - /// Used by subclasses to assert that the [ChangeNotifier] has not yet been - /// disposed. - /// - /// {@tool snippet} - /// The [debugAssertNotDisposed] function should only be called inside of an - /// assert, as in this example. - /// - /// ```dart - /// class MyNotifier with ChangeNotifier { - /// void doUpdate() { - /// assert(ChangeNotifier.debugAssertNotDisposed(this)); - /// // ... - /// } - /// } - /// ``` - /// {@end-tool} - // This is static and not an instance method because too many people try to - // implement ChangeNotifier instead of extending it (and so it is too breaking - // to add a method, especially for debug). - static bool debugAssertNotDisposed(AsyncEventEmitter notifier) { - assert(() { - if (notifier._debugDisposed) { - throw FlutterError( - 'A ${notifier.runtimeType} was used after being disposed.\n' - 'Once you have called dispose() on a ${notifier.runtimeType}, it ' - 'can no longer be used.', - ); - } - return true; - }()); - return true; - } - - /// Whether any listeners are currently registered. - /// - /// Clients should not depend on this value for their behavior, because having - /// one listener's logic change when another listener happens to start or stop - /// listening will lead to extremely hard-to-track bugs. Subclasses might use - /// this information to determine whether to do any work when there are no - /// listeners, however; for example, resuming a [Stream] when a listener is - /// added and pausing it when a listener is removed. - /// - /// Typically this is used by overriding [addListener], checking if - /// [hasListeners] is false before calling `super.addListener()`, and if so, - /// starting whatever work is needed to determine when to call - /// [notifyListeners]; and similarly, by overriding [removeListener], checking - /// if [hasListeners] is false after calling `super.removeListener()`, and if - /// so, stopping that same work. - /// - /// This method returns false if [dispose] has been called. - @protected - bool get hasListeners => _count > 0; - - /// Register a closure to be called when the object changes. - /// - /// If the given closure is already registered, an additional instance is - /// added, and must be removed the same number of times it is added before it - /// will stop being called. - /// - /// This method must not be called after [dispose] has been called. - /// - /// {@template flutter.foundation.ChangeNotifier.addListener} - /// If a listener is added twice, and is removed once during an iteration - /// (e.g. in response to a notification), it will still be called again. If, - /// on the other hand, it is removed as many times as it was registered, then - /// it will no longer be called. This odd behavior is the result of the - /// [ChangeNotifier] not being able to determine which listener is being - /// removed, since they are identical, therefore it will conservatively still - /// call all the listeners when it knows that any are still registered. - /// - /// This surprising behavior can be unexpectedly observed when registering a - /// listener on two separate objects which are both forwarding all - /// registrations to a common upstream object. - /// {@endtemplate} - /// - /// See also: - /// - /// * [removeListener], which removes a previously registered closure from - /// the list of closures that are notified when the object changes. - @override - EventSubscription addListener(FutureOr Function() listener) { - assert(AsyncEventEmitter.debugAssertNotDisposed(this)); - if (kFlutterMemoryAllocationsEnabled && !_creationDispatched) { - MemoryAllocations.instance.dispatchObjectCreated( - library: _thisLibrary, - className: '$AsyncEventEmitter', - object: this, - ); - _creationDispatched = true; - } - if (_count == _listeners.length) { - if (_count == 0) { - _listeners = List Function()?>.filled(1, null); - } else { - final List Function()?> newListeners = - List Function()?>.filled(_listeners.length * 2, null); - for (int i = 0; i < _count; i++) { - newListeners[i] = _listeners[i]; - } - _listeners = newListeners; - } - } - _listeners[_count++] = listener; - return EventSubscription(emitter: this, listener: listener); - } - - void _removeAt(int index) { - // The list holding the listeners is not growable for performances reasons. - // We still want to shrink this list if a lot of listeners have been added - // and then removed outside a notifyListeners iteration. - // We do this only when the real number of listeners is half the length - // of our list. - _count -= 1; - if (_count * 2 <= _listeners.length) { - final List Function()?> newListeners = List Function()?>.filled(_count, null); - - // Listeners before the index are at the same place. - for (int i = 0; i < index; i++) { - newListeners[i] = _listeners[i]; - } - - // Listeners after the index move towards the start of the list. - for (int i = index; i < _count; i++) { - newListeners[i] = _listeners[i + 1]; - } - - _listeners = newListeners; - } else { - // When there are more listeners than half the length of the list, we only - // shift our listeners, so that we avoid to reallocate memory for the - // whole list. - for (int i = index; i < _count; i++) { - _listeners[i] = _listeners[i + 1]; - } - _listeners[_count] = null; - } - } - - /// Remove a previously registered closure from the list of closures that are - /// notified when the object changes. - /// - /// If the given listener is not registered, the call is ignored. - /// - /// This method returns immediately if [dispose] has been called. - /// - /// {@macro flutter.foundation.ChangeNotifier.addListener} - /// - /// See also: - /// - /// * [addListener], which registers a closure to be called when the object - /// changes. - @override - void removeListener(FutureOr Function() listener) { - // This method is allowed to be called on disposed instances for usability - // reasons. Due to how our frame scheduling logic between render objects and - // overlays, it is common that the owner of this instance would be disposed a - // frame earlier than the listeners. Allowing calls to this method after it - // is disposed makes it easier for listeners to properly clean up. - for (int i = 0; i < _count; i++) { - final FutureOr Function()? listenerAtIndex = _listeners[i]; - if (listenerAtIndex == listener) { - if (_notificationCallStackDepth > 0) { - // We don't resize the list during notifyListeners iterations - // but we set to null, the listeners we want to remove. We will - // effectively resize the list at the end of all notifyListeners - // iterations. - _listeners[i] = null; - _reentrantlyRemovedListeners++; - } else { - // When we are outside the notifyListeners iterations we can - // effectively shrink the list. - _removeAt(i); - } - break; - } - } - } - - /// Discards any resources used by the object. After this is called, the - /// object is not in a usable state and should be discarded (calls to - /// [addListener] will throw after the object is disposed). - /// - /// This method should only be called by the object's owner. - /// - /// This method does not notify listeners, and clears the listener list once - /// it is called. Consumers of this class must decide on whether to notify - /// listeners or not immediately before disposal. - @mustCallSuper - void dispose() { - assert(AsyncEventEmitter.debugAssertNotDisposed(this)); - assert( - _notificationCallStackDepth == 0, - 'The "dispose()" method on $this was called during the call to ' - '"notifyListeners()". This is likely to cause errors since it modifies ' - 'the list of listeners while the list is being used.', - ); - assert(() { - _debugDisposed = true; - return true; - }()); - if (kFlutterMemoryAllocationsEnabled && _creationDispatched) { - MemoryAllocations.instance.dispatchObjectDisposed(object: this); - } - _listeners = _emptyListeners; - _count = 0; - } - - /// Call all the registered listeners. - /// - /// Call this method whenever the object changes, to notify any clients the - /// object may have changed. Listeners that are added during this iteration - /// will not be visited. Listeners that are removed during this iteration will - /// not be visited after they are removed. - /// - /// Exceptions thrown by listeners will be caught and reported using - /// [FlutterError.reportError]. - /// - /// This method must not be called after [dispose] has been called. - /// - /// Surprising behavior can result when reentrantly removing a listener (e.g. - /// in response to a notification) that has been registered multiple times. - /// See the discussion at [removeListener]. - @pragma('vm:notify-debugger-on-exception') - Future notifyListeners() async { - assert(AsyncEventEmitter.debugAssertNotDisposed(this)); - if (_count == 0) { - return; - } - - // To make sure that listeners removed during this iteration are not called, - // we set them to null, but we don't shrink the list right away. - // By doing this, we can continue to iterate on our list until it reaches - // the last listener added before the call to this method. - - // To allow potential listeners to recursively call notifyListener, we track - // the number of times this method is called in _notificationCallStackDepth. - // Once every recursive iteration is finished (i.e. when _notificationCallStackDepth == 0), - // we can safely shrink our list so that it will only contain not null - // listeners. - - _notificationCallStackDepth++; - - final int end = _count; - for (int i = 0; i < end; i++) { - try { - final listener = _listeners[i]; - if (listener != null) { - final result = listener.call(); - if (result is Future) { - await result; - } - } - } catch (exception, stack) { - FlutterError.reportError(FlutterErrorDetails( - exception: exception, - stack: stack, - library: 'foundation library', - context: ErrorDescription('while dispatching notifications for $runtimeType'), - informationCollector: () => [ - DiagnosticsProperty( - 'The $runtimeType sending notification was', - this, - style: DiagnosticsTreeStyle.errorProperty, - ), - ], - )); - } - } - - _notificationCallStackDepth--; - - if (_notificationCallStackDepth == 0 && _reentrantlyRemovedListeners > 0) { - // We really remove the listeners when all notifications are done. - final int newLength = _count - _reentrantlyRemovedListeners; - if (newLength * 2 <= _listeners.length) { - // As in _removeAt, we only shrink the list when the real number of - // listeners is half the length of our list. - final List Function()?> newListeners = List Function()?>.filled(newLength, null); - - int newIndex = 0; - for (int i = 0; i < _count; i++) { - final FutureOr Function()? listener = _listeners[i]; - if (listener != null) { - newListeners[newIndex++] = listener; - } - } - - _listeners = newListeners; - } else { - // Otherwise we put all the null references at the end. - for (int i = 0; i < newLength; i += 1) { - if (_listeners[i] == null) { - // We swap this item with the next not null item. - int swapIndex = i + 1; - while (_listeners[swapIndex] == null) { - swapIndex += 1; - } - _listeners[i] = _listeners[swapIndex]; - _listeners[swapIndex] = null; - } - } - } - - _reentrantlyRemovedListeners = 0; - _count = newLength; - } - } -} - -class EventSubscription { - final FutureOr Function() listener; - final AsyncEventEmitter emitter; - - const EventSubscription({ - required this.emitter, - required this.listener, - }); - - void cancel() { - emitter.removeListener(listener); - } -} diff --git a/lib/utils/byte_io.dart b/lib/utils/byte_io.dart deleted file mode 100644 index e754d6954..000000000 --- a/lib/utils/byte_io.dart +++ /dev/null @@ -1,201 +0,0 @@ -import 'dart:convert'; -import 'dart:typed_data'; - -class ByteWriter { - late ByteData _view; - Uint8List _buffer; - int _offset = 0; - - ByteWriter(int initialCapacity) : _buffer = Uint8List(initialCapacity) { - _view = _buffer.buffer.asByteData(); - } - - int get size => _offset + 1; - - int get capacity => _buffer.length; - - void _checkCapacity({required int requireBytes}) { - if (_offset + 1 + requireBytes > _buffer.length) { - _grow(size + requireBytes); - } - } - - void _grow(int required) { - // We will create a list in the range of 2-4 times larger than - // required. - int newSize = required * 2; - if (newSize < _buffer.length) { - newSize = _buffer.length; - } else { - newSize = _pow2roundup(newSize); - } - var newBuffer = Uint8List(newSize); - newBuffer.setRange(0, _buffer.length, _buffer); - _buffer = newBuffer; - _view = newBuffer.buffer.asByteData(); - } - - void int8(int value) { - _checkCapacity(requireBytes: 1); - _view.setInt8(_offset, value); - _offset += 1; - } - - void int16(int value, [Endian endian = Endian.big]) { - _checkCapacity(requireBytes: 2); - _view.setInt16(_offset, value, endian); - _offset += 2; - } - - void int32(int value, [Endian endian = Endian.big]) { - _checkCapacity(requireBytes: 4); - _view.setInt32(_offset, value, endian); - _offset += 4; - } - - void int64(int value, [Endian endian = Endian.big]) { - _checkCapacity(requireBytes: 8); - _view.setInt64(_offset, value, endian); - _offset += 8; - } - - void uint8(int value) { - _checkCapacity(requireBytes: 1); - _view.setUint8(_offset, value); - _offset += 1; - } - - void uint16(int value, [Endian endian = Endian.big]) { - _checkCapacity(requireBytes: 2); - _view.setUint16(_offset, value, endian); - _offset += 2; - } - - void uint32(int value, [Endian endian = Endian.big]) { - _checkCapacity(requireBytes: 4); - _view.setUint32(_offset, value, endian); - _offset += 4; - } - - void uint64(int value, [Endian endian = Endian.big]) { - _checkCapacity(requireBytes: 8); - _view.setUint64(_offset, value, endian); - _offset += 8; - } - - void float32(double value, [Endian endian = Endian.big]) { - _checkCapacity(requireBytes: 4); - _view.setFloat32(_offset, value, endian); - _offset += 4; - } - - void float64(double value, [Endian endian = Endian.big]) { - _checkCapacity(requireBytes: 8); - _view.setFloat64(_offset, value, endian); - _offset += 8; - } - - void strUtf8(String str, [Endian endian = Endian.big]) { - List nameBytes = utf8.encode(str); - uint64(nameBytes.length, endian); - _checkCapacity(requireBytes: nameBytes.length); - for (int i = 0; i < nameBytes.length; i++) { - uint8(nameBytes[i]); - } - } - - Uint8List build() { - return _buffer.sublist(0, _offset); - } -} - -int _pow2roundup(int x) { - assert(x > 0); - --x; - x |= x >> 1; - x |= x >> 2; - x |= x >> 4; - x |= x >> 8; - x |= x >> 16; - return x + 1; -} - -class ByteReader { - final Uint8List data; - late final ByteData _view; - int _offset = 0; - - ByteReader(this.data) { - _view = data.buffer.asByteData(); - } - - int int8() { - final value = _view.getInt8(_offset); - _offset += 1; - return value; - } - - int int16([Endian endian = Endian.big]) { - final value = _view.getInt16(_offset, endian); - _offset += 2; - return value; - } - - int int32([Endian endian = Endian.big]) { - final value = _view.getInt32(_offset, endian); - _offset += 4; - return value; - } - - int int64([Endian endian = Endian.big]) { - final value = _view.getInt64(_offset, endian); - _offset += 8; - return value; - } - - int uint8() { - final value = _view.getUint8(_offset); - _offset += 1; - return value; - } - - int uint16([Endian endian = Endian.big]) { - final value = _view.getUint16(_offset, endian); - _offset += 2; - return value; - } - - int uint32([Endian endian = Endian.big]) { - final value = _view.getUint32(_offset, endian); - _offset += 4; - return value; - } - - int uint64([Endian endian = Endian.big]) { - final value = _view.getUint64(_offset, endian); - _offset += 8; - return value; - } - - double float32([Endian endian = Endian.big]) { - final value = _view.getFloat32(_offset, endian); - _offset += 4; - return value; - } - - double float64([Endian endian = Endian.big]) { - final value = _view.getFloat64(_offset, endian); - _offset += 8; - return value; - } - - String strUtf8([Endian endian = Endian.big]) { - final length = uint64(endian); - List charCodes = []; - for (int i = 0; i < length; i++) { - final code = uint8(); - charCodes.add(code); - } - return utf8.decode(charCodes); - } -} diff --git a/lib/utils/collection.dart b/lib/utils/collection.dart deleted file mode 100644 index acf038c0f..000000000 --- a/lib/utils/collection.dart +++ /dev/null @@ -1,15 +0,0 @@ -extension DistinctEx on List { - List distinct({bool inplace = true}) { - final ids = {}; - var list = inplace ? this : List.from(this); - list.retainWhere((x) => ids.add(x)); - return list; - } - - List distinctBy(Id Function(E element) id, {bool inplace = true}) { - final ids = {}; - var list = inplace ? this : List.from(this); - list.retainWhere((x) => ids.add(id(x))); - return list; - } -} diff --git a/lib/utils/color.dart b/lib/utils/color.dart deleted file mode 100644 index 7d1acc287..000000000 --- a/lib/utils/color.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'dart:ui'; - -import 'package:flutter/foundation.dart' show defaultTargetPlatform; -import 'package:system_theme/system_theme.dart'; - -extension ColorX on Color { - Color monochrome({double progress = 1}) { - final gray = 0.21 * red + 0.71 * green + 0.07 * blue; - final iProgress = 1.0 - progress; - return Color.fromARGB( - alpha, - (red * iProgress + gray * progress).toInt(), - (green * iProgress + gray * progress).toInt(), - (blue * iProgress + gray * progress).toInt(), - ); - } -} - -extension SystemAccentColorX on SystemAccentColor { - Color? get maybeAccent => supportsSystemAccentColor ? accent : null; -} - -bool get supportsSystemAccentColor => defaultTargetPlatform.supportsAccentColor; diff --git a/lib/utils/cookies.dart b/lib/utils/cookies.dart deleted file mode 100644 index 7014e14d0..000000000 --- a/lib/utils/cookies.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:cookie_jar/cookie_jar.dart'; -import 'package:webview_flutter/webview_flutter.dart'; - -extension WebViewCookieJarX on CookieJar { - Future> loadAsWebViewCookie(Uri uri) async { - final cookies = await loadForRequest(uri); - return cookies.map((cookie) { - return cookie.toWebviewCooke(uri); - }).toList(); - } -} - -extension WebViewCookieX on Cookie { - WebViewCookie toWebviewCooke(Uri uri) { - return WebViewCookie( - name: name, - value: value, - domain: domain ?? uri.host, - ); - } -} diff --git a/lib/utils/date.dart b/lib/utils/date.dart deleted file mode 100644 index 92fc70657..000000000 --- a/lib/utils/date.dart +++ /dev/null @@ -1,35 +0,0 @@ -bool isLeapYear({ - required int year, -}) { - if (year % 400 == 0) return true; - if (year % 4 == 0 && year % 100 != 0) return true; - return false; -} - -int daysInMonth({ - required int year, - required int month, -}) { - assert(1 <= month && month <= 12, "month must be in [1,12]"); - return switch (month) { - 1 => 31, - 2 => isLeapYear(year: year) ? 29 : 28, - 3 => 31, - 4 => 30, - 5 => 31, - 6 => 30, - 7 => 31, - 8 => 31, - 9 => 30, - 10 => 31, - 11 => 30, - 12 => 31, - _ => 30, - }; -} - -List daysInEachMonth({ - required int year, -}) { - return [31, isLeapYear(year: year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; -} diff --git a/lib/utils/dio.dart b/lib/utils/dio.dart deleted file mode 100644 index b887e9300..000000000 --- a/lib/utils/dio.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:dio/dio.dart'; - -Options disableRedirectFormEncodedOptions({ - Map? headers, - ResponseType? responseType, -}) { - return Options( - headers: headers, - contentType: Headers.formUrlEncodedContentType, - followRedirects: false, - responseType: responseType, - validateStatus: (status) { - return status! < 400; - }, - ); -} - -Future processRedirect( - Dio dio, - Response response, { - Map? headers, -}) async { - //Prevent the redirect being processed by HttpClient, with the 302 response caught manually. - if (response.statusCode == 302 && response.headers['location'] != null && response.headers['location']!.isNotEmpty) { - String location = response.headers['location']![0]; - if (location.isEmpty) return response; - if (!Uri.parse(location).isAbsolute) { - location = '${response.requestOptions.uri.origin}/$location'; - } - return processRedirect( - dio, - await dio.get( - location, - options: disableRedirectFormEncodedOptions( - responseType: response.requestOptions.responseType, - headers: headers, - ), - ), - headers: headers, - ); - } else { - return response; - } -} diff --git a/lib/utils/error.dart b/lib/utils/error.dart deleted file mode 100644 index 9d3e2da2c..000000000 --- a/lib/utils/error.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:dio/dio.dart'; -import 'package:flutter/cupertino.dart'; - -void debugPrintError(Object error, [StackTrace? stackTrace]) { - if (error is DioException) { - debugPrint(error.toString()); - debugPrintStack(stackTrace: error.stackTrace, maxFrames: 10); - } else { - debugPrint(error.toString()); - debugPrintStack(stackTrace: stackTrace); - } -} diff --git a/lib/utils/format.dart b/lib/utils/format.dart deleted file mode 100644 index 99375f5d2..000000000 --- a/lib/utils/format.dart +++ /dev/null @@ -1,29 +0,0 @@ -String formatWithoutTrailingZeros(double amount) { - if (amount == 0) return "0"; - final number = amount.toStringAsFixed(2); - if (number.contains('.')) { - int index = number.length - 1; - while (index >= 0 && number[index] == '0') { - index--; - if (index >= 0 && number[index] == '.') { - index--; - break; - } - } - return number.substring(0, index + 1); - } - return number; -} - -final _trailingIntRe = RegExp(r"(.*\s+)(\d+)$"); - -String getDuplicateFileName(String origin) { - final matched = _trailingIntRe.firstMatch(origin); - if (matched == null) return "$origin 2"; - final prefix = matched.group(1); - final number = matched.group(2); - if (prefix == null || number == null) return "$origin 2"; - final integer = int.tryParse(number, radix: 10); - if (integer == null) return "$origin 2"; - return "$prefix${integer + 1}"; -} diff --git a/lib/utils/guard_launch.dart b/lib/utils/guard_launch.dart deleted file mode 100644 index e5b2cc779..000000000 --- a/lib/utils/guard_launch.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:go_router/go_router.dart'; -import 'package:universal_platform/universal_platform.dart'; -import 'package:url_launcher/url_launcher.dart'; - -Future guardLaunchUrl(BuildContext ctx, Uri url) async { - if (url.scheme == "http" || url.scheme == "https") { - // guards the http(s) - if (!UniversalPlatform.isDesktop) { - ctx.push(Uri( - path: "/browser", - queryParameters: {"url": url}, - ).toString()); - return true; - } - try { - return await launchUrl(url, mode: LaunchMode.externalApplication); - } catch (err) { - return false; - } - } - // not http(s) - try { - return await launchUrl(url); - } catch (err) { - return false; - } -} - -Future guardLaunchUrlString(BuildContext ctx, String url) async { - final uri = Uri.tryParse(url); - if (uri == null) return false; - return await guardLaunchUrl(ctx, uri); -} diff --git a/lib/utils/iconfont.dart b/lib/utils/iconfont.dart deleted file mode 100644 index a43f16edb..000000000 --- a/lib/utils/iconfont.dart +++ /dev/null @@ -1,773 +0,0 @@ -import 'package:flutter/widgets.dart'; - -class IconFont { - static const String _family = 'ywb_iconfont'; - static const String _defaultIconKey = 'icon-Customermanagement-fill'; - - static final Map _iconMapping = { - 'icon-biaodanzujian-biaoge': const IconData(0xeb96, fontFamily: _family), - 'icon-rmb': const IconData(0xe705, fontFamily: _family), - 'icon-company-fill': const IconData(0xe835, fontFamily: _family), - 'icon-biaodanzujian-xialakuang': const IconData(0xeb97, fontFamily: _family), - 'icon-similar-product': const IconData(0xe707, fontFamily: _family), - 'icon-discount-fill': const IconData(0xe836, fontFamily: _family), - 'icon-tubiao-bingtu': const IconData(0xeb98, fontFamily: _family), - 'icon-Exportservices': const IconData(0xe702, fontFamily: _family), - 'icon-insurance-fill': const IconData(0xe837, fontFamily: _family), - 'icon-biaodanzujian-anniu': const IconData(0xeb99, fontFamily: _family), - 'icon-sendinquiry': const IconData(0xe70d, fontFamily: _family), - 'icon-inquiry-template-fill': const IconData(0xe838, fontFamily: _family), - 'icon-gongyezujian-yibiaopan': const IconData(0xeb9a, fontFamily: _family), - 'icon-all-fill': const IconData(0xe718, fontFamily: _family), - 'icon-leftbutton-fill': const IconData(0xe839, fontFamily: _family), - 'icon-tubiao-qiapian': const IconData(0xeb9b, fontFamily: _family), - 'icon-favorites-fill': const IconData(0xe721, fontFamily: _family), - 'icon-integral-fill1': const IconData(0xe83a, fontFamily: _family), - 'icon-gongyezujian-zhishideng': const IconData(0xeb9c, fontFamily: _family), - 'icon-integral-fill': const IconData(0xe726, fontFamily: _family), - 'icon-help1': const IconData(0xe83b, fontFamily: _family), - 'icon-tubiao-zhexiantu': const IconData(0xeb9d, fontFamily: _family), - 'icon-namecard-fill': const IconData(0xe72a, fontFamily: _family), - 'icon-listing-content-fill': const IconData(0xe83c, fontFamily: _family), - 'icon-xingzhuang-juxing': const IconData(0xeb9e, fontFamily: _family), - 'icon-pic-fill': const IconData(0xe72e, fontFamily: _family), - 'icon-logistic-logo-fill': const IconData(0xe83d, fontFamily: _family), - 'icon-xingzhuang-jianxing': const IconData(0xeb9f, fontFamily: _family), - 'icon-play-fill': const IconData(0xe72f, fontFamily: _family), - 'icon-Moneymanagement-fill': const IconData(0xe83e, fontFamily: _family), - 'icon-gongyezujian-kaiguan': const IconData(0xeba0, fontFamily: _family), - 'icon-prompt-fill': const IconData(0xe730, fontFamily: _family), - 'icon-manage-order-fill': const IconData(0xe83f, fontFamily: _family), - 'icon-tubiao-zhuzhuangtu': const IconData(0xeba1, fontFamily: _family), - 'icon-stop-fill': const IconData(0xe738, fontFamily: _family), - 'icon-multi-language-fill': const IconData(0xe840, fontFamily: _family), - 'icon-xingzhuang-tupian': const IconData(0xeba2, fontFamily: _family), - 'icon-column': const IconData(0xe741, fontFamily: _family), - 'icon-logistics-icon-fill': const IconData(0xe841, fontFamily: _family), - 'icon-xingzhuang-wenzi': const IconData(0xeba3, fontFamily: _family), - 'icon-add-account': const IconData(0xe742, fontFamily: _family), - 'icon-Newuserzone-fill': const IconData(0xe842, fontFamily: _family), - 'icon-xingzhuang-tuoyuanxing': const IconData(0xeba4, fontFamily: _family), - 'icon-column1': const IconData(0xe743, fontFamily: _family), - 'icon-nightmode-fill': const IconData(0xe843, fontFamily: _family), - 'icon-xingzhuang-sanjiaoxing': const IconData(0xeba5, fontFamily: _family), - 'icon-add': const IconData(0xe744, fontFamily: _family), - 'icon-office-supplies-fill': const IconData(0xe844, fontFamily: _family), - 'icon-xingzhuang-xingxing': const IconData(0xeba6, fontFamily: _family), - 'icon-agriculture': const IconData(0xe745, fontFamily: _family), - 'icon-notice-fill': const IconData(0xe845, fontFamily: _family), - 'icon-guize': const IconData(0xebb7, fontFamily: _family), - 'icon-years': const IconData(0xe746, fontFamily: _family), - 'icon-mute': const IconData(0xe846, fontFamily: _family), - 'icon-shebeiguanli': const IconData(0xebb8, fontFamily: _family), - 'icon-add-cart': const IconData(0xe747, fontFamily: _family), - 'icon-order-fill': const IconData(0xe847, fontFamily: _family), - 'icon-gongnengdingyi1': const IconData(0xebb9, fontFamily: _family), - 'icon-arrow-right': const IconData(0xe748, fontFamily: _family), - 'icon-password1': const IconData(0xe848, fontFamily: _family), - 'icon-jishufuwu1': const IconData(0xebce, fontFamily: _family), - 'icon-arrow-left': const IconData(0xe749, fontFamily: _family), - 'icon-map1': const IconData(0xe849, fontFamily: _family), - 'icon-yunyingzhongxin': const IconData(0xebd0, fontFamily: _family), - 'icon-apparel': const IconData(0xe74a, fontFamily: _family), - 'icon-paylater-fill': const IconData(0xe84a, fontFamily: _family), - 'icon-yunyingguanli': const IconData(0xebd1, fontFamily: _family), - 'icon-all1': const IconData(0xe74b, fontFamily: _family), - 'icon-phone-fill': const IconData(0xe84b, fontFamily: _family), - 'icon-zuzhixiaxia': const IconData(0xebd8, fontFamily: _family), - 'icon-arrow-up': const IconData(0xe74c, fontFamily: _family), - 'icon-online-tracking-fill': const IconData(0xe84c, fontFamily: _family), - 'icon-zuzhizhankai': const IconData(0xebd9, fontFamily: _family), - 'icon-ascending': const IconData(0xe74d, fontFamily: _family), - 'icon-play-fill1': const IconData(0xe84d, fontFamily: _family), - 'icon-zuzhiqunzu': const IconData(0xebda, fontFamily: _family), - 'icon-ashbin': const IconData(0xe74e, fontFamily: _family), - 'icon-pdf-fill': const IconData(0xe84e, fontFamily: _family), - 'icon-dakai': const IconData(0xebdf, fontFamily: _family), - 'icon-atm': const IconData(0xe74f, fontFamily: _family), - 'icon-phone1': const IconData(0xe84f, fontFamily: _family), - 'icon-yingwen': const IconData(0xebe0, fontFamily: _family), - 'icon-bad': const IconData(0xe750, fontFamily: _family), - 'icon-pin-fill': const IconData(0xe850, fontFamily: _family), - 'icon-zhongwen': const IconData(0xebe2, fontFamily: _family), - 'icon-attachent': const IconData(0xe751, fontFamily: _family), - 'icon-product-fill': const IconData(0xe851, fontFamily: _family), - 'icon-miwen': const IconData(0xebe3, fontFamily: _family), - 'icon-browse': const IconData(0xe752, fontFamily: _family), - 'icon-rankinglist-fill': const IconData(0xe852, fontFamily: _family), - 'icon-xianhao': const IconData(0xebe4, fontFamily: _family), - 'icon-beauty': const IconData(0xe753, fontFamily: _family), - 'icon-reduce-fill': const IconData(0xe853, fontFamily: _family), - 'icon-kongxinduigou': const IconData(0xebe5, fontFamily: _family), - 'icon-atm-away': const IconData(0xe754, fontFamily: _family), - 'icon-reeor-fill': const IconData(0xe854, fontFamily: _family), - 'icon-huixingzhen': const IconData(0xebe6, fontFamily: _family), - 'icon-assessed-badge': const IconData(0xe755, fontFamily: _family), - 'icon-pic-fill1': const IconData(0xe855, fontFamily: _family), - 'icon-duigou': const IconData(0xebe7, fontFamily: _family), - 'icon-auto1': const IconData(0xe756, fontFamily: _family), - 'icon-rankinglist': const IconData(0xe856, fontFamily: _family), - 'icon-xiayibu': const IconData(0xebef, fontFamily: _family), - 'icon-bags': const IconData(0xe757, fontFamily: _family), - 'icon-product1': const IconData(0xe857, fontFamily: _family), - 'icon-shangyibu': const IconData(0xebf0, fontFamily: _family), - 'icon-calendar': const IconData(0xe758, fontFamily: _family), - 'icon-prompt-fill1': const IconData(0xe858, fontFamily: _family), - 'icon-kongjianxuanzhong': const IconData(0xebf1, fontFamily: _family), - 'icon-cart-full': const IconData(0xe759, fontFamily: _family), - 'icon-resonserate-fill': const IconData(0xe859, fontFamily: _family), - 'icon-kongjianweixuan': const IconData(0xebf2, fontFamily: _family), - 'icon-calculator': const IconData(0xe75a, fontFamily: _family), - 'icon-remind-fill': const IconData(0xe85a, fontFamily: _family), - 'icon-kongjianyixuan': const IconData(0xebf3, fontFamily: _family), - 'icon-cameraswitching': const IconData(0xe75b, fontFamily: _family), - 'icon-Rightbutton-fill': const IconData(0xe85b, fontFamily: _family), - 'icon--diangan': const IconData(0xebfb, fontFamily: _family), - 'icon-cecurity-protection': const IconData(0xe75c, fontFamily: _family), - 'icon-RFQ-logo-fill': const IconData(0xe85c, fontFamily: _family), - 'icon-rongxuejirongjiechi': const IconData(0xebfc, fontFamily: _family), - 'icon-category': const IconData(0xe75d, fontFamily: _family), - 'icon-RFQ-word-fill': const IconData(0xe85d, fontFamily: _family), - 'icon-lubiantingchechang': const IconData(0xebfd, fontFamily: _family), - 'icon-close': const IconData(0xe75e, fontFamily: _family), - 'icon-searchcart-fill': const IconData(0xe85e, fontFamily: _family), - 'icon--lumingpai': const IconData(0xebfe, fontFamily: _family), - 'icon-certified-supplier': const IconData(0xe75f, fontFamily: _family), - 'icon-salescenter-fill': const IconData(0xe85f, fontFamily: _family), - 'icon-jietouzuoyi': const IconData(0xebff, fontFamily: _family), - 'icon-cart-Empty': const IconData(0xe760, fontFamily: _family), - 'icon-save-fill': const IconData(0xe860, fontFamily: _family), - 'icon--zhongdaweixian': const IconData(0xec00, fontFamily: _family), - 'icon-code1': const IconData(0xe761, fontFamily: _family), - 'icon-security-fill': const IconData(0xe861, fontFamily: _family), - 'icon--jiaotongbiaozhipai': const IconData(0xec01, fontFamily: _family), - 'icon-color': const IconData(0xe762, fontFamily: _family), - 'icon-Similarproducts-fill': const IconData(0xe862, fontFamily: _family), - 'icon-gongcezhishipai': const IconData(0xec02, fontFamily: _family), - 'icon-conditions': const IconData(0xe763, fontFamily: _family), - 'icon-signboard-fill': const IconData(0xe863, fontFamily: _family), - 'icon-fangkuai': const IconData(0xec06, fontFamily: _family), - 'icon-confirm': const IconData(0xe764, fontFamily: _family), - 'icon-service-fill': const IconData(0xe864, fontFamily: _family), - 'icon-fangkuai-': const IconData(0xec07, fontFamily: _family), - 'icon-company': const IconData(0xe765, fontFamily: _family), - 'icon-shuffling-banner-fill': const IconData(0xe865, fontFamily: _family), - 'icon-shuaxin': const IconData(0xec08, fontFamily: _family), - 'icon-ali-clould': const IconData(0xe766, fontFamily: _family), - 'icon-supplier-features-fill': const IconData(0xe866, fontFamily: _family), - 'icon-baocun': const IconData(0xec09, fontFamily: _family), - 'icon-copy1': const IconData(0xe767, fontFamily: _family), - 'icon-store-fill': const IconData(0xe867, fontFamily: _family), - 'icon-fabu': const IconData(0xec0a, fontFamily: _family), - 'icon-credit-level': const IconData(0xe768, fontFamily: _family), - 'icon-smile-fill': const IconData(0xe868, fontFamily: _family), - 'icon-xiayibu1': const IconData(0xec0b, fontFamily: _family), - 'icon-coupons': const IconData(0xe769, fontFamily: _family), - 'icon-success-fill': const IconData(0xe869, fontFamily: _family), - 'icon-shangyibu1': const IconData(0xec0c, fontFamily: _family), - 'icon-connections': const IconData(0xe76a, fontFamily: _family), - 'icon-sound-filling-fill': const IconData(0xe86a, fontFamily: _family), - 'icon-xiangxiazhanhang': const IconData(0xec0d, fontFamily: _family), - 'icon-cry': const IconData(0xe76b, fontFamily: _family), - 'icon-sound-Mute1': const IconData(0xe86b, fontFamily: _family), - 'icon-xiangshangzhanhang': const IconData(0xec0e, fontFamily: _family), - 'icon-costoms-alearance': const IconData(0xe76c, fontFamily: _family), - 'icon-suspended-fill': const IconData(0xe86c, fontFamily: _family), - 'icon-tupianjiazaishibai': const IconData(0xec0f, fontFamily: _family), - 'icon-clock': const IconData(0xe76d, fontFamily: _family), - 'icon-tool-fill': const IconData(0xe86d, fontFamily: _family), - 'icon-fuwudiqiu': const IconData(0xec10, fontFamily: _family), - 'icon-CurrencyConverter': const IconData(0xe76e, fontFamily: _family), - 'icon-task-management-fill': const IconData(0xe86e, fontFamily: _family), - 'icon-suoxiao': const IconData(0xec13, fontFamily: _family), - 'icon-cut': const IconData(0xe76f, fontFamily: _family), - 'icon-unlock-fill': const IconData(0xe86f, fontFamily: _family), - 'icon-fangda': const IconData(0xec14, fontFamily: _family), - 'icon-data1': const IconData(0xe770, fontFamily: _family), - 'icon-trust-fill': const IconData(0xe870, fontFamily: _family), - 'icon-huanyuanhuabu': const IconData(0xec15, fontFamily: _family), - 'icon-Customermanagement': const IconData(0xe771, fontFamily: _family), - 'icon-vip-fill': const IconData(0xe871, fontFamily: _family), - 'icon-quanping': const IconData(0xec16, fontFamily: _family), - 'icon-descending': const IconData(0xe772, fontFamily: _family), - 'icon-set1': const IconData(0xe872, fontFamily: _family), - 'icon-biaodanzujian-biaoge1': const IconData(0xec17, fontFamily: _family), - 'icon-double-arro-right': const IconData(0xe773, fontFamily: _family), - 'icon-Top-fill': const IconData(0xe873, fontFamily: _family), - 'icon-APIshuchu': const IconData(0xec18, fontFamily: _family), - 'icon-customization': const IconData(0xe774, fontFamily: _family), - 'icon-viewlarger1': const IconData(0xe874, fontFamily: _family), - 'icon-APIjieru': const IconData(0xec19, fontFamily: _family), - 'icon-double-arrow-left': const IconData(0xe775, fontFamily: _family), - 'icon-voice-fill': const IconData(0xe875, fontFamily: _family), - 'icon-wenjianjia': const IconData(0xec1a, fontFamily: _family), - 'icon-discount': const IconData(0xe776, fontFamily: _family), - 'icon-warning-fill': const IconData(0xe876, fontFamily: _family), - 'icon-DOC': const IconData(0xec1b, fontFamily: _family), - 'icon-download': const IconData(0xe777, fontFamily: _family), - 'icon-warehouse-fill': const IconData(0xe877, fontFamily: _family), - 'icon-BMP': const IconData(0xec1c, fontFamily: _family), - 'icon-dollar1': const IconData(0xe778, fontFamily: _family), - 'icon-zip-fill': const IconData(0xe878, fontFamily: _family), - 'icon-GIF': const IconData(0xec1d, fontFamily: _family), - 'icon-default-template': const IconData(0xe779, fontFamily: _family), - 'icon-trade-assurance-fill': const IconData(0xe879, fontFamily: _family), - 'icon-JPG': const IconData(0xec1e, fontFamily: _family), - 'icon-editor1': const IconData(0xe77a, fontFamily: _family), - 'icon-vs-fill': const IconData(0xe87a, fontFamily: _family), - 'icon-PNG': const IconData(0xec1f, fontFamily: _family), - 'icon-eletrical': const IconData(0xe77b, fontFamily: _family), - 'icon-video1': const IconData(0xe87b, fontFamily: _family), - 'icon-weizhigeshi': const IconData(0xec20, fontFamily: _family), - 'icon-electronics': const IconData(0xe77c, fontFamily: _family), - 'icon-template-fill': const IconData(0xe87c, fontFamily: _family), - 'icon-gengduo': const IconData(0xec21, fontFamily: _family), - 'icon-etrical-equipm': const IconData(0xe77d, fontFamily: _family), - 'icon-wallet1': const IconData(0xe87d, fontFamily: _family), - 'icon-yunduanxiazai': const IconData(0xec22, fontFamily: _family), - 'icon-ellipsis': const IconData(0xe77e, fontFamily: _family), - 'icon-training1': const IconData(0xe87e, fontFamily: _family), - 'icon-yunduanshangchuan': const IconData(0xec23, fontFamily: _family), - 'icon-email': const IconData(0xe77f, fontFamily: _family), - 'icon-packing-labeling-fill': const IconData(0xe87f, fontFamily: _family), - 'icon-dian': const IconData(0xec24, fontFamily: _family), - 'icon-falling': const IconData(0xe780, fontFamily: _family), - 'icon-Exportservices-fill': const IconData(0xe880, fontFamily: _family), - 'icon-mian': const IconData(0xec25, fontFamily: _family), - 'icon-earth': const IconData(0xe781, fontFamily: _family), - 'icon-brand-fill': const IconData(0xe881, fontFamily: _family), - 'icon-xian': const IconData(0xec26, fontFamily: _family), - 'icon-filter': const IconData(0xe782, fontFamily: _family), - 'icon-collection': const IconData(0xe882, fontFamily: _family), - 'icon-shebeizhuangtai': const IconData(0xec27, fontFamily: _family), - 'icon-furniture': const IconData(0xe783, fontFamily: _family), - 'icon-consumption-fill': const IconData(0xe883, fontFamily: _family), - 'icon-fenzuguanli': const IconData(0xec28, fontFamily: _family), - 'icon-folder': const IconData(0xe784, fontFamily: _family), - 'icon-collection-fill': const IconData(0xe884, fontFamily: _family), - 'icon-kuaisubianpai': const IconData(0xec29, fontFamily: _family), - 'icon-feeds': const IconData(0xe785, fontFamily: _family), - 'icon-brand': const IconData(0xe885, fontFamily: _family), - 'icon-APPkaifa': const IconData(0xec2a, fontFamily: _family), - 'icon-history1': const IconData(0xe786, fontFamily: _family), - 'icon-rejected-order-fill': const IconData(0xe886, fontFamily: _family), - 'icon-wentijieda': const IconData(0xec2e, fontFamily: _family), - 'icon-hardware': const IconData(0xe787, fontFamily: _family), - 'icon-homepage-ads-fill': const IconData(0xe887, fontFamily: _family), - 'icon-kefu': const IconData(0xec2f, fontFamily: _family), - 'icon-help': const IconData(0xe788, fontFamily: _family), - 'icon-homepage-ads': const IconData(0xe888, fontFamily: _family), - 'icon-ruanjiankaifabao': const IconData(0xec30, fontFamily: _family), - 'icon-good': const IconData(0xe789, fontFamily: _family), - 'icon-scenes-fill': const IconData(0xe889, fontFamily: _family), - 'icon-sousuobianxiao': const IconData(0xec32, fontFamily: _family), - 'icon-Householdappliances': const IconData(0xe78a, fontFamily: _family), - 'icon-scenes': const IconData(0xe88a, fontFamily: _family), - 'icon-sousuofangda': const IconData(0xec33, fontFamily: _family), - 'icon-gift1': const IconData(0xe78b, fontFamily: _family), - 'icon-similar-product-fill': const IconData(0xe88b, fontFamily: _family), - 'icon-dingwei': const IconData(0xec34, fontFamily: _family), - 'icon-form': const IconData(0xe78c, fontFamily: _family), - 'icon-topraning-fill': const IconData(0xe88c, fontFamily: _family), - 'icon-wumoxing': const IconData(0xec35, fontFamily: _family), - 'icon-image-text': const IconData(0xe78d, fontFamily: _family), - 'icon-consumption': const IconData(0xe88d, fontFamily: _family), - 'icon-gaojing': const IconData(0xec36, fontFamily: _family), - 'icon-hot': const IconData(0xe78e, fontFamily: _family), - 'icon-topraning': const IconData(0xe88e, fontFamily: _family), - 'icon-renwujincheng': const IconData(0xec37, fontFamily: _family), - 'icon-inspection': const IconData(0xe78f, fontFamily: _family), - 'icon-gold-supplier': const IconData(0xe88f, fontFamily: _family), - 'icon-xiaoxitongzhi': const IconData(0xec38, fontFamily: _family), - 'icon-leftbutton': const IconData(0xe790, fontFamily: _family), - 'icon-messagecenter-fill': const IconData(0xe890, fontFamily: _family), - 'icon-youhui': const IconData(0xec39, fontFamily: _family), - 'icon-jewelry': const IconData(0xe791, fontFamily: _family), - 'icon-quick': const IconData(0xe891, fontFamily: _family), - 'icon-gaojing1': const IconData(0xec3a, fontFamily: _family), - 'icon-ipad': const IconData(0xe792, fontFamily: _family), - 'icon-writing': const IconData(0xe892, fontFamily: _family), - 'icon-zhihangfankui': const IconData(0xec3b, fontFamily: _family), - 'icon-leftarrow': const IconData(0xe793, fontFamily: _family), - 'icon-docjpge-fill': const IconData(0xe893, fontFamily: _family), - 'icon-gongdanqueren': const IconData(0xec3c, fontFamily: _family), - 'icon-integral1': const IconData(0xe794, fontFamily: _family), - 'icon-jpge-fill': const IconData(0xe894, fontFamily: _family), - 'icon-guangbo': const IconData(0xec3d, fontFamily: _family), - 'icon-kitchen': const IconData(0xe795, fontFamily: _family), - 'icon-gifjpge-fill': const IconData(0xe895, fontFamily: _family), - 'icon-gongdan': const IconData(0xec3e, fontFamily: _family), - 'icon-inquiry-template': const IconData(0xe796, fontFamily: _family), - 'icon-bmpjpge-fill': const IconData(0xe896, fontFamily: _family), - 'icon-xiaoxi': const IconData(0xec3f, fontFamily: _family), - 'icon-link': const IconData(0xe797, fontFamily: _family), - 'icon-tifjpge-fill': const IconData(0xe897, fontFamily: _family), - 'icon-ditu-qi': const IconData(0xec40, fontFamily: _family), - 'icon-libra': const IconData(0xe798, fontFamily: _family), - 'icon-pngjpge-fill': const IconData(0xe898, fontFamily: _family), - 'icon-ditu-dibiao': const IconData(0xec41, fontFamily: _family), - 'icon-loading': const IconData(0xe799, fontFamily: _family), - 'icon-Hometextile': const IconData(0xe899, fontFamily: _family), - 'icon-ditu-cha': const IconData(0xec42, fontFamily: _family), - 'icon-listing-content': const IconData(0xe79a, fontFamily: _family), - 'icon-home': const IconData(0xe89a, fontFamily: _family), - 'icon-ditu-qipao': const IconData(0xec43, fontFamily: _family), - 'icon-lights': const IconData(0xe79b, fontFamily: _family), - 'icon-sendinquiry-fill': const IconData(0xe89b, fontFamily: _family), - 'icon-ditu-tuding': const IconData(0xec44, fontFamily: _family), - 'icon-logistics-icon': const IconData(0xe79c, fontFamily: _family), - 'icon-comments-fill': const IconData(0xe89c, fontFamily: _family), - 'icon-ditu-huan': const IconData(0xec45, fontFamily: _family), - 'icon-messagecenter': const IconData(0xe79d, fontFamily: _family), - 'icon-account-fill': const IconData(0xe89d, fontFamily: _family), - 'icon-ditu-xing': const IconData(0xec46, fontFamily: _family), - 'icon-mobile-phone': const IconData(0xe79e, fontFamily: _family), - 'icon-feed-logo-fill': const IconData(0xe89e, fontFamily: _family), - 'icon-ditu-yuan': const IconData(0xec47, fontFamily: _family), - 'icon-manage-order': const IconData(0xe79f, fontFamily: _family), - 'icon-feed-logo': const IconData(0xe89f, fontFamily: _family), - 'icon-chehuisekuai': const IconData(0xec48, fontFamily: _family), - 'icon-move': const IconData(0xe7a0, fontFamily: _family), - 'icon-home-fill': const IconData(0xe8a0, fontFamily: _family), - 'icon-shanchusekuai': const IconData(0xec49, fontFamily: _family), - 'icon-Moneymanagement': const IconData(0xe7a1, fontFamily: _family), - 'icon-add-select': const IconData(0xe8a1, fontFamily: _family), - 'icon-fabusekuai': const IconData(0xec4a, fontFamily: _family), - 'icon-namecard': const IconData(0xe7a2, fontFamily: _family), - 'icon-sami-select': const IconData(0xe8a2, fontFamily: _family), - 'icon-xinhao': const IconData(0xec4b, fontFamily: _family), - 'icon-map': const IconData(0xe7a3, fontFamily: _family), - 'icon-camera': const IconData(0xe8a3, fontFamily: _family), - 'icon-lanya': const IconData(0xec4c, fontFamily: _family), - 'icon-Newuserzone': const IconData(0xe7a4, fontFamily: _family), - 'icon-arrow-down': const IconData(0xe8a4, fontFamily: _family), - 'icon-Wi-Fi': const IconData(0xec4d, fontFamily: _family), - 'icon-multi-language': const IconData(0xe7a5, fontFamily: _family), - 'icon-account': const IconData(0xe8a5, fontFamily: _family), - 'icon-chaxun': const IconData(0xec4e, fontFamily: _family), - 'icon-office': const IconData(0xe7a6, fontFamily: _family), - 'icon-comments': const IconData(0xe8a6, fontFamily: _family), - 'icon-dianbiao': const IconData(0xec4f, fontFamily: _family), - 'icon-notice': const IconData(0xe7a7, fontFamily: _family), - 'icon-cart-Empty1': const IconData(0xe8a7, fontFamily: _family), - 'icon-anquan': const IconData(0xec50, fontFamily: _family), - 'icon-ontimeshipment': const IconData(0xe7a8, fontFamily: _family), - 'icon-favorites': const IconData(0xe8a8, fontFamily: _family), - 'icon-daibanshixiang': const IconData(0xec51, fontFamily: _family), - 'icon-office-supplies': const IconData(0xe7a9, fontFamily: _family), - 'icon-order': const IconData(0xe8a9, fontFamily: _family), - 'icon-bingxiang': const IconData(0xec52, fontFamily: _family), - 'icon-password': const IconData(0xe7aa, fontFamily: _family), - 'icon-search': const IconData(0xe8aa, fontFamily: _family), - 'icon-fanshe': const IconData(0xec53, fontFamily: _family), - 'icon-Notvisible1': const IconData(0xe7ab, fontFamily: _family), - 'icon-trade-assurance': const IconData(0xe8ab, fontFamily: _family), - 'icon-fengche': const IconData(0xec54, fontFamily: _family), - 'icon-operation': const IconData(0xe7ac, fontFamily: _family), - 'icon-usercenter1': const IconData(0xe8ac, fontFamily: _family), - 'icon-guandao': const IconData(0xec55, fontFamily: _family), - 'icon-packaging': const IconData(0xe7ad, fontFamily: _family), - 'icon-tradingdata': const IconData(0xe8ad, fontFamily: _family), - 'icon-guize1': const IconData(0xec56, fontFamily: _family), - 'icon-online-tracking': const IconData(0xe7ae, fontFamily: _family), - 'icon-microphone': const IconData(0xe8ae, fontFamily: _family), - 'icon-guizeyinqing': const IconData(0xec57, fontFamily: _family), - 'icon-packing-labeling': const IconData(0xe7af, fontFamily: _family), - 'icon-txt': const IconData(0xe8af, fontFamily: _family), - 'icon-huowudui': const IconData(0xec58, fontFamily: _family), - 'icon-phone': const IconData(0xe7b0, fontFamily: _family), - 'icon-xlsx': const IconData(0xe8b0, fontFamily: _family), - 'icon-jianceqi': const IconData(0xec59, fontFamily: _family), - 'icon-pic1': const IconData(0xe7b1, fontFamily: _family), - 'icon-banzhengfuwu': const IconData(0xe8b1, fontFamily: _family), - 'icon-jinggai': const IconData(0xec5a, fontFamily: _family), - 'icon-pin': const IconData(0xe7b2, fontFamily: _family), - 'icon-cangku': const IconData(0xe8b2, fontFamily: _family), - 'icon-liujisuan': const IconData(0xec5b, fontFamily: _family), - 'icon-play1': const IconData(0xe7b3, fontFamily: _family), - 'icon-daibancaishui': const IconData(0xe8b3, fontFamily: _family), - 'icon-hanshu': const IconData(0xec5c, fontFamily: _family), - 'icon-logistic-logo': const IconData(0xe7b4, fontFamily: _family), - 'icon-jizhuangxiang': const IconData(0xe8b4, fontFamily: _family), - 'icon-lianjieliu': const IconData(0xec5d, fontFamily: _family), - 'icon-print': const IconData(0xe7b5, fontFamily: _family), - 'icon-jiaobiao': const IconData(0xe8b5, fontFamily: _family), - 'icon-ludeng': const IconData(0xec5e, fontFamily: _family), - 'icon-product': const IconData(0xe7b6, fontFamily: _family), - 'icon-kehupandian': const IconData(0xe8b6, fontFamily: _family), - 'icon-shexiangji': const IconData(0xec5f, fontFamily: _family), - 'icon-machinery': const IconData(0xe7b7, fontFamily: _family), - 'icon-dongtai': const IconData(0xe8b7, fontFamily: _family), - 'icon-rentijiance': const IconData(0xec60, fontFamily: _family), - 'icon-process': const IconData(0xe7b8, fontFamily: _family), - 'icon-daikuan': const IconData(0xe8b8, fontFamily: _family), - 'icon-moshubang': const IconData(0xec61, fontFamily: _family), - 'icon-prompt': const IconData(0xe7b9, fontFamily: _family), - 'icon-shengyijing': const IconData(0xe8b9, fontFamily: _family), - 'icon-shujuwajue': const IconData(0xec62, fontFamily: _family), - 'icon-QRcode1': const IconData(0xe7ba, fontFamily: _family), - 'icon-jiehui': const IconData(0xe8ba, fontFamily: _family), - 'icon-wangguan': const IconData(0xec63, fontFamily: _family), - 'icon-reeor': const IconData(0xe7bb, fontFamily: _family), - 'icon-fencengpeizhi': const IconData(0xe8bb, fontFamily: _family), - 'icon-shenjing': const IconData(0xec64, fontFamily: _family), - 'icon-reduce': const IconData(0xe7bc, fontFamily: _family), - 'icon-shenqingjilu': const IconData(0xe8bc, fontFamily: _family), - 'icon-chucun': const IconData(0xec65, fontFamily: _family), - 'icon-Non-staplefood': const IconData(0xe7bd, fontFamily: _family), - 'icon-shangchuanbeiandanzheng': const IconData(0xe8bd, fontFamily: _family), - 'icon-wuguan': const IconData(0xec66, fontFamily: _family), - 'icon-rejected-order': const IconData(0xe7be, fontFamily: _family), - 'icon-shangchuan': const IconData(0xe8be, fontFamily: _family), - 'icon-yunduanshuaxin': const IconData(0xec67, fontFamily: _family), - 'icon-resonserate': const IconData(0xe7bf, fontFamily: _family), - 'icon-kehuquanyi': const IconData(0xe8bf, fontFamily: _family), - 'icon-yunhang': const IconData(0xec68, fontFamily: _family), - 'icon-remind': const IconData(0xe7c0, fontFamily: _family), - 'icon-suoxiao1': const IconData(0xe8c0, fontFamily: _family), - 'icon-luyouqi': const IconData(0xec69, fontFamily: _family), - 'icon-responsetime': const IconData(0xe7c1, fontFamily: _family), - 'icon-quanyipeizhi': const IconData(0xe8c1, fontFamily: _family), - 'icon-bug': const IconData(0xec6a, fontFamily: _family), - 'icon-return': const IconData(0xe7c2, fontFamily: _family), - 'icon-shuangshen': const IconData(0xe8c2, fontFamily: _family), - 'icon-get': const IconData(0xec6b, fontFamily: _family), - 'icon-paylater': const IconData(0xe7c3, fontFamily: _family), - 'icon-tongguan': const IconData(0xe8c3, fontFamily: _family), - 'icon-PIR': const IconData(0xec6c, fontFamily: _family), - 'icon-rising1': const IconData(0xe7c4, fontFamily: _family), - 'icon-tuishui': const IconData(0xe8c4, fontFamily: _family), - 'icon-zhexiantu': const IconData(0xec6d, fontFamily: _family), - 'icon-Rightarrow': const IconData(0xe7c5, fontFamily: _family), - 'icon-tongguanshuju': const IconData(0xe8c5, fontFamily: _family), - 'icon-shuibiao': const IconData(0xec6e, fontFamily: _family), - 'icon-rmb1': const IconData(0xe7c6, fontFamily: _family), - 'icon-kuaidiwuliu': const IconData(0xe8c6, fontFamily: _family), - 'icon-js': const IconData(0xec6f, fontFamily: _family), - 'icon-RFQ-logo': const IconData(0xe7c7, fontFamily: _family), - 'icon-wuliuchanpin': const IconData(0xe8c7, fontFamily: _family), - 'icon-zihangche': const IconData(0xec70, fontFamily: _family), - 'icon-save': const IconData(0xe7c8, fontFamily: _family), - 'icon-waihuishuju': const IconData(0xe8c8, fontFamily: _family), - 'icon-liebiao': const IconData(0xec71, fontFamily: _family), - 'icon-scanning': const IconData(0xe7c9, fontFamily: _family), - 'icon-xinxibar-shouji': const IconData(0xe8c9, fontFamily: _family), - 'icon-qichedingwei': const IconData(0xec72, fontFamily: _family), - 'icon-security': const IconData(0xe7ca, fontFamily: _family), - 'icon-xinwaizongyewu': const IconData(0xe8ca, fontFamily: _family), - 'icon-dici': const IconData(0xec73, fontFamily: _family), - 'icon-salescenter': const IconData(0xe7cb, fontFamily: _family), - 'icon-wuliudingdan': const IconData(0xe8cb, fontFamily: _family), - 'icon-mysql': const IconData(0xec74, fontFamily: _family), - 'icon-seleted': const IconData(0xe7cc, fontFamily: _family), - 'icon-zhongjianren': const IconData(0xe8cc, fontFamily: _family), - 'icon-qiche': const IconData(0xec75, fontFamily: _family), - 'icon-searchcart': const IconData(0xe7cd, fontFamily: _family), - 'icon-xinxibar-zhanghu': const IconData(0xe8cd, fontFamily: _family), - 'icon-shenjing1': const IconData(0xec76, fontFamily: _family), - 'icon-raw': const IconData(0xe7ce, fontFamily: _family), - 'icon-yidatong': const IconData(0xe8ce, fontFamily: _family), - 'icon-chengshi': const IconData(0xec77, fontFamily: _family), - 'icon-service': const IconData(0xe7cf, fontFamily: _family), - 'icon-zhuanyequanwei': const IconData(0xe8cf, fontFamily: _family), - 'icon-tixingshixin': const IconData(0xec78, fontFamily: _family), - 'icon-share': const IconData(0xe7d0, fontFamily: _family), - 'icon-zhanghucaozuo': const IconData(0xe8d0, fontFamily: _family), - 'icon-menci': const IconData(0xec79, fontFamily: _family), - 'icon-signboard': const IconData(0xe7d1, fontFamily: _family), - 'icon-xuanzhuandu': const IconData(0xe8d1, fontFamily: _family), - 'icon-chazuo': const IconData(0xec7a, fontFamily: _family), - 'icon-shuffling-banner': const IconData(0xe7d2, fontFamily: _family), - 'icon-tuishuirongzi': const IconData(0xe8d2, fontFamily: _family), - 'icon-ranqijianceqi': const IconData(0xec7b, fontFamily: _family), - 'icon-Rightbutton': const IconData(0xe7d3, fontFamily: _family), - 'icon-AddProducts': const IconData(0xe8d3, fontFamily: _family), - 'icon-kaiguan': const IconData(0xec7c, fontFamily: _family), - 'icon-sorting': const IconData(0xe7d4, fontFamily: _family), - 'icon-ziyingyewu': const IconData(0xe8d4, fontFamily: _family), - 'icon-chatou': const IconData(0xec7d, fontFamily: _family), - 'icon-sound-Mute': const IconData(0xe7d5, fontFamily: _family), - 'icon-addcell': const IconData(0xe8d5, fontFamily: _family), - 'icon-xiyiji': const IconData(0xec7e, fontFamily: _family), - 'icon-Similarproducts': const IconData(0xe7d6, fontFamily: _family), - 'icon-background-color': const IconData(0xe8d6, fontFamily: _family), - 'icon-yijiankaiguan': const IconData(0xec7f, fontFamily: _family), - 'icon-sound-filling': const IconData(0xe7d7, fontFamily: _family), - 'icon-cascades': const IconData(0xe8d7, fontFamily: _family), - 'icon-yanwubaojingqi': const IconData(0xec80, fontFamily: _family), - 'icon-suggest': const IconData(0xe7d8, fontFamily: _family), - 'icon-beijing': const IconData(0xe8d8, fontFamily: _family), - 'icon-wuxiandianbo': const IconData(0xec81, fontFamily: _family), - 'icon-stop': const IconData(0xe7d9, fontFamily: _family), - 'icon-bold': const IconData(0xe8d9, fontFamily: _family), - 'icon-fuzhi': const IconData(0xec82, fontFamily: _family), - 'icon-success': const IconData(0xe7da, fontFamily: _family), - 'icon-zijin': const IconData(0xe8da, fontFamily: _family), - 'icon-shanchu': const IconData(0xec83, fontFamily: _family), - 'icon-supplier-features': const IconData(0xe7db, fontFamily: _family), - 'icon-eraser': const IconData(0xe8db, fontFamily: _family), - 'icon-bianjisekuai': const IconData(0xec84, fontFamily: _family), - 'icon-switch': const IconData(0xe7dc, fontFamily: _family), - 'icon-centeralignment': const IconData(0xe8dc, fontFamily: _family), - 'icon-ishipinshixiao': const IconData(0xec85, fontFamily: _family), - 'icon-survey': const IconData(0xe7dd, fontFamily: _family), - 'icon-click': const IconData(0xe8dd, fontFamily: _family), - 'icon-iframetianjia': const IconData(0xec86, fontFamily: _family), - 'icon-template': const IconData(0xe7de, fontFamily: _family), - 'icon-aspjiesuan': const IconData(0xe8de, fontFamily: _family), - 'icon-tupiantianjia': const IconData(0xec87, fontFamily: _family), - 'icon-text': const IconData(0xe7df, fontFamily: _family), - 'icon-flag': const IconData(0xe8df, fontFamily: _family), - 'icon-liebiaomoshi-kuai': const IconData(0xec88, fontFamily: _family), - 'icon-suspended': const IconData(0xe7e0, fontFamily: _family), - 'icon-falg-fill': const IconData(0xe8e0, fontFamily: _family), - 'icon-qiapianmoshi-kuai': const IconData(0xec89, fontFamily: _family), - 'icon-task-management': const IconData(0xe7e1, fontFamily: _family), - 'icon-Fee': const IconData(0xe8e1, fontFamily: _family), - 'icon-fenlan': const IconData(0xec8a, fontFamily: _family), - 'icon-tool': const IconData(0xe7e2, fontFamily: _family), - 'icon-filling': const IconData(0xe8e2, fontFamily: _family), - 'icon-fengexian': const IconData(0xec8b, fontFamily: _family), - 'icon-Top': const IconData(0xe7e3, fontFamily: _family), - 'icon-Foreigncurrency': const IconData(0xe8e3, fontFamily: _family), - 'icon-dianzan': const IconData(0xec8c, fontFamily: _family), - 'icon-smile': const IconData(0xe7e4, fontFamily: _family), - 'icon-guanliyuan': const IconData(0xe8e4, fontFamily: _family), - 'icon-charulianjie': const IconData(0xec8d, fontFamily: _family), - 'icon-textile-products': const IconData(0xe7e5, fontFamily: _family), - 'icon-language': const IconData(0xe8e5, fontFamily: _family), - 'icon-charutupian': const IconData(0xec8e, fontFamily: _family), - 'icon-tradealert': const IconData(0xe7e6, fontFamily: _family), - 'icon-leftalignment': const IconData(0xe8e6, fontFamily: _family), - 'icon-quxiaolianjie': const IconData(0xec8f, fontFamily: _family), - 'icon-topsales': const IconData(0xe7e7, fontFamily: _family), - 'icon-extra-inquiries': const IconData(0xe8e7, fontFamily: _family), - 'icon-wuxupailie': const IconData(0xec90, fontFamily: _family), - 'icon-tradingvolume': const IconData(0xe7e8, fontFamily: _family), - 'icon-Italic': const IconData(0xe8e8, fontFamily: _family), - 'icon-juzhongduiqi': const IconData(0xec91, fontFamily: _family), - 'icon-training': const IconData(0xe7e9, fontFamily: _family), - 'icon-pcm': const IconData(0xe8e9, fontFamily: _family), - 'icon-yinyong': const IconData(0xec92, fontFamily: _family), - 'icon-upload': const IconData(0xe7ea, fontFamily: _family), - 'icon-reducecell': const IconData(0xe8ea, fontFamily: _family), - 'icon-youxupailie': const IconData(0xec93, fontFamily: _family), - 'icon-RFQ-word': const IconData(0xe7eb, fontFamily: _family), - 'icon-rightalignment': const IconData(0xe8eb, fontFamily: _family), - 'icon-youduiqi': const IconData(0xec94, fontFamily: _family), - 'icon-viewlarger': const IconData(0xe7ec, fontFamily: _family), - 'icon-pointerleft': const IconData(0xe8ec, fontFamily: _family), - 'icon-zitidaima': const IconData(0xec95, fontFamily: _family), - 'icon-viewgallery': const IconData(0xe7ed, fontFamily: _family), - 'icon-subscript': const IconData(0xe8ed, fontFamily: _family), - 'icon-xiaolian': const IconData(0xec96, fontFamily: _family), - 'icon-vehivles': const IconData(0xe7ee, fontFamily: _family), - 'icon-square': const IconData(0xe8ee, fontFamily: _family), - 'icon-zitijiacu': const IconData(0xec97, fontFamily: _family), - 'icon-trust': const IconData(0xe7ef, fontFamily: _family), - 'icon-superscript': const IconData(0xe8ef, fontFamily: _family), - 'icon-zitishanchuxian': const IconData(0xec98, fontFamily: _family), - 'icon-warning': const IconData(0xe7f0, fontFamily: _family), - 'icon-tag-subscript': const IconData(0xe8f0, fontFamily: _family), - 'icon-zitishangbiao': const IconData(0xec99, fontFamily: _family), - 'icon-warehouse': const IconData(0xe7f1, fontFamily: _family), - 'icon-danjuzhuanhuan': const IconData(0xe8f1, fontFamily: _family), - 'icon-zitibiaoti': const IconData(0xec9a, fontFamily: _family), - 'icon-shoes': const IconData(0xe7f2, fontFamily: _family), - 'icon-Transfermoney': const IconData(0xe8f2, fontFamily: _family), - 'icon-zitixiahuaxian': const IconData(0xec9b, fontFamily: _family), - 'icon-video': const IconData(0xe7f3, fontFamily: _family), - 'icon-under-line': const IconData(0xe8f3, fontFamily: _family), - 'icon-zitixieti': const IconData(0xec9c, fontFamily: _family), - 'icon-viewlist': const IconData(0xe7f4, fontFamily: _family), - 'icon-xiakuangxian': const IconData(0xe8f4, fontFamily: _family), - 'icon-zitiyanse': const IconData(0xec9d, fontFamily: _family), - 'icon-set': const IconData(0xe7f5, fontFamily: _family), - 'icon-shouqi': const IconData(0xe8f5, fontFamily: _family), - 'icon-zuoduiqi': const IconData(0xec9e, fontFamily: _family), - 'icon-store': const IconData(0xe7f6, fontFamily: _family), - 'icon-zhankai': const IconData(0xe8f6, fontFamily: _family), - 'icon-zitiyulan': const IconData(0xec9f, fontFamily: _family), - 'icon-tool-hardware': const IconData(0xe7f7, fontFamily: _family), - 'icon-tongxunlu': const IconData(0xe8f7, fontFamily: _family), - 'icon-zitixiabiao': const IconData(0xeca0, fontFamily: _family), - 'icon-vs': const IconData(0xe7f8, fontFamily: _family), - 'icon-yiguanzhugongyingshang': const IconData(0xe8f8, fontFamily: _family), - 'icon-zuoyouduiqi': const IconData(0xeca1, fontFamily: _family), - 'icon-toy': const IconData(0xe7f9, fontFamily: _family), - 'icon-goumaipianhao': const IconData(0xe8f9, fontFamily: _family), - 'icon-tianxie': const IconData(0xeca2, fontFamily: _family), - 'icon-sport': const IconData(0xe7fa, fontFamily: _family), - 'icon-Subscribe': const IconData(0xe8fa, fontFamily: _family), - 'icon-huowudui1': const IconData(0xeca3, fontFamily: _family), - 'icon-creditcard': const IconData(0xe7fb, fontFamily: _family), - 'icon-becomeagoldsupplier': const IconData(0xe8fb, fontFamily: _family), - 'icon-yingjian': const IconData(0xeca4, fontFamily: _family), - 'icon-contacts': const IconData(0xe7fc, fontFamily: _family), - 'icon-new': const IconData(0xe8fc, fontFamily: _family), - 'icon-shebeikaifa': const IconData(0xeca5, fontFamily: _family), - 'icon-checkstand': const IconData(0xe7fd, fontFamily: _family), - 'icon-free': const IconData(0xe8fd, fontFamily: _family), - 'icon-dianzan-kuai': const IconData(0xeca6, fontFamily: _family), - 'icon-aviation': const IconData(0xe7fe, fontFamily: _family), - 'icon-cad-fill': const IconData(0xe8fe, fontFamily: _family), - 'icon-zhihuan': const IconData(0xeca7, fontFamily: _family), - 'icon-Daytimemode': const IconData(0xe7ff, fontFamily: _family), - 'icon-robot': const IconData(0xe8ff, fontFamily: _family), - 'icon-tuoguan': const IconData(0xeca8, fontFamily: _family), - 'icon-infantmom': const IconData(0xe800, fontFamily: _family), - 'icon-inspection1': const IconData(0xe900, fontFamily: _family), - 'icon-duigoux': const IconData(0xeca9, fontFamily: _family), - 'icon-discounts': const IconData(0xe801, fontFamily: _family), - 'icon-guanbi1': const IconData(0xecaa, fontFamily: _family), - 'icon-invoice': const IconData(0xe802, fontFamily: _family), - 'icon-aixin-shixin': const IconData(0xecab, fontFamily: _family), - 'icon-insurance': const IconData(0xe803, fontFamily: _family), - 'icon-ranqixieloubaojingqi': const IconData(0xecac, fontFamily: _family), - 'icon-nightmode': const IconData(0xe804, fontFamily: _family), - 'icon-dianbiao-shiti': const IconData(0xecad, fontFamily: _family), - 'icon-usercenter': const IconData(0xe805, fontFamily: _family), - 'icon-aixin': const IconData(0xecae, fontFamily: _family), - 'icon-unlock': const IconData(0xe806, fontFamily: _family), - 'icon-shuibiao-shiti': const IconData(0xecaf, fontFamily: _family), - 'icon-vip': const IconData(0xe807, fontFamily: _family), - 'icon-zhinengxiaofangshuan': const IconData(0xecb0, fontFamily: _family), - 'icon-wallet': const IconData(0xe808, fontFamily: _family), - 'icon-shoucangjia': const IconData(0xe600, fontFamily: _family), - 'icon-ranqibiao-shiti': const IconData(0xecb1, fontFamily: _family), - 'icon-landtransportation': const IconData(0xe809, fontFamily: _family), - 'icon-tiaoshi': const IconData(0xeb61, fontFamily: _family), - 'icon-shexiangtou-shiti': const IconData(0xecb2, fontFamily: _family), - 'icon-voice': const IconData(0xe80a, fontFamily: _family), - 'icon-changjingguanli': const IconData(0xeb62, fontFamily: _family), - 'icon-shexiangtou-guanbi': const IconData(0xecb3, fontFamily: _family), - 'icon-exchangerate': const IconData(0xe80b, fontFamily: _family), - 'icon-bianji': const IconData(0xeb63, fontFamily: _family), - 'icon-shexiangtou': const IconData(0xecb4, fontFamily: _family), - 'icon-contacts-fill': const IconData(0xe80c, fontFamily: _family), - 'icon-guanlianshebei': const IconData(0xeb64, fontFamily: _family), - 'icon-shengyin-shiti': const IconData(0xecb5, fontFamily: _family), - 'icon-add-account1': const IconData(0xe80d, fontFamily: _family), - 'icon-guanfangbanben': const IconData(0xeb65, fontFamily: _family), - 'icon-shengyinkai': const IconData(0xecb6, fontFamily: _family), - 'icon-years-fill': const IconData(0xe80e, fontFamily: _family), - 'icon-gongnengdingyi': const IconData(0xeb66, fontFamily: _family), - 'icon-shoucang-shixin': const IconData(0xecb7, fontFamily: _family), - 'icon-add-cart-fill': const IconData(0xe80f, fontFamily: _family), - 'icon-jichuguanli': const IconData(0xeb67, fontFamily: _family), - 'icon-shoucang': const IconData(0xecb8, fontFamily: _family), - 'icon-add-fill': const IconData(0xe810, fontFamily: _family), - 'icon-jishufuwu': const IconData(0xeb68, fontFamily: _family), - 'icon-shengyinwu': const IconData(0xecb9, fontFamily: _family), - 'icon-all-fill1': const IconData(0xe811, fontFamily: _family), - 'icon-hezuohuobanmiyueguanli': const IconData(0xeb69, fontFamily: _family), - 'icon-shengyinjingyin': const IconData(0xecba, fontFamily: _family), - 'icon-ashbin-fill': const IconData(0xe812, fontFamily: _family), - 'icon-ceshishenqing': const IconData(0xeb6a, fontFamily: _family), - 'icon-zhunbeiliangchan': const IconData(0xecbb, fontFamily: _family), - 'icon-calendar-fill': const IconData(0xe813, fontFamily: _family), - 'icon-jiedianguanli': const IconData(0xeb6b, fontFamily: _family), - 'icon-shebeikaifa1': const IconData(0xecbc, fontFamily: _family), - 'icon-bad-fill': const IconData(0xe814, fontFamily: _family), - 'icon-jinggao': const IconData(0xeb6c, fontFamily: _family), - 'icon-kongxinwenhao': const IconData(0xed19, fontFamily: _family), - 'icon-bussiness-man-fill': const IconData(0xe815, fontFamily: _family), - 'icon-peiwangyindao': const IconData(0xeb6d, fontFamily: _family), - 'icon-cuowukongxin': const IconData(0xed1a, fontFamily: _family), - 'icon-atm-fill': const IconData(0xe816, fontFamily: _family), - 'icon-renjijiaohu': const IconData(0xeb6e, fontFamily: _family), - 'icon-fangkuai1': const IconData(0xed1b, fontFamily: _family), - 'icon-cart-full-fill': const IconData(0xe817, fontFamily: _family), - 'icon-shiyongwendang': const IconData(0xeb6f, fontFamily: _family), - 'icon-fangkuai2': const IconData(0xed1c, fontFamily: _family), - 'icon-cart-Empty-fill': const IconData(0xe818, fontFamily: _family), - 'icon-quanxianshenpi': const IconData(0xeb70, fontFamily: _family), - 'icon-kongjianxuanzhong1': const IconData(0xed1d, fontFamily: _family), - 'icon-cameraswitching-fill': const IconData(0xe819, fontFamily: _family), - 'icon-yishouquan': const IconData(0xeb71, fontFamily: _family), - 'icon-kongxinduigou1': const IconData(0xed1e, fontFamily: _family), - 'icon-atm-away-fill': const IconData(0xe81a, fontFamily: _family), - 'icon-tianshenpi': const IconData(0xeb72, fontFamily: _family), - 'icon-xinxikongxin': const IconData(0xed1f, fontFamily: _family), - 'icon-certified-supplier-fill': const IconData(0xe81b, fontFamily: _family), - 'icon-shujukanban': const IconData(0xeb73, fontFamily: _family), - 'icon-kongjian': const IconData(0xed20, fontFamily: _family), - 'icon-calculator-fill': const IconData(0xe81c, fontFamily: _family), - 'icon-yingyongguanli': const IconData(0xeb74, fontFamily: _family), - 'icon-gaojingkongxin': const IconData(0xed21, fontFamily: _family), - 'icon-clock-fill': const IconData(0xe81d, fontFamily: _family), - 'icon-yibiaopan': const IconData(0xeb75, fontFamily: _family), - 'icon-duigou-kuai': const IconData(0xed22, fontFamily: _family), - 'icon-ali-clould-fill': const IconData(0xe81e, fontFamily: _family), - 'icon-zhanghaoquanxianguanli': const IconData(0xeb76, fontFamily: _family), - 'icon-cuocha-kuai': const IconData(0xed23, fontFamily: _family), - 'icon-color-fill': const IconData(0xe81f, fontFamily: _family), - 'icon-yuanquyunwei': const IconData(0xeb77, fontFamily: _family), - 'icon-jia-sekuai': const IconData(0xed24, fontFamily: _family), - 'icon-coupons-fill': const IconData(0xe820, fontFamily: _family), - 'icon-jizhanguanli': const IconData(0xeb78, fontFamily: _family), - 'icon-jian-sekuai': const IconData(0xed25, fontFamily: _family), - 'icon-cecurity-protection-fill': const IconData(0xe821, fontFamily: _family), - 'icon-guanbi': const IconData(0xeb79, fontFamily: _family), - 'icon-fenxiangfangshi': const IconData(0xed2e, fontFamily: _family), - 'icon-credit-level-fill': const IconData(0xe822, fontFamily: _family), - 'icon-zidingyi': const IconData(0xeb7a, fontFamily: _family), - 'icon-auto': const IconData(0xe6eb, fontFamily: _family), - 'icon-default-template-fill': const IconData(0xe823, fontFamily: _family), - 'icon-xiajiantou': const IconData(0xeb7b, fontFamily: _family), - 'icon-all': const IconData(0xe6ef, fontFamily: _family), - 'icon-CurrencyConverter-fill': const IconData(0xe824, fontFamily: _family), - 'icon-shangjiantou': const IconData(0xeb7c, fontFamily: _family), - 'icon-bussiness-man': const IconData(0xe6f0, fontFamily: _family), - 'icon-Customermanagement-fill': const IconData(0xe825, fontFamily: _family), - 'icon-icon-loading': const IconData(0xeb80, fontFamily: _family), - 'icon-widgets': const IconData(0xe6f2, fontFamily: _family), - 'icon-discounts-fill': const IconData(0xe826, fontFamily: _family), - 'icon-icon-renwujincheng': const IconData(0xeb88, fontFamily: _family), - 'icon-code': const IconData(0xe6f3, fontFamily: _family), - 'icon-Daytimemode-fill': const IconData(0xe827, fontFamily: _family), - 'icon-icon-rukou': const IconData(0xeb89, fontFamily: _family), - 'icon-copy': const IconData(0xe6f4, fontFamily: _family), - 'icon-exl-fill': const IconData(0xe828, fontFamily: _family), - 'icon-icon-yiwenkongxin': const IconData(0xeb8a, fontFamily: _family), - 'icon-dollar': const IconData(0xe6f5, fontFamily: _family), - 'icon-cry-fill': const IconData(0xe829, fontFamily: _family), - 'icon-icon-fabu': const IconData(0xeb8b, fontFamily: _family), - 'icon-history': const IconData(0xe6f8, fontFamily: _family), - 'icon-email-fill': const IconData(0xe82a, fontFamily: _family), - 'icon-icon-tianjia': const IconData(0xeb8c, fontFamily: _family), - 'icon-editor': const IconData(0xe6f6, fontFamily: _family), - 'icon-filter-fill': const IconData(0xe82b, fontFamily: _family), - 'icon-icon-yulan': const IconData(0xeb8d, fontFamily: _family), - 'icon-data': const IconData(0xe6f9, fontFamily: _family), - 'icon-folder-fill': const IconData(0xe82c, fontFamily: _family), - 'icon-icon-zhanghao': const IconData(0xeb8e, fontFamily: _family), - 'icon-gift': const IconData(0xe6fa, fontFamily: _family), - 'icon-feeds-fill': const IconData(0xe82d, fontFamily: _family), - 'icon-icon-wangye': const IconData(0xeb8f, fontFamily: _family), - 'icon-integral': const IconData(0xe6fb, fontFamily: _family), - 'icon-gold-supplie-fill': const IconData(0xe82e, fontFamily: _family), - 'icon-icon-shezhi': const IconData(0xeb90, fontFamily: _family), - 'icon-nav-list': const IconData(0xe6fd, fontFamily: _family), - 'icon-form-fill': const IconData(0xe82f, fontFamily: _family), - 'icon-icon-baocun': const IconData(0xeb91, fontFamily: _family), - 'icon-pic': const IconData(0xe6ff, fontFamily: _family), - 'icon-camera-fill': const IconData(0xe830, fontFamily: _family), - 'icon-icon-yingyongguanli': const IconData(0xeb92, fontFamily: _family), - 'icon-Notvisible': const IconData(0xe6fe, fontFamily: _family), - 'icon-good-fill': const IconData(0xe831, fontFamily: _family), - 'icon-icon-shiyongwendang': const IconData(0xeb93, fontFamily: _family), - 'icon-play': const IconData(0xe701, fontFamily: _family), - 'icon-image-text-fill': const IconData(0xe832, fontFamily: _family), - 'icon-icon-bangzhuwendang': const IconData(0xeb94, fontFamily: _family), - 'icon-rising': const IconData(0xe703, fontFamily: _family), - 'icon-inspection-fill': const IconData(0xe833, fontFamily: _family), - 'icon-biaodanzujian-shurukuang': const IconData(0xeb95, fontFamily: _family), - 'icon-QRcode': const IconData(0xe704, fontFamily: _family), - 'icon-hot-fill': const IconData(0xe834, fontFamily: _family), - }; - - static IconData query(String key) { - return _iconMapping[key] ?? _iconMapping[_defaultIconKey]!; - } -} diff --git a/lib/utils/json.dart b/lib/utils/json.dart deleted file mode 100644 index 2ecc5b5b8..000000000 --- a/lib/utils/json.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'dart:convert'; - -import 'package:flutter/cupertino.dart'; - -T? decodeJsonObject(String? json, T Function(dynamic obj) transform) { - if (json == null) return null; - try { - final obj = jsonDecode(json); - return transform(obj); - } catch (_) { - debugPrint("Failed to decode $json"); - return null; - } -} - -String? encodeJsonObject(T? obj, [dynamic Function(T obj)? transform]) { - if (obj == null) return null; - try { - final json = transform != null ? transform(obj) : (obj as dynamic).toJson(); - return jsonEncode(json); - } catch (_) { - debugPrint("Failed to encode $json"); - return null; - } -} - -List? decodeJsonList(String? json, T Function(dynamic element) transform) { - if (json == null) return null; - try { - final list = jsonDecode(json) as List; - return list.map(transform).toList(); - } catch (_) { - debugPrint("Failed to decode $json"); - return null; - } -} - -String? encodeJsonList(List? list, [dynamic Function(T element)? transform]) { - if (list == null) return null; - try { - final json = list.map(transform ?? (e) => (e as dynamic).toJson()).toList(); - return jsonEncode(json); - } catch (_) { - debugPrint("Failed to encode $json"); - return null; - } -} diff --git a/lib/utils/permission.dart b/lib/utils/permission.dart deleted file mode 100644 index 1888ebb17..000000000 --- a/lib/utils/permission.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:permission_handler/permission_handler.dart'; - -Future ensurePermission(Permission permission) async { - PermissionStatus status = await permission.status; - - if (status != PermissionStatus.granted) { - status = await Permission.storage.request(); - } - return status == PermissionStatus.granted; -} diff --git a/lib/utils/strings.dart b/lib/utils/strings.dart deleted file mode 100644 index 767162292..000000000 --- a/lib/utils/strings.dart +++ /dev/null @@ -1,5 +0,0 @@ -extension StringEx on String { - String removeSuffix(String suffix) => endsWith(suffix) ? substring(0, length - suffix.length) : this; - - String removePrefix(String prefix) => startsWith(prefix) ? substring(prefix.length) : this; -} diff --git a/lib/utils/timer.dart b/lib/utils/timer.dart deleted file mode 100644 index 68a23bc27..000000000 --- a/lib/utils/timer.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'dart:async'; - -Timer runPeriodically( - Duration duration, - void Function(Timer timer) callback, -) { - final timer = Timer.periodic(duration, callback); - Timer.run(() => callback(timer)); - return timer; -} diff --git a/lib/utils/vibration.dart b/lib/utils/vibration.dart deleted file mode 100644 index 88df40c6e..000000000 --- a/lib/utils/vibration.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'package:universal_platform/universal_platform.dart'; -import 'package:vibration/vibration.dart' as vb; - -abstract class VibrationProtocol { - Future emit(); -} - -abstract class TimedProtocol { - int get milliseconds; -} - -class Vibration implements VibrationProtocol, TimedProtocol { - @override - final int milliseconds; - final int amplitude; - - const Vibration({this.milliseconds = 500, this.amplitude = -1}); - - @override - Future emit() async { - if (UniversalPlatform.isAndroid || UniversalPlatform.isIOS) { - if (await vb.Vibration.hasVibrator() ?? false) { - if (await vb.Vibration.hasCustomVibrationsSupport() ?? false) { - vb.Vibration.vibrate(duration: milliseconds, amplitude: amplitude); - } else { - vb.Vibration.vibrate(); - } - } - } - } - - VibrationProtocol operator +(Wait wait) { - return CompoundVibration._(timedList: [this, wait]); - } -} - -class Wait implements TimedProtocol { - @override - final int milliseconds; - - const Wait({this.milliseconds = 500}); -} - -class CompoundVibration implements VibrationProtocol { - final List timedList; - - CompoundVibration._({this.timedList = const []}); - - @override - Future emit() async { - if (UniversalPlatform.isAndroid || UniversalPlatform.isIOS) { - if (await vb.Vibration.hasVibrator() ?? false) { - if (await vb.Vibration.hasCustomVibrationsSupport() ?? false) { - vb.Vibration.vibrate(pattern: timedList.map((e) => e.milliseconds).toList()); - } else { - for (int i = 0; i < timedList.length; i++) { - if (i % 2 == 0) { - vb.Vibration.vibrate(); - } else { - await Future.delayed(Duration(milliseconds: timedList[i].milliseconds)); - } - } - } - } - } - } - - VibrationProtocol operator +(CompoundVibration other) { - return CompoundVibration._(timedList: timedList + other.timedList); - } -} diff --git a/lib/widgets/captcha_box.dart b/lib/widgets/captcha_box.dart deleted file mode 100644 index e22f98b27..000000000 --- a/lib/widgets/captcha_box.dart +++ /dev/null @@ -1,80 +0,0 @@ -import 'dart:typed_data'; - -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:sit/design/adaptive/foundation.dart'; -import 'package:sit/l10n/common.dart'; -import 'package:rettulf/rettulf.dart'; - -class CaptchaDialog extends StatefulWidget { - final Uint8List captchaData; - - const CaptchaDialog({ - super.key, - required this.captchaData, - }); - - @override - State createState() => _CaptchaDialogState(); -} - -class _CaptchaDialogState extends State { - final $captcha = TextEditingController(); - - @override - Widget build(BuildContext context) { - return $Dialog$( - title: _i18n.title, - primary: $Action$( - text: _i18n.submit, - warning: true, - isDefault: true, - onPressed: () { - context.navigator.pop($captcha.text); - }, - ), - secondary: $Action$( - text: _i18n.cancel, - onPressed: () { - context.navigator.pop(null); - }, - ), - make: (ctx) => [ - Image.memory( - widget.captchaData, - scale: 0.5, - ), - $TextField$( - controller: $captcha, - autofocus: true, - placeholder: _i18n.enterHint, - keyboardType: TextInputType.text, - autofillHints: const [AutofillHints.oneTimeCode], - onSubmit: (value) { - context.navigator.pop(value); - }, - ).padOnly(t: 15), - ].column(mas: MainAxisSize.min).padAll(5), - ); - } - - @override - void dispose() { - super.dispose(); - $captcha.dispose(); - } -} - -const _i18n = CaptchaI18n(); - -class CaptchaI18n with CommonI18nMixin { - static const ns = "captcha"; - - const CaptchaI18n(); - - String get title => "$ns.title".tr(); - - String get enterHint => "$ns.enterHint".tr(); - - String get emptyInputError => "$ns.emptyInputError".tr(); -} diff --git a/lib/widgets/html.dart b/lib/widgets/html.dart deleted file mode 100644 index d6e65bf1f..000000000 --- a/lib/widgets/html.dart +++ /dev/null @@ -1,116 +0,0 @@ -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; -import 'package:go_router/go_router.dart'; -import 'package:sit/utils/guard_launch.dart'; -import 'package:rettulf/rettulf.dart'; - -class RestyledHtmlWidget extends StatelessWidget { - final String html; - final RenderMode renderMode; - final TextStyle? textStyle; - - const RestyledHtmlWidget( - this.html, { - super.key, - this.renderMode = RenderMode.column, - this.textStyle, - }); - - @override - Widget build(BuildContext context) { - final textStyle = this.textStyle ?? context.textTheme.bodyMedium; - return HtmlWidget( - html, - buildAsync: true, - renderMode: renderMode, - factoryBuilder: () => RestyledWidgetFactory( - textStyle: textStyle, - borderColor: context.colorScheme.surfaceVariant, - ), - textStyle: textStyle, - onTapUrl: (url) async { - return await guardLaunchUrlString(context, url); - }, - onTapImage: (ImageMetadata image) { - final url = image.sources.toList().firstOrNull?.url; - final title = image.title ?? image.alt; - context.push( - Uri(path: "/image", queryParameters: { - if (title != null && title.isNotEmpty) "title": title, - if (url?.startsWith("http") == true) "origin": url, - }).toString(), - extra: url, - ); - }, - ); - } -} - -class RestyledWidgetFactory extends WidgetFactory { - final TextStyle? textStyle; - final Color? borderColor; - - RestyledWidgetFactory({ - this.textStyle, - this.borderColor, - }); - - @override - InlineSpan? buildTextSpan({ - List? children, - GestureRecognizer? recognizer, - TextStyle? style, - String? text, - }) { - return super.buildTextSpan( - children: children, - recognizer: recognizer, - style: textStyle?.copyWith( - color: style?.color, - decoration: style?.decoration, - decorationColor: style?.decorationColor, - decorationStyle: style?.decorationStyle, - decorationThickness: style?.decorationThickness, - fontStyle: style?.fontStyle, - ), - text: text, - ); - } - - @override - Widget? buildDecoration( - BuildTree tree, - Widget child, { - BoxBorder? border, - BorderRadius? borderRadius, - Color? color, - DecorationImage? image, - }) { - return super.buildDecoration( - tree, - child, - border: _restyleBorder(border, borderColor), - borderRadius: borderRadius, - color: Colors.transparent, - image: image, - ); - } -} - -BoxBorder? _restyleBorder(BoxBorder? border, Color? color) { - if (border is Border) { - return Border( - top: _restyleBorderSide(border.top, color), - right: _restyleBorderSide(border.right, color), - bottom: _restyleBorderSide(border.top, color), - left: _restyleBorderSide(border.left, color), - ); - } else { - return border; - } -} - -BorderSide _restyleBorderSide(BorderSide side, Color? color) { - return side.copyWith(color: color); -} diff --git a/lib/widgets/image.dart b/lib/widgets/image.dart deleted file mode 100644 index 5f0f20729..000000000 --- a/lib/widgets/image.dart +++ /dev/null @@ -1,168 +0,0 @@ -import 'dart:convert'; -import 'dart:typed_data'; - -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:flutter/material.dart'; -import 'package:sit/l10n/common.dart'; -import 'package:rettulf/rettulf.dart'; - -const _i18n = CommonI18n(); - -enum _ImageMode { - url, - base64, - error, -} - -class CachedNetworkImageView extends StatelessWidget { - final String imageUrl; - - const CachedNetworkImageView({ - super.key, - required this.imageUrl, - }); - - @override - Widget build(BuildContext context) { - return CachedNetworkImage( - imageUrl: imageUrl, - placeholder: (context, url) => const CircularProgressIndicator.adaptive(), - errorWidget: (context, url, error) => const Icon(Icons.broken_image_rounded), - fit: BoxFit.fitHeight, - ); - } -} - -class ImageView extends StatefulWidget { - /// It could be a url of image, or a base64 string. - final String? data; - - const ImageView( - this.data, { - super.key, - }); - - @override - State createState() => _ImageViewState(); -} - -class _ImageViewState extends State { - var mode = _ImageMode.url; - Uint8List? imageData; - - @override - void initState() { - super.initState(); - refresh(); - } - - @override - void didUpdateWidget(covariant ImageView oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.data != oldWidget.data) { - refresh(); - } - } - - void refresh() { - final data = widget.data; - setState(() { - imageData = null; - }); - if (data == null) { - setState(() { - mode = _ImageMode.error; - }); - } else if (data.startsWith("data")) { - setState(() { - mode = _ImageMode.base64; - }); - try { - final parts = data.split(","); - final bytes = const Base64Decoder().convert(parts[1]); - setState(() { - imageData = bytes; - }); - } catch (error) { - setState(() { - mode = _ImageMode.error; - }); - } - } else { - setState(() { - mode = _ImageMode.url; - }); - } - } - - @override - Widget build(BuildContext context) { - ImageProvider? provider; - final data = widget.data; - final imageData = this.imageData; - if (mode == _ImageMode.url && data != null) { - provider = CachedNetworkImageProvider(data); - } else if (mode == _ImageMode.base64 && imageData != null) { - provider = MemoryImage(imageData); - } else { - provider = null; - } - // TODO: zoom overlay - return InteractiveViewer( - minScale: 1, - maxScale: 10.0, - child: provider == null ? buildBrokenImage() : buildImage(provider), - ); - } - - Widget buildImage(ImageProvider provider) { - return Image( - image: provider, - loadingBuilder: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) { - if (loadingProgress == null) { - return child; - } else { - final current = loadingProgress.cumulativeBytesLoaded; - final total = loadingProgress.expectedTotalBytes; - if (total == null || total == 0) { - return const CircularProgressIndicator.adaptive(); - } else { - return CircularProgressIndicator(value: current / total); - } - } - }, - errorBuilder: (ctx, error, stacktrace) { - return buildBrokenImage(); - }, - ); - } - - Widget buildBrokenImage() { - return const FittedBox( - fit: BoxFit.fill, - child: Icon(Icons.broken_image_rounded), - ); - } -} - -class ImageViewPage extends StatelessWidget { - final String? data; - final String? title; - - const ImageViewPage( - this.data, { - super.key, - this.title, - }); - - @override - Widget build(BuildContext context) { - // TODO: Save the image. - return Scaffold( - appBar: AppBar( - title: (title ?? _i18n.untitled).text(), - ), - body: ImageView(data).center(), - ); - } -} diff --git a/lib/widgets/lazy_list.dart b/lib/widgets/lazy_list.dart deleted file mode 100644 index edc81bd59..000000000 --- a/lib/widgets/lazy_list.dart +++ /dev/null @@ -1,86 +0,0 @@ -import 'package:flutter/widgets.dart'; -// steal from "https://github.com/QuirijnGB/lazy-load-scrollview" - -// ignore: constant_identifier_names -enum LoadingStatus { LOADING, STABLE } - -/// Signature for EndOfPageListeners -typedef EndOfPageListenerCallback = void Function(); - -/// A widget that wraps a [Widget] and will trigger [onEndOfPage] when it -/// reaches the bottom of the list -class LazyColumn extends StatefulWidget { - /// The [Widget] that this widget watches for changes on - final Widget child; - - /// Called when the [child] reaches the end of the list - final EndOfPageListenerCallback onEndOfPage; - - /// The offset to take into account when triggering [onEndOfPage] in pixels - final int scrollOffset; - - /// Used to determine if loading of new data has finished. You should use set this if you aren't using a FutureBuilder or StreamBuilder - final bool isLoading; - - /// Prevented update nested listview with other axis direction - final Axis scrollDirection; - - @override - State createState() => LazyColumnState(); - - const LazyColumn({ - super.key, - required this.child, - required this.onEndOfPage, - this.scrollDirection = Axis.vertical, - this.isLoading = false, - this.scrollOffset = 100, - }); -} - -class LazyColumnState extends State { - LoadingStatus loadMoreStatus = LoadingStatus.STABLE; - - @override - void didUpdateWidget(LazyColumn oldWidget) { - super.didUpdateWidget(oldWidget); - if (!widget.isLoading) { - loadMoreStatus = LoadingStatus.STABLE; - } - } - - @override - Widget build(BuildContext context) { - return NotificationListener( - child: widget.child, - onNotification: (notification) => _onNotification(notification, context), - ); - } - - bool _onNotification(ScrollNotification notification, BuildContext context) { - if (widget.scrollDirection == notification.metrics.axis) { - if (notification is ScrollUpdateNotification) { - if (notification.metrics.maxScrollExtent > notification.metrics.pixels && - notification.metrics.maxScrollExtent - notification.metrics.pixels <= widget.scrollOffset) { - _loadMore(); - } - return true; - } - - if (notification is OverscrollNotification) { - if (notification.overscroll > 0) { - _loadMore(); - } - return true; - } - } - return false; - } - - void _loadMore() { - if (loadMoreStatus == LoadingStatus.STABLE) { - loadMoreStatus = LoadingStatus.LOADING; - widget.onEndOfPage(); - } - } -} diff --git a/lib/widgets/markdown_widget.dart b/lib/widgets/markdown_widget.dart deleted file mode 100644 index e1284b81e..000000000 --- a/lib/widgets/markdown_widget.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:markdown/markdown.dart'; - -import 'html.dart'; - -class MyMarkdownWidget extends StatelessWidget { - final String markdown; - - const MyMarkdownWidget( - this.markdown, { - super.key, - }); - - @override - Widget build(BuildContext context) { - final html = markdownToHtml( - markdown, - inlineSyntaxes: [ - InlineHtmlSyntax(), - StrikethroughSyntax(), - AutolinkExtensionSyntax(), - EmojiSyntax(), - ], - blockSyntaxes: const [ - FencedCodeBlockSyntax(), - TableSyntax(), - ], - ); - return RestyledHtmlWidget(html); - } -} diff --git a/lib/widgets/not_found.dart b/lib/widgets/not_found.dart deleted file mode 100644 index a5e6f4eb0..000000000 --- a/lib/widgets/not_found.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:sit/design/widgets/common.dart'; -import 'package:sit/settings/settings.dart'; - -class NotFoundPage extends StatelessWidget { - final String routeName; - - const NotFoundPage(this.routeName, {super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: _i18n.title.tr().text(), - ), - body: LeavingBlank( - icon: Icons.browser_not_supported, - desc: Settings.isDeveloperMode ? routeName : _i18n.subtitle, - ), - ); - } -} - -const _i18n = _I18n(); - -class _I18n { - const _I18n(); - - static const ns = "404"; - - String get title => "$ns.title"; - - String get subtitle => "$ns.subtitle"; -} diff --git a/lib/widgets/page_grouper.dart b/lib/widgets/page_grouper.dart deleted file mode 100644 index 576b24c1d..000000000 --- a/lib/widgets/page_grouper.dart +++ /dev/null @@ -1,251 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/material.dart'; -import 'package:rettulf/rettulf.dart'; - -// steal from "https://github.com/Akifcan/flutter_pagination" -class PageGrouper extends StatefulWidget { - final SkipBtnStyle preBtnStyles; - final PageBtnStyles paginateButtonStyles; - final bool useSkipButton; - final int currentPageIndex; - final int totalPage; - final int btnPerGroup; - final double? width; - final double? height; - final Function(int number) onPageChange; - - const PageGrouper({ - super.key, - this.width, - this.height, - this.useSkipButton = true, - required this.preBtnStyles, - required this.paginateButtonStyles, - required this.onPageChange, - required this.totalPage, - required this.btnPerGroup, - required this.currentPageIndex, - }); - - @override - State createState() => _PageGrouperState(); -} - -class _PageGrouperState extends State { - late PageController pageController; - List> groupedPages = []; - double defaultHeight = 50; - - void groupPageBtn() { - final btnPerGroup = min(widget.btnPerGroup, widget.totalPage); - List curGroup = []; - setState(() { - groupedPages = []; - - for (int i = 0; i < widget.totalPage; i++) { - curGroup.add(i); - if (curGroup.length >= btnPerGroup) { - groupedPages.add(curGroup); - curGroup = []; - } - } - if (curGroup.isNotEmpty) { - groupedPages.add(curGroup); - } - }); - } - - @override - Widget build(BuildContext context) { - groupPageBtn(); - pageController = PageController(); - return buildGroupedChild(context); - } - - Widget buildGroupedChild(BuildContext ctx) { - return [ - if (widget.useSkipButton) - _SkipBtn( - buttonStyles: widget.preBtnStyles, - height: widget.height ?? defaultHeight, - onTap: () { - pageController.previousPage( - duration: const Duration(milliseconds: 500), curve: Curves.fastEaseInToSlowEaseOut); - }, - isPre: true, - ), - PageView.builder( - controller: pageController, - itemCount: groupedPages.where((element) => element.isNotEmpty).length, - itemBuilder: (_, index) { - return Row( - children: groupedPages[index].map((e) { - return _PageBtn( - active: widget.currentPageIndex == e + 1, - buttonStyles: widget.paginateButtonStyles, - height: widget.height ?? defaultHeight, - page: e + 1, - color: e + 1 == widget.currentPageIndex ? Colors.blueGrey : Colors.blue, - onTap: (number) { - widget.onPageChange(number); - }).expanded(); - }).toList(), - ); - }).expanded(), - if (widget.useSkipButton) - _SkipBtn( - buttonStyles: widget.preBtnStyles.symmetricL2R(), - height: widget.height ?? defaultHeight, - onTap: () { - pageController.nextPage(duration: const Duration(milliseconds: 500), curve: Curves.fastEaseInToSlowEaseOut); - }, - isPre: false, - ), - ].row().sized( - w: widget.width ?? ctx.mediaQuery.size.width, - h: 60, - ); - } -} - -class _SkipBtn extends StatelessWidget { - final SkipBtnStyle buttonStyles; - final double height; - final bool isPre; - final VoidCallback onTap; - - const _SkipBtn({ - super.key, - required this.buttonStyles, - required this.height, - required this.isPre, - required this.onTap, - }); - - final double radius = 20; - - @override - Widget build(BuildContext context) { - return Material( - clipBehavior: Clip.hardEdge, - borderRadius: buttonStyles.borderRadius ?? BorderRadius.circular(0), - color: context.theme.colorScheme.background, - child: InkWell( - onTap: onTap, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: isPre - ? buttonStyles.icon ?? const Icon(Icons.chevron_left, size: 35) - : buttonStyles.icon ?? const Icon(Icons.chevron_right, size: 35), - ), - ), - ).sized(h: height); - } -} - -class _PageBtn extends StatelessWidget { - final bool active; - final double height; - final int page; - final Color color; - final Function(int number) onTap; - final PageBtnStyles buttonStyles; - - const _PageBtn({ - super.key, - required this.active, - required this.buttonStyles, - required this.page, - required this.height, - required this.color, - required this.onTap, - }); - - @override - Widget build(BuildContext context) { - return Material( - clipBehavior: Clip.hardEdge, - borderRadius: buttonStyles.borderRadius ?? BorderRadius.circular(0), - color: active ? context.colorScheme.surfaceVariant : buttonStyles.bgColor ?? context.theme.colorScheme.background, - child: InkWell( - onTap: () { - onTap(page); - }, - child: page - .toString() - .text(style: active ? buttonStyles.activeTextStyle : buttonStyles.textStyle, textAlign: TextAlign.center) - .center(), - ), - ).sized( - h: height, - w: MediaQuery.of(context).size.width, - ); - } -} - -class PageBtnStyles { - final double? fontSize; - final BorderRadius? borderRadius; - final Color? bgColor; - final Color? activeBgColor; - final TextStyle? textStyle; - final TextStyle? activeTextStyle; - - PageBtnStyles({ - this.fontSize, - this.bgColor, - this.activeBgColor, - this.borderRadius, - this.textStyle, - this.activeTextStyle, - }); - - PageBtnStyles copyWith({ - double? fontSize, - BorderRadius? borderRadius, - Color? bgColor, - Color? activeBgColor, - TextStyle? textStyle, - TextStyle? activeTextStyle, - }) => - PageBtnStyles( - fontSize: fontSize ?? this.fontSize, - bgColor: bgColor ?? this.bgColor, - activeBgColor: activeBgColor ?? this.activeBgColor, - borderRadius: borderRadius ?? this.borderRadius, - textStyle: textStyle ?? this.textStyle, - activeTextStyle: activeTextStyle ?? this.activeTextStyle, - ); -} - -class SkipBtnStyle { - final Icon? icon; - final BorderRadius? borderRadius; - final Color? color; - - SkipBtnStyle({ - this.icon, - this.borderRadius, - this.color, - }); - - SkipBtnStyle copyWith({ - Icon? icon, - BorderRadius? borderRadius, - Color? color, - }) => - SkipBtnStyle( - icon: icon ?? this.icon, - borderRadius: borderRadius ?? this.borderRadius, - color: color ?? this.color, - ); - - SkipBtnStyle symmetricL2R() => copyWith(borderRadius: borderRadius?.symmetricL2R()); -} - -extension _BorderRadiusEx on BorderRadius { - BorderRadius symmetricL2R() { - return BorderRadius.only(topRight: topLeft, bottomRight: bottomLeft); - } -} diff --git a/lib/widgets/placeholder_future_builder.dart b/lib/widgets/placeholder_future_builder.dart deleted file mode 100644 index acbb1ab05..000000000 --- a/lib/widgets/placeholder_future_builder.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; - -enum FutureState { loading, failed, end } - -typedef PlaceholderWidgetBuilder = Widget Function(BuildContext context, T? data, FutureState state); - -class PlaceholderBuilderController { - late _PlaceholderFutureBuilderState _state; - - void _bindState(State> state) => _state = state as _PlaceholderFutureBuilderState; - - Future refresh() => _state.refresh(); -} - -class PlaceholderFutureBuilder extends StatefulWidget { - final Future? future; - final PlaceholderWidgetBuilder builder; - final PlaceholderBuilderController? controller; - - final Future Function()? futureGetter; - - const PlaceholderFutureBuilder({ - super.key, - this.future, - required this.builder, - this.controller, - this.futureGetter, - }); - - @override - State> createState() => _PlaceholderFutureBuilderState(); -} - -class _PlaceholderFutureBuilderState extends State> { - Completer completer = Completer(); - - @override - void initState() { - widget.controller?._bindState(this); - super.initState(); - } - - @override - Widget build(BuildContext context) { - return FutureBuilder( - key: UniqueKey(), - future: fetchData(), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - if (snapshot.hasData) { - return widget.builder(context, snapshot.data, FutureState.end); - } else if (snapshot.hasError) { - return widget.builder(context, null, FutureState.failed); - } else { - if (!completer.isCompleted) completer.complete(); - throw Exception('snapshot has no data or error'); - } - } - return widget.builder(context, null, FutureState.loading); - }, - ); - } - - Future refresh() { - setState(() {}); - return completer.future; - } - - Future fetchData() async { - var getter = widget.futureGetter; - if (getter != null) { - return await getter(); - } - var future = widget.future; - if (future != null) { - return await future; - } - throw UnsupportedError('PlaceholderFutureBuilder requires a Future or FutureGetter'); - } -} diff --git a/lib/widgets/search.dart b/lib/widgets/search.dart deleted file mode 100644 index 9efee9fe7..000000000 --- a/lib/widgets/search.dart +++ /dev/null @@ -1,223 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:sit/design/widgets/common.dart'; -import 'package:rettulf/rettulf.dart'; - -typedef CandidateBuilder = Widget Function( - BuildContext ctx, - List matchedItems, - String query, - void Function(T item) selectIt, -); -typedef HistoryBuilder = Widget Function( - BuildContext ctx, - List items, - void Function(T item) selectIt, -); -typedef Stringifier = String Function(T item); -typedef QueryProcessor = String Function(String raw); -typedef ItemPredicate = bool Function(String query, T item); -typedef HighlightedCandidateBuilder = Widget Function( - BuildContext ctx, - List matchedItems, - (String full, TextRange highlighted) Function(T item) highlight, - void Function(T item) selectIt, -); -typedef HighlightedHistoryBuilder = Widget Function( - BuildContext ctx, - List items, - String Function(T item) stringify, - void Function(T item) selectIt, -); - -class ItemSearchDelegate extends SearchDelegate { - final ({ValueNotifier> history, HistoryBuilder builder})? searchHistory; - final List candidates; - final CandidateBuilder candidateBuilder; - final ItemPredicate predicate; - final QueryProcessor? queryProcessor; - final double maxCrossAxisExtent; - final double childAspectRatio; - final String? invalidSearchTip; - - /// If this is given, it means user can send a empty query without suggestion limitation. - /// If so, this object will be returned. - final Object? emptyIndicator; - - ItemSearchDelegate({ - required this.candidateBuilder, - required this.candidates, - required this.predicate, - this.searchHistory, - this.queryProcessor, - required this.maxCrossAxisExtent, - required this.childAspectRatio, - this.emptyIndicator, - this.invalidSearchTip, - super.keyboardType, - }); - - factory ItemSearchDelegate.highlight({ - required HighlightedCandidateBuilder candidateBuilder, - required List candidates, - required HighlightedHistoryBuilder historyBuilder, - - /// Using [String.contains] by default. - ItemPredicate? predicate, - ValueNotifier>? searchHistory, - QueryProcessor? queryProcessor, - required double maxCrossAxisExtent, - required double childAspectRatio, - Object? emptyIndicator, - String? invalidSearchTip, - TextInputType? keyboardType, - - /// Using [Object.toString] by default. - Stringifier? stringifier, - }) { - return ItemSearchDelegate( - maxCrossAxisExtent: maxCrossAxisExtent, - childAspectRatio: childAspectRatio, - queryProcessor: queryProcessor, - candidates: candidates, - invalidSearchTip: invalidSearchTip, - emptyIndicator: emptyIndicator, - searchHistory: searchHistory == null - ? null - : ( - history: searchHistory, - builder: (ctx, items, selectIt) { - return historyBuilder( - ctx, - items, - (item) => stringifier?.call(item) ?? item.toString(), - selectIt, - ); - } - ), - predicate: (query, item) { - if (query.isEmpty) return false; - final candidate = stringifier?.call(item) ?? item.toString(); - if (predicate == null) return candidate.contains(query); - return predicate(query, candidate); - }, - candidateBuilder: (ctx, items, query, selectIt) { - return candidateBuilder( - ctx, - items, - (item) { - final candidate = stringifier?.call(item) ?? item.toString(); - final highlighted = findSelected(full: candidate, selected: query); - return (candidate, highlighted); - }, - selectIt, - ); - }, - ); - } - - String getRealQuery() => queryProcessor?.call(query) ?? query; - - @override - List? buildActions(BuildContext context) { - return [ - IconButton( - icon: const Icon(Icons.clear), - onPressed: () => query = "", - ) - ]; - } - - @override - Widget? buildLeading(BuildContext context) { - return IconButton( - icon: AnimatedIcon(icon: AnimatedIcons.menu_arrow, progress: transitionAnimation), - onPressed: () => close(context, null), - ); - } - - @override - Widget buildResults(BuildContext context) { - final query = getRealQuery(); - if (query.isEmpty && emptyIndicator != null) { - return const SizedBox(); - } - if (T == String && predicate(query, query as T)) { - if (candidates.contains(query)) { - return const SizedBox(); - } else { - return LeavingBlank(icon: Icons.search_off_rounded, desc: invalidSearchTip); - } - } - return LeavingBlank(icon: Icons.search_off_rounded, desc: invalidSearchTip); - } - - @override - void showResults(BuildContext context) { - super.showResults(context); - final query = getRealQuery(); - if (query.isEmpty && emptyIndicator != null) { - close(context, emptyIndicator); - return; - } - if (T == String && predicate(query, query as T) && candidates.contains(query)) { - close(context, query); - return; - } - } - - @override - Widget buildSuggestions(BuildContext context) { - final query = getRealQuery(); - final searchHistory = this.searchHistory; - if (query.isEmpty && searchHistory != null) { - final (:history, :builder) = searchHistory; - return history >> (ctx, value) => builder(context, value, (item) => close(context, item)); - } else { - final query = getRealQuery(); - final matched = candidates.where((candidate) => predicate(query, candidate)).toList(); - return candidateBuilder(context, matched, query, (candidate) => close(context, candidate)); - } - } -} - -TextRange findSelected({ - required String full, - required String selected, -}) { - final start = full.indexOf(selected); - if (start < 0) return TextRange.empty; - return TextRange(start: start, end: start + selected.length); -} - -class HighlightedText extends StatelessWidget { - final String full; - final TextRange highlighted; - final TextStyle? baseStyle; - - const HighlightedText({ - super.key, - required this.full, - this.highlighted = TextRange.empty, - this.baseStyle, - }); - - @override - Widget build(BuildContext context) { - final baseStyle = this.baseStyle ?? const TextStyle(); - final plainStyle = baseStyle.copyWith(color: baseStyle.color?.withOpacity(0.5)); - final highlightedStyle = baseStyle.copyWith(color: context.colorScheme.primary, fontWeight: FontWeight.bold); - return RichText( - text: TextSpan( - children: !highlighted.isValid || !highlighted.isNormalized - ? [ - TextSpan(text: full, style: highlightedStyle), - ] - : [ - TextSpan(text: highlighted.textBefore(full), style: plainStyle), - TextSpan(text: highlighted.textInside(full), style: highlightedStyle), - TextSpan(text: highlighted.textAfter(full), style: plainStyle), - ], - ), - ); - } -} diff --git a/lib/widgets/webview/injectable.dart b/lib/widgets/webview/injectable.dart deleted file mode 100644 index fb3995c46..000000000 --- a/lib/widgets/webview/injectable.dart +++ /dev/null @@ -1,166 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:sit/design/widgets/common.dart'; -import 'package:universal_platform/universal_platform.dart'; -import 'package:url_launcher/url_launcher_string.dart'; -import 'package:webview_flutter/webview_flutter.dart'; - -typedef JavaScriptMessageCallback = void Function(JavaScriptMessage msg); - -class Injection { - /// js注入的url匹配规则 - final bool Function(String url)? matcher; - - /// 若为空,则表示不注入 - final String? js; - - /// 异步js字符串,若为空,则表示不注入 - final Future? asyncJs; - - const Injection({ - this.matcher, - this.js, - this.asyncJs, - }); -} - -class InjectableWebView extends StatefulWidget { - final String initialUrl; - final WebViewController? controller; - - /// JavaScript injection when page started. - final List? pageStartedInjections; - - /// JavaScript injection when page finished. - final List? pageFinishedInjections; - - /// hooks - final void Function(String url)? onPageStarted; - final void Function(String url)? onPageFinished; - final void Function(int progress)? onProgress; - - /// 注入cookies - final List? initialCookies; - - /// 自定义 UA - final String? userAgent; - - final JavaScriptMode mode; - - /// 暴露dart回调到js接口 - final Map? javaScriptChannels; - - const InjectableWebView({ - super.key, - required this.initialUrl, - this.controller, - this.mode = JavaScriptMode.unrestricted, - this.pageStartedInjections, - this.pageFinishedInjections, - this.onPageStarted, - this.onPageFinished, - this.onProgress, - this.userAgent, - this.initialCookies, - this.javaScriptChannels, - }); - - @override - State createState() => _InjectableWebViewState(); -} - -class _InjectableWebViewState extends State { - late WebViewController controller; - final cookieManager = WebViewCookieManager(); - - @override - void initState() { - super.initState(); - controller = (widget.controller ?? WebViewController()) - ..setJavaScriptMode(widget.mode) - ..setUserAgent(widget.userAgent) - ..setNavigationDelegate(NavigationDelegate( - onWebResourceError: onResourceError, - onPageStarted: (String url) async { - debugPrint('"$url" starts loading.'); - await Future.wait(widget.pageStartedInjections.matching(url).map(injectJs)); - widget.onPageStarted?.call(url); - }, - onPageFinished: (String url) async { - debugPrint('"$url" loaded.'); - await Future.wait(widget.pageFinishedInjections.matching(url).map(injectJs)); - widget.onPageFinished?.call(url); - }, - onProgress: widget.onProgress, - )); - final channels = widget.javaScriptChannels; - if (channels != null) { - for (final entry in channels.entries) { - controller.addJavaScriptChannel(entry.key, onMessageReceived: entry.value); - } - } - final cookies = widget.initialCookies; - if (cookies != null) { - for (final cookie in cookies) { - cookieManager.setCookie(cookie); - } - } - controller.loadRequest(Uri.parse(widget.initialUrl)); - } - - @override - Widget build(BuildContext context) { - if (UniversalPlatform.isDesktop) { - return LeavingBlank(icon: Icons.desktop_access_disabled_rounded); - } - return WebViewWidget( - controller: controller, - ); - } - - /// 根据当前url筛选所有符合条件的js脚本,执行js注入 - Future injectJs(Injection injection) async { - var injected = false; - // 同步获取js代码 - if (injection.js != null) { - injected = true; - await controller.runJavaScript(injection.js!); - } - // 异步获取js代码 - if (injection.asyncJs != null) { - injected = true; - String? js = await injection.asyncJs; - if (js != null) { - await controller.runJavaScript(js); - } - } - if (injected) { - debugPrint('JavaScript code was injected.'); - } - } - - void onResourceError(WebResourceError error) { - if (error.description.startsWith('http')) { - launchUrlString( - error.description, - mode: LaunchMode.externalApplication, - ); - controller.goBack(); - } - } -} - -extension _InjectionsX on List? { - /// 获取该url匹配的所有注入项 - Iterable matching(String url) sync* { - final injections = this; - if (injections != null) { - for (final injection in injections) { - if (injection.matcher?.call(url) != false) { - yield injection; - } - } - } - } -} diff --git a/lib/widgets/webview/page.dart b/lib/widgets/webview/page.dart deleted file mode 100644 index f0b092ead..000000000 --- a/lib/widgets/webview/page.dart +++ /dev/null @@ -1,197 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:sit/l10n/common.dart'; -import 'package:sit/widgets/webview/injectable.dart'; -import 'package:rettulf/rettulf.dart'; -import 'package:share_plus/share_plus.dart'; -import 'package:text_scroll/text_scroll.dart'; -import 'package:url_launcher/url_launcher_string.dart'; -import 'package:webview_flutter/webview_flutter.dart'; - -// TODO: remove this -const _kUserAgent = - "Mozilla/5.0 (Linux; Android 10; HMA-AL00 Build/HUAWEIHMA-AL00; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/83.0.4103.106 Mobile Safari/537.36"; - -// TODO: Support proxy -class WebViewPage extends StatefulWidget { - /// 初始的url - final String initialUrl; - - /// 固定的标题名?若为null则自动获取目标页面标题 - final String? fixedTitle; - - /// JavaScript injection when page started. - final List? pageStartedInjections; - - /// JavaScript injection when page finished. - final List? pageFinishedInjections; - - /// 显示分享按钮(默认不显示) - final bool showSharedButton; - - /// 显示刷新按钮(默认显示) - final bool showRefreshButton; - - /// 显示在浏览器中打开按钮(默认不显示) - final bool showOpenInBrowser; - - /// 浮动按钮控件 - final Widget? floatingActionButton; - - /// 自定义 UA - final String? userAgent; - - final WebViewController? controller; - - /// hooks - final void Function(String url)? onPageStarted; - final void Function(String url)? onPageFinished; - final void Function(int progress)? onProgress; - - final Map? javaScriptChannels; - - /// 自定义Action按钮 - final List? otherActions; - - /// 夜间模式 - final bool followDarkMode; - - /// 注入cookies - final List initialCookies; - final Widget? bottomNavigationBar; - - const WebViewPage({ - super.key, - required this.initialUrl, - this.controller, - this.fixedTitle, - this.pageStartedInjections, - this.pageFinishedInjections, - this.floatingActionButton, - this.showSharedButton = true, - this.showRefreshButton = true, - this.showOpenInBrowser = true, - this.userAgent = _kUserAgent, - this.javaScriptChannels, - this.onPageStarted, - this.onPageFinished, - this.onProgress, - this.otherActions, - this.followDarkMode = false, - this.initialCookies = const [], - this.bottomNavigationBar, - }); - - @override - State createState() => _WebViewPageState(); -} - -class _WebViewPageState extends State { - late WebViewController controller; - late String? title = Uri.tryParse(widget.initialUrl)?.authority; - int progress = 0; - - @override - void initState() { - super.initState(); - controller = widget.controller ?? WebViewController(); - } - - void _onRefresh() async { - await controller.reload(); - } - - void _onShared() async { - final url = await controller.currentUrl(); - final uri = url == null ? Uri.tryParse(widget.initialUrl) : Uri.tryParse(url); - if (uri != null) { - Share.shareUri(uri); - } - } - - PreferredSizeWidget buildTopIndicator() { - return PreferredSize( - preferredSize: const Size.fromHeight(4), - child: LinearProgressIndicator( - value: progress / 100, - ), - ); - } - - @override - Widget build(BuildContext context) { - final actions = [ - if (widget.showRefreshButton) - IconButton( - onPressed: _onRefresh, - icon: const Icon(Icons.refresh), - ), - if (widget.showSharedButton) - IconButton( - onPressed: _onShared, - icon: const Icon(Icons.share), - ), - if (widget.showOpenInBrowser) - IconButton( - onPressed: () => launchUrlString( - widget.initialUrl, - mode: LaunchMode.externalApplication, - ), - icon: const Icon(Icons.open_in_browser), - ), - ...?widget.otherActions, - ]; - final curTitle = widget.fixedTitle ?? title ?? const CommonI18n().untitled; - return WillPopScope( - onWillPop: () async { - final canGoBack = await controller.canGoBack(); - if (canGoBack) await controller.goBack(); - // 如果wv能后退就不能退出路由 - return !canGoBack; - }, - child: Scaffold( - appBar: AppBar( - title: TextScroll(curTitle), - actions: actions, - bottom: buildTopIndicator(), - ), - bottomNavigationBar: widget.bottomNavigationBar, - floatingActionButton: widget.floatingActionButton, - body: InjectableWebView( - initialUrl: widget.initialUrl, - controller: widget.controller, - onPageStarted: widget.onPageFinished, - onPageFinished: (url) async { - if (!mounted) return; - if (widget.fixedTitle == null) { - final newTitle = await controller.getTitle(); - if (newTitle != title && newTitle != null && newTitle.isNotEmpty) { - setState(() { - title = newTitle; - }); - } - } - widget.onPageFinished?.call(url); - }, - onProgress: (value) { - if (!mounted) return; - widget.onProgress?.call(value); - setState(() => progress = value % 100); - }, - pageStartedInjections: widget.pageStartedInjections, - pageFinishedInjections: [ - if (widget.followDarkMode && context.isDarkMode) - Injection( - matcher: (url) => true, - asyncJs: rootBundle.loadString('assets/webview/dark.js'), - ), - if (widget.pageFinishedInjections != null) ...widget.pageFinishedInjections!, - ], - javaScriptChannels: widget.javaScriptChannels, - userAgent: widget.userAgent, - initialCookies: widget.initialCookies, - ), - ), - ); - } -} diff --git a/linux/.gitignore b/linux/.gitignore deleted file mode 100644 index d3896c984..000000000 --- a/linux/.gitignore +++ /dev/null @@ -1 +0,0 @@ -flutter/ephemeral diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt deleted file mode 100644 index f6c413f91..000000000 --- a/linux/CMakeLists.txt +++ /dev/null @@ -1,116 +0,0 @@ -cmake_minimum_required(VERSION 3.10) -project(runner LANGUAGES CXX) - -set(BINARY_NAME "sit_life") -set(APPLICATION_ID "life.mysit.SITLife") - -cmake_policy(SET CMP0063 NEW) - -set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") - -# Root filesystem for cross-building. -if(FLUTTER_TARGET_PLATFORM_SYSROOT) - set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) - set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) - set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) - set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) - set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) - set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) -endif() - -# Configure build options. -if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) - set(CMAKE_BUILD_TYPE "Debug" CACHE - STRING "Flutter build mode" FORCE) - set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS - "Debug" "Profile" "Release") -endif() - -# Compilation settings that should be applied to most targets. -function(APPLY_STANDARD_SETTINGS TARGET) - target_compile_features(${TARGET} PUBLIC cxx_std_14) - target_compile_options(${TARGET} PRIVATE -Wall -Werror) - target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") - target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") -endfunction() - -set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") - -# Flutter library and tool build rules. -add_subdirectory(${FLUTTER_MANAGED_DIR}) - -# System-level dependencies. -find_package(PkgConfig REQUIRED) -pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) - -add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") - -# Application build -add_executable(${BINARY_NAME} - "main.cc" - "my_application.cc" - "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" -) -apply_standard_settings(${BINARY_NAME}) -target_link_libraries(${BINARY_NAME} PRIVATE flutter) -target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) -add_dependencies(${BINARY_NAME} flutter_assemble) -# Only the install-generated bundle's copy of the executable will launch -# correctly, since the resources must in the right relative locations. To avoid -# people trying to run the unbundled copy, put it in a subdirectory instead of -# the default top-level location. -set_target_properties(${BINARY_NAME} - PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" -) - -# Generated plugin build rules, which manage building the plugins and adding -# them to the application. -include(flutter/generated_plugins.cmake) - - -# === Installation === -# By default, "installing" just makes a relocatable bundle in the build -# directory. -set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") -if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) - set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) -endif() - -# Start with a clean build bundle directory every time. -install(CODE " - file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") - " COMPONENT Runtime) - -set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") -set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") - -install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" - COMPONENT Runtime) - -install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" - COMPONENT Runtime) - -install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) - -if(PLUGIN_BUNDLED_LIBRARIES) - install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" - DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) -endif() - -# Fully re-copy the assets directory on each build to avoid having stale files -# from a previous install. -set(FLUTTER_ASSET_DIR_NAME "flutter_assets") -install(CODE " - file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") - " COMPONENT Runtime) -install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" - DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) - -# Install the AOT library on non-Debug builds only. -if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") - install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) -endif() diff --git a/linux/flutter/CMakeLists.txt b/linux/flutter/CMakeLists.txt deleted file mode 100644 index 33fd5801e..000000000 --- a/linux/flutter/CMakeLists.txt +++ /dev/null @@ -1,87 +0,0 @@ -cmake_minimum_required(VERSION 3.10) - -set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") - -# Configuration provided via flutter tool. -include(${EPHEMERAL_DIR}/generated_config.cmake) - -# TODO: Move the rest of this into files in ephemeral. See -# https://github.com/flutter/flutter/issues/57146. - -# Serves the same purpose as list(TRANSFORM ... PREPEND ...), -# which isn't available in 3.10. -function(list_prepend LIST_NAME PREFIX) - set(NEW_LIST "") - foreach(element ${${LIST_NAME}}) - list(APPEND NEW_LIST "${PREFIX}${element}") - endforeach(element) - set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) -endfunction() - -# === Flutter Library === -# System-level dependencies. -find_package(PkgConfig REQUIRED) -pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) -pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) -pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) - -set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") - -# Published to parent scope for install step. -set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) -set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) -set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) -set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) - -list(APPEND FLUTTER_LIBRARY_HEADERS - "fl_basic_message_channel.h" - "fl_binary_codec.h" - "fl_binary_messenger.h" - "fl_dart_project.h" - "fl_engine.h" - "fl_json_message_codec.h" - "fl_json_method_codec.h" - "fl_message_codec.h" - "fl_method_call.h" - "fl_method_channel.h" - "fl_method_codec.h" - "fl_method_response.h" - "fl_plugin_registrar.h" - "fl_plugin_registry.h" - "fl_standard_message_codec.h" - "fl_standard_method_codec.h" - "fl_string_codec.h" - "fl_value.h" - "fl_view.h" - "flutter_linux.h" -) -list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") -add_library(flutter INTERFACE) -target_include_directories(flutter INTERFACE - "${EPHEMERAL_DIR}" -) -target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") -target_link_libraries(flutter INTERFACE - PkgConfig::GTK - PkgConfig::GLIB - PkgConfig::GIO -) -add_dependencies(flutter flutter_assemble) - -# === Flutter tool backend === -# _phony_ is a non-existent file to force this command to run every time, -# since currently there's no way to get a full input/output list from the -# flutter tool. -add_custom_command( - OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} - ${CMAKE_CURRENT_BINARY_DIR}/_phony_ - COMMAND ${CMAKE_COMMAND} -E env - ${FLUTTER_TOOL_ENVIRONMENT} - "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" - ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} - VERBATIM -) -add_custom_target(flutter_assemble DEPENDS - "${FLUTTER_LIBRARY}" - ${FLUTTER_LIBRARY_HEADERS} -) diff --git a/linux/main.cc b/linux/main.cc deleted file mode 100644 index e7c5c5437..000000000 --- a/linux/main.cc +++ /dev/null @@ -1,6 +0,0 @@ -#include "my_application.h" - -int main(int argc, char** argv) { - g_autoptr(MyApplication) app = my_application_new(); - return g_application_run(G_APPLICATION(app), argc, argv); -} diff --git a/linux/my_application.cc b/linux/my_application.cc deleted file mode 100644 index 54ad76287..000000000 --- a/linux/my_application.cc +++ /dev/null @@ -1,104 +0,0 @@ -#include "my_application.h" - -#include -#ifdef GDK_WINDOWING_X11 -#include -#endif - -#include "flutter/generated_plugin_registrant.h" - -struct _MyApplication { - GtkApplication parent_instance; - char** dart_entrypoint_arguments; -}; - -G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) - -// Implements GApplication::activate. -static void my_application_activate(GApplication* application) { - MyApplication* self = MY_APPLICATION(application); - GtkWindow* window = - GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); - - // Use a header bar when running in GNOME as this is the common style used - // by applications and is the setup most users will be using (e.g. Ubuntu - // desktop). - // If running on X and not using GNOME then just use a traditional title bar - // in case the window manager does more exotic layout, e.g. tiling. - // If running on Wayland assume the header bar will work (may need changing - // if future cases occur). - gboolean use_header_bar = TRUE; -#ifdef GDK_WINDOWING_X11 - GdkScreen* screen = gtk_window_get_screen(window); - if (GDK_IS_X11_SCREEN(screen)) { - const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); - if (g_strcmp0(wm_name, "GNOME Shell") != 0) { - use_header_bar = FALSE; - } - } -#endif - if (use_header_bar) { - GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); - gtk_widget_show(GTK_WIDGET(header_bar)); - gtk_header_bar_set_title(header_bar, "SIT Life"); - gtk_header_bar_set_show_close_button(header_bar, TRUE); - gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); - } else { - gtk_window_set_title(window, "SIT Life"); - } - - gtk_window_set_default_size(window, 1280, 720); - gtk_widget_show(GTK_WIDGET(window)); - - g_autoptr(FlDartProject) project = fl_dart_project_new(); - fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); - - FlView* view = fl_view_new(project); - gtk_widget_show(GTK_WIDGET(view)); - gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); - - fl_register_plugins(FL_PLUGIN_REGISTRY(view)); - - gtk_widget_grab_focus(GTK_WIDGET(view)); -} - -// Implements GApplication::local_command_line. -static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { - MyApplication* self = MY_APPLICATION(application); - // Strip out the first argument as it is the binary name. - self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); - - g_autoptr(GError) error = nullptr; - if (!g_application_register(application, nullptr, &error)) { - g_warning("Failed to register: %s", error->message); - *exit_status = 1; - return TRUE; - } - - g_application_activate(application); - *exit_status = 0; - - return TRUE; -} - -// Implements GObject::dispose. -static void my_application_dispose(GObject* object) { - MyApplication* self = MY_APPLICATION(object); - g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); - G_OBJECT_CLASS(my_application_parent_class)->dispose(object); -} - -static void my_application_class_init(MyApplicationClass* klass) { - G_APPLICATION_CLASS(klass)->activate = my_application_activate; - G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; - G_OBJECT_CLASS(klass)->dispose = my_application_dispose; -} - -static void my_application_init(MyApplication* self) {} - -MyApplication* my_application_new() { - return MY_APPLICATION(g_object_new(my_application_get_type(), - "application-id", APPLICATION_ID, - "flags", G_APPLICATION_NON_UNIQUE, - nullptr)); -} diff --git a/linux/my_application.h b/linux/my_application.h deleted file mode 100644 index 72271d5e4..000000000 --- a/linux/my_application.h +++ /dev/null @@ -1,18 +0,0 @@ -#ifndef FLUTTER_MY_APPLICATION_H_ -#define FLUTTER_MY_APPLICATION_H_ - -#include - -G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, - GtkApplication) - -/** - * my_application_new: - * - * Creates a new Flutter-based application. - * - * Returns: a new #MyApplication. - */ -MyApplication* my_application_new(); - -#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/macos/.gitignore b/macos/.gitignore deleted file mode 100644 index 746adbb6b..000000000 --- a/macos/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -# Flutter-related -**/Flutter/ephemeral/ -**/Pods/ - -# Xcode-related -**/dgph -**/xcuserdata/ diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig deleted file mode 100644 index 4b81f9b2d..000000000 --- a/macos/Flutter/Flutter-Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig deleted file mode 100644 index 5caa9d157..000000000 --- a/macos/Flutter/Flutter-Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Podfile b/macos/Podfile deleted file mode 100644 index e20d34c61..000000000 --- a/macos/Podfile +++ /dev/null @@ -1,43 +0,0 @@ -platform :osx, '10.14' - -# CocoaPods analytics sends network stats synchronously affecting flutter build latency. -ENV['COCOAPODS_DISABLE_STATS'] = 'true' - -project 'Runner', { - 'Debug' => :debug, - 'Profile' => :release, - 'Release' => :release, -} - -def flutter_root - generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) - unless File.exist?(generated_xcode_build_settings_path) - raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" - end - - File.foreach(generated_xcode_build_settings_path) do |line| - matches = line.match(/FLUTTER_ROOT\=(.*)/) - return matches[1].strip if matches - end - raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" -end - -require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) - -flutter_macos_podfile_setup - -target 'Runner' do - use_frameworks! - use_modular_headers! - - flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) -end - -post_install do |installer| - installer.pods_project.targets.each do |target| - flutter_additional_macos_build_settings(target) - target.build_configurations.each do |config| - config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = '10.13' - end - end -end diff --git a/macos/Podfile.lock b/macos/Podfile.lock deleted file mode 100644 index e506442c6..000000000 --- a/macos/Podfile.lock +++ /dev/null @@ -1,146 +0,0 @@ -PODS: - - app_links (1.0.0): - - FlutterMacOS - - audio_session (0.0.1): - - FlutterMacOS - - connectivity_plus (0.0.1): - - FlutterMacOS - - ReachabilitySwift - - device_info_plus (0.0.1): - - FlutterMacOS - - dynamic_color (0.0.2): - - FlutterMacOS - - file_selector_macos (0.0.1): - - FlutterMacOS - - FlutterMacOS (1.0.0) - - FMDB (2.7.5): - - FMDB/standard (= 2.7.5) - - FMDB/standard (2.7.5) - - just_audio (0.0.1): - - FlutterMacOS - - mobile_scanner (3.5.5): - - FlutterMacOS - - package_info_plus (0.0.1): - - FlutterMacOS - - path_provider_foundation (0.0.1): - - Flutter - - FlutterMacOS - - ReachabilitySwift (5.0.0) - - screen_retriever (0.0.1): - - FlutterMacOS - - share_plus (0.0.1): - - FlutterMacOS - - shared_preferences_foundation (0.0.1): - - Flutter - - FlutterMacOS - - sqflite (0.0.2): - - FlutterMacOS - - FMDB (>= 2.7.5) - - system_theme (0.0.1): - - FlutterMacOS - - url_launcher_macos (0.0.1): - - FlutterMacOS - - video_player_avfoundation (0.0.1): - - Flutter - - FlutterMacOS - - wakelock_plus (0.0.1): - - FlutterMacOS - - window_manager (0.2.0): - - FlutterMacOS - -DEPENDENCIES: - - app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`) - - audio_session (from `Flutter/ephemeral/.symlinks/plugins/audio_session/macos`) - - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`) - - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - - dynamic_color (from `Flutter/ephemeral/.symlinks/plugins/dynamic_color/macos`) - - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) - - FlutterMacOS (from `Flutter/ephemeral`) - - just_audio (from `Flutter/ephemeral/.symlinks/plugins/just_audio/macos`) - - mobile_scanner (from `Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos`) - - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) - - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - - screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`) - - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) - - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/macos`) - - system_theme (from `Flutter/ephemeral/.symlinks/plugins/system_theme/macos`) - - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - - video_player_avfoundation (from `Flutter/ephemeral/.symlinks/plugins/video_player_avfoundation/darwin`) - - wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`) - - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) - -SPEC REPOS: - trunk: - - FMDB - - ReachabilitySwift - -EXTERNAL SOURCES: - app_links: - :path: Flutter/ephemeral/.symlinks/plugins/app_links/macos - audio_session: - :path: Flutter/ephemeral/.symlinks/plugins/audio_session/macos - connectivity_plus: - :path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos - device_info_plus: - :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos - dynamic_color: - :path: Flutter/ephemeral/.symlinks/plugins/dynamic_color/macos - file_selector_macos: - :path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos - FlutterMacOS: - :path: Flutter/ephemeral - just_audio: - :path: Flutter/ephemeral/.symlinks/plugins/just_audio/macos - mobile_scanner: - :path: Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos - package_info_plus: - :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos - path_provider_foundation: - :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin - screen_retriever: - :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos - share_plus: - :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos - shared_preferences_foundation: - :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin - sqflite: - :path: Flutter/ephemeral/.symlinks/plugins/sqflite/macos - system_theme: - :path: Flutter/ephemeral/.symlinks/plugins/system_theme/macos - url_launcher_macos: - :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos - video_player_avfoundation: - :path: Flutter/ephemeral/.symlinks/plugins/video_player_avfoundation/darwin - wakelock_plus: - :path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos - window_manager: - :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos - -SPEC CHECKSUMS: - app_links: 4481ed4d71f384b0c3ae5016f4633aa73d32ff67 - audio_session: dea1f41890dbf1718f04a56f1d6150fd50039b72 - connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 - device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f - dynamic_color: 2eaa27267de1ca20d879fbd6e01259773fb1670f - file_selector_macos: 468fb6b81fac7c0e88d71317f3eec34c3b008ff9 - FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a - just_audio: 9b67ca7b97c61cfc9784ea23cd8cc55eb226d489 - mobile_scanner: d12930b68bf502497f78b8b5182aeccfaa1e04f6 - package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce - path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 - ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 - screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 - share_plus: 76dd39142738f7a68dd57b05093b5e8193f220f7 - shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 - sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea - system_theme: c7b9f6659a5caa26c9bc2284da096781e9a6fcbc - url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 - video_player_avfoundation: e9e6f9cae7d7a6d9b43519b0aab382bca60fcfd1 - wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269 - window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 - -PODFILE CHECKSUM: b0cc1fdf1eda0fefb5163971bbf18550427d02c4 - -COCOAPODS: 1.12.1 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index eb1ff251e..000000000 --- a/macos/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,683 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 54; - objects = { - -/* Begin PBXAggregateTarget section */ - 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { - isa = PBXAggregateTarget; - buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; - buildPhases = ( - 33CC111E2044C6BF0003C045 /* ShellScript */, - ); - dependencies = ( - ); - name = "Flutter Assemble"; - productName = FLX; - }; -/* End PBXAggregateTarget section */ - -/* Begin PBXBuildFile section */ - 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; - 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; - 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; - 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; - 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; - BBF5F89762A48F4B78F75560 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 31BC25393ADB1CDF5F664AD1 /* Pods_Runner.framework */; }; - EAB651C928EFC30000296F90 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = EAB651CB28EFC30000296F90 /* InfoPlist.strings */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 33CC10E52044A3C60003C045 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 33CC111A2044C6BA0003C045; - remoteInfo = FLX; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 33CC110E2044A8840003C045 /* Bundle Framework */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Bundle Framework"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 0E9153046B361D34CB645F76 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - 31BC25393ADB1CDF5F664AD1 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; - 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 3360E23D23C18C7FCAAC779B /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* Mimir.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Mimir.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; - 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; - 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; - 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; - 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; - 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; - 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; - 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; - 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; - 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; - AABBD71A10CD22ABCF03ED29 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - EAB651CA28EFC30000296F90 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; - EAB651CC28EFC30A00296F90 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/MainMenu.strings"; sourceTree = ""; }; - EAB651CD28EFC30B00296F90 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = ""; }; - EAB651CE28EFC30F00296F90 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/MainMenu.strings"; sourceTree = ""; }; - EAB651CF28EFC30F00296F90 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/InfoPlist.strings"; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 33CC10EA2044A3C60003C045 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - BBF5F89762A48F4B78F75560 /* Pods_Runner.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 19EFB498F54B3878F1ED2C62 /* Pods */ = { - isa = PBXGroup; - children = ( - 3360E23D23C18C7FCAAC779B /* Pods-Runner.debug.xcconfig */, - 0E9153046B361D34CB645F76 /* Pods-Runner.release.xcconfig */, - AABBD71A10CD22ABCF03ED29 /* Pods-Runner.profile.xcconfig */, - ); - path = Pods; - sourceTree = ""; - }; - 33BA886A226E78AF003329D5 /* Configs */ = { - isa = PBXGroup; - children = ( - 33E5194F232828860026EE4D /* AppInfo.xcconfig */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, - ); - path = Configs; - sourceTree = ""; - }; - 33CC10E42044A3C60003C045 = { - isa = PBXGroup; - children = ( - 33FAB671232836740065AC1E /* Runner */, - 33CEB47122A05771004F2AC0 /* Flutter */, - 33CC10EE2044A3C60003C045 /* Products */, - D73912EC22F37F3D000D13A0 /* Frameworks */, - 19EFB498F54B3878F1ED2C62 /* Pods */, - ); - sourceTree = ""; - }; - 33CC10EE2044A3C60003C045 /* Products */ = { - isa = PBXGroup; - children = ( - 33CC10ED2044A3C60003C045 /* Mimir.app */, - ); - name = Products; - sourceTree = ""; - }; - 33CC11242044D66E0003C045 /* Resources */ = { - isa = PBXGroup; - children = ( - 33CC10F22044A3C60003C045 /* Assets.xcassets */, - 33CC10F42044A3C60003C045 /* MainMenu.xib */, - 33CC10F72044A3C60003C045 /* Info.plist */, - EAB651CB28EFC30000296F90 /* InfoPlist.strings */, - ); - name = Resources; - path = ..; - sourceTree = ""; - }; - 33CEB47122A05771004F2AC0 /* Flutter */ = { - isa = PBXGroup; - children = ( - 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, - 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, - 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, - 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, - ); - path = Flutter; - sourceTree = ""; - }; - 33FAB671232836740065AC1E /* Runner */ = { - isa = PBXGroup; - children = ( - 33CC10F02044A3C60003C045 /* AppDelegate.swift */, - 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, - 33E51913231747F40026EE4D /* DebugProfile.entitlements */, - 33E51914231749380026EE4D /* Release.entitlements */, - 33CC11242044D66E0003C045 /* Resources */, - 33BA886A226E78AF003329D5 /* Configs */, - ); - path = Runner; - sourceTree = ""; - }; - D73912EC22F37F3D000D13A0 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 31BC25393ADB1CDF5F664AD1 /* Pods_Runner.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 33CC10EC2044A3C60003C045 /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - A287D1D2D5940EB3BD59DF04 /* [CP] Check Pods Manifest.lock */, - 33CC10E92044A3C60003C045 /* Sources */, - 33CC10EA2044A3C60003C045 /* Frameworks */, - 33CC10EB2044A3C60003C045 /* Resources */, - 33CC110E2044A8840003C045 /* Bundle Framework */, - 3399D490228B24CF009A79C7 /* ShellScript */, - 5931A97FA1938FAA0E9F84A9 /* [CP] Embed Pods Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - 33CC11202044C79F0003C045 /* PBXTargetDependency */, - ); - name = Runner; - productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* Mimir.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 33CC10E52044A3C60003C045 /* Project object */ = { - isa = PBXProject; - attributes = { - LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1430; - ORGANIZATIONNAME = ""; - TargetAttributes = { - 33CC10EC2044A3C60003C045 = { - CreatedOnToolsVersion = 9.2; - LastSwiftMigration = 1100; - SystemCapabilities = { - com.apple.Sandbox = { - enabled = 1; - }; - }; - }; - 33CC111A2044C6BA0003C045 = { - CreatedOnToolsVersion = 9.2; - ProvisioningStyle = Manual; - }; - }; - }; - buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - "zh-Hans", - "zh-Hant", - ); - mainGroup = 33CC10E42044A3C60003C045; - productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 33CC10EC2044A3C60003C045 /* Runner */, - 33CC111A2044C6BA0003C045 /* Flutter Assemble */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 33CC10EB2044A3C60003C045 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, - EAB651C928EFC30000296F90 /* InfoPlist.strings in Resources */, - 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3399D490228B24CF009A79C7 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; - }; - 33CC111E2044C6BF0003C045 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - Flutter/ephemeral/FlutterInputs.xcfilelist, - ); - inputPaths = ( - Flutter/ephemeral/tripwire, - ); - outputFileListPaths = ( - Flutter/ephemeral/FlutterOutputs.xcfilelist, - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; - }; - 5931A97FA1938FAA0E9F84A9 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - A287D1D2D5940EB3BD59DF04 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 33CC10E92044A3C60003C045 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, - 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, - 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; - targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { - isa = PBXVariantGroup; - children = ( - 33CC10F52044A3C60003C045 /* Base */, - EAB651CC28EFC30A00296F90 /* zh-Hans */, - EAB651CE28EFC30F00296F90 /* zh-Hant */, - ); - name = MainMenu.xib; - path = Runner; - sourceTree = ""; - }; - EAB651CB28EFC30000296F90 /* InfoPlist.strings */ = { - isa = PBXVariantGroup; - children = ( - EAB651CA28EFC30000296F90 /* en */, - EAB651CD28EFC30B00296F90 /* zh-Hans */, - EAB651CF28EFC30F00296F90 /* zh-Hant */, - ); - name = InfoPlist.strings; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 338D0CE9231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - 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_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - 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_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - }; - name = Profile; - }; - 338D0CEA231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 31; - DEVELOPMENT_TEAM = M5APZD5CKA; - INFOPLIST_FILE = Runner/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = "小应生活"; - INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = life.mysit.SITLife; - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; - }; - name = Profile; - }; - 338D0CEB231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Manual; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Profile; - }; - 33CC10F92044A3C60003C045 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - 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_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - 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_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - 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_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = macosx; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - 33CC10FA2044A3C60003C045 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - 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_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - 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_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - }; - name = Release; - }; - 33CC10FC2044A3C60003C045 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 31; - DEVELOPMENT_TEAM = M5APZD5CKA; - INFOPLIST_FILE = Runner/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = "小应生活"; - INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = life.mysit.SITLife; - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - }; - name = Debug; - }; - 33CC10FD2044A3C60003C045 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 31; - DEVELOPMENT_TEAM = M5APZD5CKA; - INFOPLIST_FILE = Runner/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = "小应生活"; - INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = life.mysit.SITLife; - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; - }; - name = Release; - }; - 33CC111C2044C6BA0003C045 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Manual; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Debug; - }; - 33CC111D2044C6BA0003C045 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC10F92044A3C60003C045 /* Debug */, - 33CC10FA2044A3C60003C045 /* Release */, - 338D0CE9231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC10FC2044A3C60003C045 /* Debug */, - 33CC10FD2044A3C60003C045 /* Release */, - 338D0CEA231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC111C2044C6BA0003C045 /* Debug */, - 33CC111D2044C6BA0003C045 /* Release */, - 338D0CEB231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 33CC10E52044A3C60003C045 /* Project object */; -} diff --git a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d981003..000000000 --- a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index 7e90a5532..000000000 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/macos/Runner.xcworkspace/contents.xcworkspacedata b/macos/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 21a3cc14c..000000000 --- a/macos/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d981003..000000000 --- a/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift deleted file mode 100644 index d53ef6437..000000000 --- a/macos/Runner/AppDelegate.swift +++ /dev/null @@ -1,9 +0,0 @@ -import Cocoa -import FlutterMacOS - -@NSApplicationMain -class AppDelegate: FlutterAppDelegate { - override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { - return true - } -} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/1024x1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/1024x1024.png deleted file mode 100644 index 67c2ee91e24b0189017a8c136dfa60857dd00f59..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30646 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 diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/16x16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/16x16.png deleted file mode 100644 index a1bc240359f0ccfa5aed22247f819e8757e4f3ff..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 465 zcmV;?0WSWDP)x=*~&DE5zz0EylW&t8BT9{cEY4=?&{6v>ec{ z>_J{e=J^R)!$i|c*IAJvuth-FzLJb!J|5(?0u$-P9^T3*6l+~)<@2~42goI<23_9H zP~CWQytI|y+PnPz-#!Dbf@Z=Tc;~02@AJsL=sdrBdc$2^+?DuhcVPoimJf}4KVek^ z6|=$PoT@Q^2~jR#CK0f?w*k&yRfI!s9w&n%2oHI9yc(axY}Ai4O+jqJX9L(KA{G@a zL_^RU>|{bAnKej~i9xt=^-zLLs+f!TodM~`JanOs#i>!O9UnojHz8D95Of#ZWrEn4 z7faI-B!&Zyub_&GO#4+kV3YQ*RHq9IqV~|J{%HP-J_Hy561t>Vry8@500000NkvXX Hu0mjfHv7mH diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/256x256 1.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/256x256 1.png deleted file mode 100644 index be63d63975d65610ea35faaafa3c492b82a97f0a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6952 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 DmCFvp diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/256x256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/256x256.png deleted file mode 100644 index be63d63975d65610ea35faaafa3c492b82a97f0a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6952 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 DmCFvp diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/32x32 1.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/32x32 1.png deleted file mode 100644 index 166eb676da35990f604f6b8cea0786ca91e459b4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 934 zcmV;X16lluP)) zV1Po53m!G@aDj1AcRu3Q=*Aes4{&9C>{#gzcYujY!@>;&6F?JXr7e;e6UrmC((-6& z`kI#MT<=UN?MPd=McC-c+?+FK=HBz&@7%ff4Deq{2q9#Uwh7u+$nhnzH`a}#1|KCS zCo&LArQk1EM_iAmm_bycok{M0_)M<8nI$80ORQb*<>P9KFx+KbD@(DMqNf)^tX;3? zjoKn-JL@Pz7tZ68F9NsA&f0Y_Z*WB^m??fxEZgY}Hc|qzM1llD-!0DFUX${g11A-H zPTmNEuJ*u~9F;#AC@C|62;TVv*v!Nc6Vni~B19R8jDlXANXN<0pc!f_dhb4+v-=zt z4BzQ1JHDn;@Mt;a}!f>8uE=y;f zEoe~9$yx+%UTUo#z)mcUxhcP5KfLWKnajf@v?IV5OrW>j%$|bz?_0Sy=)|fGKJ@Af z9*ubwn;C%2577d=O{a4EA~A%b>H*`sA|}51RlbGQC=!B*W{XZ~%m9cCK*xdS)tw)_ zpU3oKQ005OwuX_9OIQvT7VUz62dDvhJIvL61*qj9Q3edgqthQ3vir6t9EH@#p{L7I z-6AA4L`gwsvynajaRKX68Zc1?&Y7zN;N7AW))CknwJQIBgGVQ?26dU|vC^4@({9Mk zej}>%ZPwHLn{?Fy2ns2>4kS363@ZP#frC4nv(bd7*Ln){N5hcxSqeyBk*Tt4Aea;o zOebMCYvHuAr@${HP-h_McbKc&h+h9#rcVjn9fgxFm`Wi;8wj_5@F=#t8_EYzHn%eh zqKGuz#_jbDxOk?C9e_6+!Q`(1x~wMLa<(bHDSAJ=xR?tpYUn{j$=j__G$+ulH%aMC z5^4jjT@7e)>S5Ds(QY!Z0g#J~$DXg~w^{Jq<$|#;_sY1>cPQ;zod#{)4X`=tX@4u+ z?Pr-P;TgGBnhg9Tp71s~VTqJtz#|tVZb!>$RQ|K2Dk8ZgTREj)k1MnwnpJ&<)Uis{ z%TowGmJj~d{tXyp^gwM`285iBC)HHSWkhCoyyDFNS@|u%0R28}O2a;XYXATM07*qo IM6N<$f=({2YXATM diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/32x32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/32x32.png deleted file mode 100644 index 166eb676da35990f604f6b8cea0786ca91e459b4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 934 zcmV;X16lluP)) zV1Po53m!G@aDj1AcRu3Q=*Aes4{&9C>{#gzcYujY!@>;&6F?JXr7e;e6UrmC((-6& z`kI#MT<=UN?MPd=McC-c+?+FK=HBz&@7%ff4Deq{2q9#Uwh7u+$nhnzH`a}#1|KCS zCo&LArQk1EM_iAmm_bycok{M0_)M<8nI$80ORQb*<>P9KFx+KbD@(DMqNf)^tX;3? zjoKn-JL@Pz7tZ68F9NsA&f0Y_Z*WB^m??fxEZgY}Hc|qzM1llD-!0DFUX${g11A-H zPTmNEuJ*u~9F;#AC@C|62;TVv*v!Nc6Vni~B19R8jDlXANXN<0pc!f_dhb4+v-=zt z4BzQ1JHDn;@Mt;a}!f>8uE=y;f zEoe~9$yx+%UTUo#z)mcUxhcP5KfLWKnajf@v?IV5OrW>j%$|bz?_0Sy=)|fGKJ@Af z9*ubwn;C%2577d=O{a4EA~A%b>H*`sA|}51RlbGQC=!B*W{XZ~%m9cCK*xdS)tw)_ zpU3oKQ005OwuX_9OIQvT7VUz62dDvhJIvL61*qj9Q3edgqthQ3vir6t9EH@#p{L7I z-6AA4L`gwsvynajaRKX68Zc1?&Y7zN;N7AW))CknwJQIBgGVQ?26dU|vC^4@({9Mk zej}>%ZPwHLn{?Fy2ns2>4kS363@ZP#frC4nv(bd7*Ln){N5hcxSqeyBk*Tt4Aea;o zOebMCYvHuAr@${HP-h_McbKc&h+h9#rcVjn9fgxFm`Wi;8wj_5@F=#t8_EYzHn%eh zqKGuz#_jbDxOk?C9e_6+!Q`(1x~wMLa<(bHDSAJ=xR?tpYUn{j$=j__G$+ulH%aMC z5^4jjT@7e)>S5Ds(QY!Z0g#J~$DXg~w^{Jq<$|#;_sY1>cPQ;zod#{)4X`=tX@4u+ z?Pr-P;TgGBnhg9Tp71s~VTqJtz#|tVZb!>$RQ|K2Dk8ZgTREj)k1MnwnpJ&<)Uis{ z%TowGmJj~d{tXyp^gwM`285iBC)HHSWkhCoyyDFNS@|u%0R28}O2a;XYXATM07*qo IM6N<$f=({2YXATM diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/512x512 1.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/512x512 1.png deleted file mode 100644 index 24b0b96ec09867bd70cff1a9080277082964625d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14319 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 diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 0c7d3fde1..000000000 --- a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "images" : [ - { - "filename" : "16x16.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "16x16" - }, - { - "filename" : "32x32.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "16x16" - }, - { - "filename" : "32x32 1.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "32x32" - }, - { - "filename" : "64x64.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "32x32" - }, - { - "filename" : "128x128.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "128x128" - }, - { - "filename" : "256x256 1.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "128x128" - }, - { - "filename" : "256x256.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "256x256" - }, - { - "filename" : "512x512 1.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "256x256" - }, - { - "filename" : "512x512.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "512x512" - }, - { - "filename" : "1024x1024.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "512x512" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/macos/Runner/Assets.xcassets/Contents.json b/macos/Runner/Assets.xcassets/Contents.json deleted file mode 100644 index 73c00596a..000000000 --- a/macos/Runner/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/macos/Runner/Base.lproj/MainMenu.xib b/macos/Runner/Base.lproj/MainMenu.xib deleted file mode 100644 index cab0afdb8..000000000 --- a/macos/Runner/Base.lproj/MainMenu.xib +++ /dev/null @@ -1,344 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig deleted file mode 100644 index 8356c8ce0..000000000 --- a/macos/Runner/Configs/AppInfo.xcconfig +++ /dev/null @@ -1,14 +0,0 @@ -// Application-level settings for the Runner target. -// -// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the -// future. If not, the values below would default to using the project name when this becomes a -// 'flutter create' template. - -// The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = Mimir - -// The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = net.liplum.Mimir - -// The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright (C) 2023 Liplum diff --git a/macos/Runner/Configs/Debug.xcconfig b/macos/Runner/Configs/Debug.xcconfig deleted file mode 100644 index 36b0fd946..000000000 --- a/macos/Runner/Configs/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Debug.xcconfig" -#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Release.xcconfig b/macos/Runner/Configs/Release.xcconfig deleted file mode 100644 index dff4f4956..000000000 --- a/macos/Runner/Configs/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Release.xcconfig" -#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Warnings.xcconfig b/macos/Runner/Configs/Warnings.xcconfig deleted file mode 100644 index 42bcbf478..000000000 --- a/macos/Runner/Configs/Warnings.xcconfig +++ /dev/null @@ -1,13 +0,0 @@ -WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings -GCC_WARN_UNDECLARED_SELECTOR = YES -CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES -CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE -CLANG_WARN__DUPLICATE_METHOD_MATCH = YES -CLANG_WARN_PRAGMA_PACK = YES -CLANG_WARN_STRICT_PROTOTYPES = YES -CLANG_WARN_COMMA = YES -GCC_WARN_STRICT_SELECTOR_MATCH = YES -CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES -CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES -GCC_WARN_SHADOW = YES -CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements deleted file mode 100644 index d3d522562..000000000 --- a/macos/Runner/DebugProfile.entitlements +++ /dev/null @@ -1,18 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.cs.allow-jit - - com.apple.security.device.camera - - com.apple.security.files.user-selected.read-only - - com.apple.security.network.client - - com.apple.security.network.server - - - diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist deleted file mode 100644 index 77c73fd09..000000000 --- a/macos/Runner/Info.plist +++ /dev/null @@ -1,47 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIconFile - - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSHasLocalizedDisplayName - - LSMinimumSystemVersion - $(MACOSX_DEPLOYMENT_TARGET) - NSHumanReadableCopyright - $(PRODUCT_COPYRIGHT) - NSMainNibFile - MainMenu - NSPrincipalClass - NSApplication - UILaunchStoryboardName - - CFBundleURLTypes - - - CFBundleURLName - SIT Life URL - CFBundleURLSchemes - - life.mysit - - - - - diff --git a/macos/Runner/MainFlutterWindow.swift b/macos/Runner/MainFlutterWindow.swift deleted file mode 100644 index 2722837ec..000000000 --- a/macos/Runner/MainFlutterWindow.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Cocoa -import FlutterMacOS - -class MainFlutterWindow: NSWindow { - override func awakeFromNib() { - let flutterViewController = FlutterViewController.init() - let windowFrame = self.frame - self.contentViewController = flutterViewController - self.setFrame(windowFrame, display: true) - - RegisterGeneratedPlugins(registry: flutterViewController) - - super.awakeFromNib() - } -} diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements deleted file mode 100644 index b657be125..000000000 --- a/macos/Runner/Release.entitlements +++ /dev/null @@ -1,16 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.device.camera - - com.apple.security.files.user-selected.read-only - - com.apple.security.network.client - - com.apple.security.network.server - - - diff --git a/macos/Runner/zh-Hans.lproj/MainMenu.strings b/macos/Runner/zh-Hans.lproj/MainMenu.strings deleted file mode 100644 index b74fd928e..000000000 --- a/macos/Runner/zh-Hans.lproj/MainMenu.strings +++ /dev/null @@ -1,195 +0,0 @@ - -/* Class = "NSMenuItem"; title = "APP_NAME"; ObjectID = "1Xt-HY-uBw"; */ -"1Xt-HY-uBw.title" = "APP_NAME"; - -/* Class = "NSMenu"; title = "Find"; ObjectID = "1b7-l0-nxx"; */ -"1b7-l0-nxx.title" = "Find"; - -/* Class = "NSMenuItem"; title = "Transformations"; ObjectID = "2oI-Rn-ZJC"; */ -"2oI-Rn-ZJC.title" = "Transformations"; - -/* Class = "NSMenu"; title = "Spelling"; ObjectID = "3IN-sU-3Bg"; */ -"3IN-sU-3Bg.title" = "Spelling"; - -/* Class = "NSMenu"; title = "Speech"; ObjectID = "3rS-ZA-NoH"; */ -"3rS-ZA-NoH.title" = "Speech"; - -/* Class = "NSMenuItem"; title = "Find"; ObjectID = "4EN-yA-p0u"; */ -"4EN-yA-p0u.title" = "Find"; - -/* Class = "NSMenuItem"; title = "Enter Full Screen"; ObjectID = "4J7-dP-txa"; */ -"4J7-dP-txa.title" = "Enter Full Screen"; - -/* Class = "NSMenuItem"; title = "Quit APP_NAME"; ObjectID = "4sb-4s-VLi"; */ -"4sb-4s-VLi.title" = "Quit APP_NAME"; - -/* Class = "NSMenuItem"; title = "Edit"; ObjectID = "5QF-Oa-p0T"; */ -"5QF-Oa-p0T.title" = "Edit"; - -/* Class = "NSMenuItem"; title = "About APP_NAME"; ObjectID = "5kV-Vb-QxS"; */ -"5kV-Vb-QxS.title" = "About APP_NAME"; - -/* Class = "NSMenuItem"; title = "Redo"; ObjectID = "6dh-zS-Vam"; */ -"6dh-zS-Vam.title" = "Redo"; - -/* Class = "NSMenuItem"; title = "Correct Spelling Automatically"; ObjectID = "78Y-hA-62v"; */ -"78Y-hA-62v.title" = "Correct Spelling Automatically"; - -/* Class = "NSMenuItem"; title = "Substitutions"; ObjectID = "9ic-FL-obx"; */ -"9ic-FL-obx.title" = "Substitutions"; - -/* Class = "NSMenuItem"; title = "Smart Copy/Paste"; ObjectID = "9yt-4B-nSM"; */ -"9yt-4B-nSM.title" = "Smart Copy/Paste"; - -/* Class = "NSMenu"; title = "Main Menu"; ObjectID = "AYu-sK-qS6"; */ -"AYu-sK-qS6.title" = "Main Menu"; - -/* Class = "NSMenuItem"; title = "Preferences…"; ObjectID = "BOF-NM-1cW"; */ -"BOF-NM-1cW.title" = "Preferences…"; - -/* Class = "NSMenuItem"; title = "Spelling and Grammar"; ObjectID = "Dv1-io-Yv7"; */ -"Dv1-io-Yv7.title" = "Spelling and Grammar"; - -/* Class = "NSMenuItem"; title = "Help"; ObjectID = "EPT-qC-fAb"; */ -"EPT-qC-fAb.title" = "Help"; - -/* Class = "NSMenu"; title = "Substitutions"; ObjectID = "FeM-D8-WVr"; */ -"FeM-D8-WVr.title" = "Substitutions"; - -/* Class = "NSMenuItem"; title = "View"; ObjectID = "H8h-7b-M4v"; */ -"H8h-7b-M4v.title" = "View"; - -/* Class = "NSMenuItem"; title = "Text Replacement"; ObjectID = "HFQ-gK-NFA"; */ -"HFQ-gK-NFA.title" = "Text Replacement"; - -/* Class = "NSMenuItem"; title = "Show Spelling and Grammar"; ObjectID = "HFo-cy-zxI"; */ -"HFo-cy-zxI.title" = "Show Spelling and Grammar"; - -/* Class = "NSMenu"; title = "View"; ObjectID = "HyV-fh-RgO"; */ -"HyV-fh-RgO.title" = "View"; - -/* Class = "NSMenuItem"; title = "Show All"; ObjectID = "Kd2-mp-pUS"; */ -"Kd2-mp-pUS.title" = "Show All"; - -/* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "LE2-aR-0XJ"; */ -"LE2-aR-0XJ.title" = "Bring All to Front"; - -/* Class = "NSMenuItem"; title = "Services"; ObjectID = "NMo-om-nkz"; */ -"NMo-om-nkz.title" = "Services"; - -/* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "OY7-WF-poV"; */ -"OY7-WF-poV.title" = "Minimize"; - -/* Class = "NSMenuItem"; title = "Hide APP_NAME"; ObjectID = "Olw-nP-bQN"; */ -"Olw-nP-bQN.title" = "Hide APP_NAME"; - -/* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "OwM-mh-QMV"; */ -"OwM-mh-QMV.title" = "Find Previous"; - -/* Class = "NSMenuItem"; title = "Stop Speaking"; ObjectID = "Oyz-dy-DGm"; */ -"Oyz-dy-DGm.title" = "Stop Speaking"; - -/* Class = "NSWindow"; title = "APP_NAME"; ObjectID = "QvC-M9-y7g"; */ -"QvC-M9-y7g.title" = "APP_NAME"; - -/* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "R4o-n2-Eq4"; */ -"R4o-n2-Eq4.title" = "Zoom"; - -/* Class = "NSMenuItem"; title = "Select All"; ObjectID = "Ruw-6m-B2m"; */ -"Ruw-6m-B2m.title" = "Select All"; - -/* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "S0p-oC-mLd"; */ -"S0p-oC-mLd.title" = "Jump to Selection"; - -/* Class = "NSMenu"; title = "Window"; ObjectID = "Td7-aD-5lo"; */ -"Td7-aD-5lo.title" = "Window"; - -/* Class = "NSMenuItem"; title = "Capitalize"; ObjectID = "UEZ-Bs-lqG"; */ -"UEZ-Bs-lqG.title" = "Capitalize"; - -/* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "Vdr-fp-XzO"; */ -"Vdr-fp-XzO.title" = "Hide Others"; - -/* Class = "NSMenu"; title = "Edit"; ObjectID = "W48-6f-4Dl"; */ -"W48-6f-4Dl.title" = "Edit"; - -/* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "WeT-3V-zwk"; */ -"WeT-3V-zwk.title" = "Paste and Match Style"; - -/* Class = "NSMenuItem"; title = "Find…"; ObjectID = "Xz5-n4-O0W"; */ -"Xz5-n4-O0W.title" = "Find…"; - -/* Class = "NSMenuItem"; title = "Find and Replace…"; ObjectID = "YEy-JH-Tfz"; */ -"YEy-JH-Tfz.title" = "Find and Replace…"; - -/* Class = "NSMenuItem"; title = "Start Speaking"; ObjectID = "Ynk-f8-cLZ"; */ -"Ynk-f8-cLZ.title" = "Start Speaking"; - -/* Class = "NSMenuItem"; title = "Window"; ObjectID = "aUF-d1-5bR"; */ -"aUF-d1-5bR.title" = "Window"; - -/* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "buJ-ug-pKt"; */ -"buJ-ug-pKt.title" = "Use Selection for Find"; - -/* Class = "NSMenu"; title = "Transformations"; ObjectID = "c8a-y6-VQd"; */ -"c8a-y6-VQd.title" = "Transformations"; - -/* Class = "NSMenuItem"; title = "Smart Links"; ObjectID = "cwL-P1-jid"; */ -"cwL-P1-jid.title" = "Smart Links"; - -/* Class = "NSMenuItem"; title = "Make Lower Case"; ObjectID = "d9M-CD-aMd"; */ -"d9M-CD-aMd.title" = "Make Lower Case"; - -/* Class = "NSMenuItem"; title = "Undo"; ObjectID = "dRJ-4n-Yzg"; */ -"dRJ-4n-Yzg.title" = "Undo"; - -/* Class = "NSMenuItem"; title = "Paste"; ObjectID = "gVA-U4-sdL"; */ -"gVA-U4-sdL.title" = "Paste"; - -/* Class = "NSMenuItem"; title = "Smart Quotes"; ObjectID = "hQb-2v-fYv"; */ -"hQb-2v-fYv.title" = "Smart Quotes"; - -/* Class = "NSMenuItem"; title = "Check Document Now"; ObjectID = "hz2-CU-CR7"; */ -"hz2-CU-CR7.title" = "Check Document Now"; - -/* Class = "NSMenu"; title = "Services"; ObjectID = "hz9-B4-Xy5"; */ -"hz9-B4-Xy5.title" = "Services"; - -/* Class = "NSMenuItem"; title = "Check Grammar With Spelling"; ObjectID = "mK6-2p-4JG"; */ -"mK6-2p-4JG.title" = "Check Grammar With Spelling"; - -/* Class = "NSMenuItem"; title = "Delete"; ObjectID = "pa3-QI-u2k"; */ -"pa3-QI-u2k.title" = "Delete"; - -/* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "q09-fT-Sye"; */ -"q09-fT-Sye.title" = "Find Next"; - -/* Class = "NSMenu"; title = "Help"; ObjectID = "rJ0-wn-3NY"; */ -"rJ0-wn-3NY.title" = "Help"; - -/* Class = "NSMenuItem"; title = "Check Spelling While Typing"; ObjectID = "rbD-Rh-wIN"; */ -"rbD-Rh-wIN.title" = "Check Spelling While Typing"; - -/* Class = "NSMenuItem"; title = "Smart Dashes"; ObjectID = "rgM-f4-ycn"; */ -"rgM-f4-ycn.title" = "Smart Dashes"; - -/* Class = "NSMenuItem"; title = "Data Detectors"; ObjectID = "tRr-pd-1PS"; */ -"tRr-pd-1PS.title" = "Data Detectors"; - -/* Class = "NSMenu"; title = "APP_NAME"; ObjectID = "uQy-DD-JDr"; */ -"uQy-DD-JDr.title" = "APP_NAME"; - -/* Class = "NSMenuItem"; title = "Cut"; ObjectID = "uRl-iY-unG"; */ -"uRl-iY-unG.title" = "Cut"; - -/* Class = "NSMenuItem"; title = "Make Upper Case"; ObjectID = "vmV-6d-7jI"; */ -"vmV-6d-7jI.title" = "Make Upper Case"; - -/* Class = "NSMenuItem"; title = "Copy"; ObjectID = "x3v-GG-iWU"; */ -"x3v-GG-iWU.title" = "Copy"; - -/* Class = "NSMenuItem"; title = "Speech"; ObjectID = "xrE-MZ-jX0"; */ -"xrE-MZ-jX0.title" = "Speech"; - -/* Class = "NSMenuItem"; title = "Show Substitutions"; ObjectID = "z6F-FW-3nz"; */ -"z6F-FW-3nz.title" = "Show Substitutions"; diff --git a/macos/Runner/zh-Hant.lproj/MainMenu.strings b/macos/Runner/zh-Hant.lproj/MainMenu.strings deleted file mode 100644 index b74fd928e..000000000 --- a/macos/Runner/zh-Hant.lproj/MainMenu.strings +++ /dev/null @@ -1,195 +0,0 @@ - -/* Class = "NSMenuItem"; title = "APP_NAME"; ObjectID = "1Xt-HY-uBw"; */ -"1Xt-HY-uBw.title" = "APP_NAME"; - -/* Class = "NSMenu"; title = "Find"; ObjectID = "1b7-l0-nxx"; */ -"1b7-l0-nxx.title" = "Find"; - -/* Class = "NSMenuItem"; title = "Transformations"; ObjectID = "2oI-Rn-ZJC"; */ -"2oI-Rn-ZJC.title" = "Transformations"; - -/* Class = "NSMenu"; title = "Spelling"; ObjectID = "3IN-sU-3Bg"; */ -"3IN-sU-3Bg.title" = "Spelling"; - -/* Class = "NSMenu"; title = "Speech"; ObjectID = "3rS-ZA-NoH"; */ -"3rS-ZA-NoH.title" = "Speech"; - -/* Class = "NSMenuItem"; title = "Find"; ObjectID = "4EN-yA-p0u"; */ -"4EN-yA-p0u.title" = "Find"; - -/* Class = "NSMenuItem"; title = "Enter Full Screen"; ObjectID = "4J7-dP-txa"; */ -"4J7-dP-txa.title" = "Enter Full Screen"; - -/* Class = "NSMenuItem"; title = "Quit APP_NAME"; ObjectID = "4sb-4s-VLi"; */ -"4sb-4s-VLi.title" = "Quit APP_NAME"; - -/* Class = "NSMenuItem"; title = "Edit"; ObjectID = "5QF-Oa-p0T"; */ -"5QF-Oa-p0T.title" = "Edit"; - -/* Class = "NSMenuItem"; title = "About APP_NAME"; ObjectID = "5kV-Vb-QxS"; */ -"5kV-Vb-QxS.title" = "About APP_NAME"; - -/* Class = "NSMenuItem"; title = "Redo"; ObjectID = "6dh-zS-Vam"; */ -"6dh-zS-Vam.title" = "Redo"; - -/* Class = "NSMenuItem"; title = "Correct Spelling Automatically"; ObjectID = "78Y-hA-62v"; */ -"78Y-hA-62v.title" = "Correct Spelling Automatically"; - -/* Class = "NSMenuItem"; title = "Substitutions"; ObjectID = "9ic-FL-obx"; */ -"9ic-FL-obx.title" = "Substitutions"; - -/* Class = "NSMenuItem"; title = "Smart Copy/Paste"; ObjectID = "9yt-4B-nSM"; */ -"9yt-4B-nSM.title" = "Smart Copy/Paste"; - -/* Class = "NSMenu"; title = "Main Menu"; ObjectID = "AYu-sK-qS6"; */ -"AYu-sK-qS6.title" = "Main Menu"; - -/* Class = "NSMenuItem"; title = "Preferences…"; ObjectID = "BOF-NM-1cW"; */ -"BOF-NM-1cW.title" = "Preferences…"; - -/* Class = "NSMenuItem"; title = "Spelling and Grammar"; ObjectID = "Dv1-io-Yv7"; */ -"Dv1-io-Yv7.title" = "Spelling and Grammar"; - -/* Class = "NSMenuItem"; title = "Help"; ObjectID = "EPT-qC-fAb"; */ -"EPT-qC-fAb.title" = "Help"; - -/* Class = "NSMenu"; title = "Substitutions"; ObjectID = "FeM-D8-WVr"; */ -"FeM-D8-WVr.title" = "Substitutions"; - -/* Class = "NSMenuItem"; title = "View"; ObjectID = "H8h-7b-M4v"; */ -"H8h-7b-M4v.title" = "View"; - -/* Class = "NSMenuItem"; title = "Text Replacement"; ObjectID = "HFQ-gK-NFA"; */ -"HFQ-gK-NFA.title" = "Text Replacement"; - -/* Class = "NSMenuItem"; title = "Show Spelling and Grammar"; ObjectID = "HFo-cy-zxI"; */ -"HFo-cy-zxI.title" = "Show Spelling and Grammar"; - -/* Class = "NSMenu"; title = "View"; ObjectID = "HyV-fh-RgO"; */ -"HyV-fh-RgO.title" = "View"; - -/* Class = "NSMenuItem"; title = "Show All"; ObjectID = "Kd2-mp-pUS"; */ -"Kd2-mp-pUS.title" = "Show All"; - -/* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "LE2-aR-0XJ"; */ -"LE2-aR-0XJ.title" = "Bring All to Front"; - -/* Class = "NSMenuItem"; title = "Services"; ObjectID = "NMo-om-nkz"; */ -"NMo-om-nkz.title" = "Services"; - -/* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "OY7-WF-poV"; */ -"OY7-WF-poV.title" = "Minimize"; - -/* Class = "NSMenuItem"; title = "Hide APP_NAME"; ObjectID = "Olw-nP-bQN"; */ -"Olw-nP-bQN.title" = "Hide APP_NAME"; - -/* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "OwM-mh-QMV"; */ -"OwM-mh-QMV.title" = "Find Previous"; - -/* Class = "NSMenuItem"; title = "Stop Speaking"; ObjectID = "Oyz-dy-DGm"; */ -"Oyz-dy-DGm.title" = "Stop Speaking"; - -/* Class = "NSWindow"; title = "APP_NAME"; ObjectID = "QvC-M9-y7g"; */ -"QvC-M9-y7g.title" = "APP_NAME"; - -/* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "R4o-n2-Eq4"; */ -"R4o-n2-Eq4.title" = "Zoom"; - -/* Class = "NSMenuItem"; title = "Select All"; ObjectID = "Ruw-6m-B2m"; */ -"Ruw-6m-B2m.title" = "Select All"; - -/* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "S0p-oC-mLd"; */ -"S0p-oC-mLd.title" = "Jump to Selection"; - -/* Class = "NSMenu"; title = "Window"; ObjectID = "Td7-aD-5lo"; */ -"Td7-aD-5lo.title" = "Window"; - -/* Class = "NSMenuItem"; title = "Capitalize"; ObjectID = "UEZ-Bs-lqG"; */ -"UEZ-Bs-lqG.title" = "Capitalize"; - -/* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "Vdr-fp-XzO"; */ -"Vdr-fp-XzO.title" = "Hide Others"; - -/* Class = "NSMenu"; title = "Edit"; ObjectID = "W48-6f-4Dl"; */ -"W48-6f-4Dl.title" = "Edit"; - -/* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "WeT-3V-zwk"; */ -"WeT-3V-zwk.title" = "Paste and Match Style"; - -/* Class = "NSMenuItem"; title = "Find…"; ObjectID = "Xz5-n4-O0W"; */ -"Xz5-n4-O0W.title" = "Find…"; - -/* Class = "NSMenuItem"; title = "Find and Replace…"; ObjectID = "YEy-JH-Tfz"; */ -"YEy-JH-Tfz.title" = "Find and Replace…"; - -/* Class = "NSMenuItem"; title = "Start Speaking"; ObjectID = "Ynk-f8-cLZ"; */ -"Ynk-f8-cLZ.title" = "Start Speaking"; - -/* Class = "NSMenuItem"; title = "Window"; ObjectID = "aUF-d1-5bR"; */ -"aUF-d1-5bR.title" = "Window"; - -/* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "buJ-ug-pKt"; */ -"buJ-ug-pKt.title" = "Use Selection for Find"; - -/* Class = "NSMenu"; title = "Transformations"; ObjectID = "c8a-y6-VQd"; */ -"c8a-y6-VQd.title" = "Transformations"; - -/* Class = "NSMenuItem"; title = "Smart Links"; ObjectID = "cwL-P1-jid"; */ -"cwL-P1-jid.title" = "Smart Links"; - -/* Class = "NSMenuItem"; title = "Make Lower Case"; ObjectID = "d9M-CD-aMd"; */ -"d9M-CD-aMd.title" = "Make Lower Case"; - -/* Class = "NSMenuItem"; title = "Undo"; ObjectID = "dRJ-4n-Yzg"; */ -"dRJ-4n-Yzg.title" = "Undo"; - -/* Class = "NSMenuItem"; title = "Paste"; ObjectID = "gVA-U4-sdL"; */ -"gVA-U4-sdL.title" = "Paste"; - -/* Class = "NSMenuItem"; title = "Smart Quotes"; ObjectID = "hQb-2v-fYv"; */ -"hQb-2v-fYv.title" = "Smart Quotes"; - -/* Class = "NSMenuItem"; title = "Check Document Now"; ObjectID = "hz2-CU-CR7"; */ -"hz2-CU-CR7.title" = "Check Document Now"; - -/* Class = "NSMenu"; title = "Services"; ObjectID = "hz9-B4-Xy5"; */ -"hz9-B4-Xy5.title" = "Services"; - -/* Class = "NSMenuItem"; title = "Check Grammar With Spelling"; ObjectID = "mK6-2p-4JG"; */ -"mK6-2p-4JG.title" = "Check Grammar With Spelling"; - -/* Class = "NSMenuItem"; title = "Delete"; ObjectID = "pa3-QI-u2k"; */ -"pa3-QI-u2k.title" = "Delete"; - -/* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "q09-fT-Sye"; */ -"q09-fT-Sye.title" = "Find Next"; - -/* Class = "NSMenu"; title = "Help"; ObjectID = "rJ0-wn-3NY"; */ -"rJ0-wn-3NY.title" = "Help"; - -/* Class = "NSMenuItem"; title = "Check Spelling While Typing"; ObjectID = "rbD-Rh-wIN"; */ -"rbD-Rh-wIN.title" = "Check Spelling While Typing"; - -/* Class = "NSMenuItem"; title = "Smart Dashes"; ObjectID = "rgM-f4-ycn"; */ -"rgM-f4-ycn.title" = "Smart Dashes"; - -/* Class = "NSMenuItem"; title = "Data Detectors"; ObjectID = "tRr-pd-1PS"; */ -"tRr-pd-1PS.title" = "Data Detectors"; - -/* Class = "NSMenu"; title = "APP_NAME"; ObjectID = "uQy-DD-JDr"; */ -"uQy-DD-JDr.title" = "APP_NAME"; - -/* Class = "NSMenuItem"; title = "Cut"; ObjectID = "uRl-iY-unG"; */ -"uRl-iY-unG.title" = "Cut"; - -/* Class = "NSMenuItem"; title = "Make Upper Case"; ObjectID = "vmV-6d-7jI"; */ -"vmV-6d-7jI.title" = "Make Upper Case"; - -/* Class = "NSMenuItem"; title = "Copy"; ObjectID = "x3v-GG-iWU"; */ -"x3v-GG-iWU.title" = "Copy"; - -/* Class = "NSMenuItem"; title = "Speech"; ObjectID = "xrE-MZ-jX0"; */ -"xrE-MZ-jX0.title" = "Speech"; - -/* Class = "NSMenuItem"; title = "Show Substitutions"; ObjectID = "z6F-FW-3nz"; */ -"z6F-FW-3nz.title" = "Show Substitutions"; diff --git a/macos/RunnerTests/RunnerTests.swift b/macos/RunnerTests/RunnerTests.swift deleted file mode 100644 index 5418c9f53..000000000 --- a/macos/RunnerTests/RunnerTests.swift +++ /dev/null @@ -1,12 +0,0 @@ -import FlutterMacOS -import Cocoa -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. - } - -} diff --git a/macos/en.lproj/InfoPlist.strings b/macos/en.lproj/InfoPlist.strings deleted file mode 100644 index 92e201e9c..000000000 --- a/macos/en.lproj/InfoPlist.strings +++ /dev/null @@ -1 +0,0 @@ -"CFBundleDisplayName" = "SIT Life"; diff --git a/macos/zh-Hans.lproj/InfoPlist.strings b/macos/zh-Hans.lproj/InfoPlist.strings deleted file mode 100644 index 16e24cf48..000000000 --- a/macos/zh-Hans.lproj/InfoPlist.strings +++ /dev/null @@ -1 +0,0 @@ -"CFBundleDisplayName" = "小应生活"; diff --git a/macos/zh-Hant.lproj/InfoPlist.strings b/macos/zh-Hant.lproj/InfoPlist.strings deleted file mode 100644 index 0b164081a..000000000 --- a/macos/zh-Hant.lproj/InfoPlist.strings +++ /dev/null @@ -1 +0,0 @@ -"CFBundleDisplayName" = "小鷹生活"; diff --git a/pubspec.lock b/pubspec.lock deleted file mode 100644 index 1b89f2efc..000000000 --- a/pubspec.lock +++ /dev/null @@ -1,2127 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - _fe_analyzer_shared: - dependency: transitive - description: - name: _fe_analyzer_shared - sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 - url: "https://pub.dev" - source: hosted - version: "64.0.0" - add_2_calendar: - dependency: "direct main" - description: - name: add_2_calendar - sha256: "8d7a82aba607d35f2a5bc913419e12f865a96a350a8ad2509a59322bc161f200" - url: "https://pub.dev" - source: hosted - version: "3.0.1" - analyzer: - dependency: transitive - description: - name: analyzer - sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" - url: "https://pub.dev" - source: hosted - version: "6.2.0" - animations: - dependency: "direct main" - description: - name: animations - sha256: "708e4b68c23228c264b038fe7003a2f5d01ce85fc64d8cae090e86b27fcea6c5" - url: "https://pub.dev" - source: hosted - version: "2.0.10" - app_links: - dependency: "direct main" - description: - name: app_links - sha256: "4e392b5eba997df356ca6021f28431ce1cfeb16758699553a94b13add874a3bb" - url: "https://pub.dev" - source: hosted - version: "3.5.0" - app_settings: - dependency: "direct main" - description: - name: app_settings - sha256: "09bc7fe0313a507087bec1a3baf555f0576e816a760cbb31813a88890a09d9e5" - url: "https://pub.dev" - source: hosted - version: "5.1.1" - archive: - dependency: transitive - description: - name: archive - sha256: "7b875fd4a20b165a3084bd2d210439b22ebc653f21cea4842729c0c30c82596b" - url: "https://pub.dev" - source: hosted - version: "3.4.9" - args: - dependency: transitive - description: - name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 - url: "https://pub.dev" - source: hosted - version: "2.4.2" - asn1lib: - dependency: transitive - description: - name: asn1lib - sha256: "21afe4333076c02877d14f4a89df111e658a6d466cbfc802eb705eb91bd5adfd" - url: "https://pub.dev" - source: hosted - version: "1.5.0" - async: - dependency: transitive - description: - name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" - url: "https://pub.dev" - source: hosted - version: "2.11.0" - audio_session: - dependency: transitive - description: - name: audio_session - sha256: "6fdf255ed3af86535c96452c33ecff1245990bb25a605bfb1958661ccc3d467f" - url: "https://pub.dev" - source: hosted - version: "0.1.18" - auto_size_text: - dependency: "direct main" - description: - name: auto_size_text - sha256: "3f5261cd3fb5f2a9ab4e2fc3fba84fd9fcaac8821f20a1d4e71f557521b22599" - url: "https://pub.dev" - source: hosted - version: "3.0.0" - basic_utils: - dependency: transitive - description: - name: basic_utils - sha256: "2064b21d3c41ed7654bc82cc476fd65542e04d60059b74d5eed490a4da08fc6c" - url: "https://pub.dev" - source: hosted - version: "5.7.0" - beautiful_soup_dart: - dependency: "direct main" - description: - name: beautiful_soup_dart - sha256: "57e23946c85776dd9515a4e9a14263fff37dbedbd559bc4412bf565886e12b10" - url: "https://pub.dev" - source: hosted - version: "0.3.0" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - bordered_text: - dependency: "direct main" - description: - name: bordered_text - sha256: e52c549c9d01fdf6359eee7220900eb5a5853b08aa862c8c604442918ca6b4c4 - url: "https://pub.dev" - source: hosted - version: "2.0.0" - build: - dependency: transitive - description: - name: build - sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - build_config: - dependency: transitive - description: - name: build_config - sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 - url: "https://pub.dev" - source: hosted - version: "1.1.1" - build_daemon: - dependency: transitive - description: - name: build_daemon - sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" - url: "https://pub.dev" - source: hosted - version: "4.0.1" - build_resolvers: - dependency: transitive - description: - name: build_resolvers - sha256: "64e12b0521812d1684b1917bc80945625391cb9bdd4312536b1d69dcb6133ed8" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - build_runner: - dependency: "direct dev" - description: - name: build_runner - sha256: "67d591d602906ef9201caf93452495ad1812bea2074f04e25dbd7c133785821b" - url: "https://pub.dev" - source: hosted - version: "2.4.7" - build_runner_core: - dependency: transitive - description: - name: build_runner_core - sha256: c9e32d21dd6626b5c163d48b037ce906bbe428bc23ab77bcd77bb21e593b6185 - url: "https://pub.dev" - source: hosted - version: "7.2.11" - built_collection: - dependency: transitive - description: - name: built_collection - sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" - url: "https://pub.dev" - source: hosted - version: "5.1.1" - built_value: - dependency: transitive - description: - name: built_value - sha256: "69acb7007eb2a31dc901512bfe0f7b767168be34cb734835d54c070bfa74c1b2" - url: "https://pub.dev" - source: hosted - version: "8.8.0" - cached_network_image: - dependency: "direct main" - description: - name: cached_network_image - sha256: f98972704692ba679db144261172a8e20feb145636c617af0eb4022132a6797f - url: "https://pub.dev" - source: hosted - version: "3.3.0" - cached_network_image_platform_interface: - dependency: transitive - description: - name: cached_network_image_platform_interface - sha256: "56aa42a7a01e3c9db8456d9f3f999931f1e05535b5a424271e9a38cabf066613" - url: "https://pub.dev" - source: hosted - version: "3.0.0" - cached_network_image_web: - dependency: transitive - description: - name: cached_network_image_web - sha256: "759b9a9f8f6ccbb66c185df805fac107f05730b1dab9c64626d1008cca532257" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - characters: - dependency: transitive - description: - name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" - url: "https://pub.dev" - source: hosted - version: "1.3.0" - check_vpn_connection: - dependency: "direct main" - description: - name: check_vpn_connection - sha256: "68029b66124eb5de2bc877f516ad9773f8b57e0a9ccf5c64609bb7cb8f14f3a0" - url: "https://pub.dev" - source: hosted - version: "0.0.2" - checked_yaml: - dependency: transitive - description: - name: checked_yaml - sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff - url: "https://pub.dev" - source: hosted - version: "2.0.3" - chewie: - dependency: "direct main" - description: - name: chewie - sha256: "3427e469d7cc99536ac4fbaa069b3352c21760263e65ffb4f0e1c054af43a73e" - url: "https://pub.dev" - source: hosted - version: "1.7.4" - clock: - dependency: transitive - description: - name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf - url: "https://pub.dev" - source: hosted - version: "1.1.1" - code_builder: - dependency: transitive - description: - name: code_builder - sha256: b2151ce26a06171005b379ecff6e08d34c470180ffe16b8e14b6d52be292b55f - url: "https://pub.dev" - source: hosted - version: "4.8.0" - collection: - dependency: "direct main" - description: - name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a - url: "https://pub.dev" - source: hosted - version: "1.18.0" - connectivity_plus: - dependency: "direct main" - description: - name: connectivity_plus - sha256: "224a77051d52a11fbad53dd57827594d3bd24f945af28bd70bab376d68d437f0" - url: "https://pub.dev" - source: hosted - version: "5.0.2" - connectivity_plus_platform_interface: - dependency: transitive - description: - name: connectivity_plus_platform_interface - sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a - url: "https://pub.dev" - source: hosted - version: "1.2.4" - convert: - dependency: transitive - description: - name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" - url: "https://pub.dev" - source: hosted - version: "3.1.1" - cookie_jar: - dependency: "direct main" - description: - name: cookie_jar - sha256: a6ac027d3ed6ed756bfce8f3ff60cb479e266f3b0fdabd6242b804b6765e52de - url: "https://pub.dev" - source: hosted - version: "4.0.8" - copy_with_extension: - dependency: "direct main" - description: - name: copy_with_extension - sha256: fbcf890b0c34aedf0894f91a11a579994b61b4e04080204656b582708b5b1125 - url: "https://pub.dev" - source: hosted - version: "5.0.4" - copy_with_extension_gen: - dependency: "direct dev" - description: - name: copy_with_extension_gen - sha256: "51cd11094096d40824c8da629ca7f16f3b7cea5fc44132b679617483d43346b0" - url: "https://pub.dev" - source: hosted - version: "5.0.4" - coverage: - dependency: transitive - description: - name: coverage - sha256: ac86d3abab0f165e4b8f561280ff4e066bceaac83c424dd19f1ae2c2fcd12ca9 - url: "https://pub.dev" - source: hosted - version: "1.7.1" - cross_file: - dependency: transitive - description: - name: cross_file - sha256: fedaadfa3a6996f75211d835aaeb8fede285dae94262485698afd832371b9a5e - url: "https://pub.dev" - source: hosted - version: "0.3.3+8" - crypto: - dependency: "direct main" - description: - name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab - url: "https://pub.dev" - source: hosted - version: "3.0.3" - csslib: - dependency: transitive - description: - name: csslib - sha256: "831883fb353c8bdc1d71979e5b342c7d88acfbc643113c14ae51e2442ea0f20f" - url: "https://pub.dev" - source: hosted - version: "0.17.3" - cupertino_icons: - dependency: transitive - description: - name: cupertino_icons - sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d - url: "https://pub.dev" - source: hosted - version: "1.0.6" - cupertino_onboarding: - dependency: "direct main" - description: - name: cupertino_onboarding - sha256: "948327610ba9204cde7d3f9a4f7fa2c5571272702a8c8c4d9ae0a2ec022393f1" - url: "https://pub.dev" - source: hosted - version: "1.2.0" - dart_style: - dependency: transitive - description: - name: dart_style - sha256: "40ae61a5d43feea6d24bd22c0537a6629db858963b99b4bc1c3db80676f32368" - url: "https://pub.dev" - source: hosted - version: "2.3.4" - dbus: - dependency: transitive - description: - name: dbus - sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" - url: "https://pub.dev" - source: hosted - version: "0.7.10" - device_info_plus: - dependency: "direct main" - description: - name: device_info_plus - sha256: "0042cb3b2a76413ea5f8a2b40cec2a33e01d0c937e91f0f7c211fde4f7739ba6" - url: "https://pub.dev" - source: hosted - version: "9.1.1" - device_info_plus_platform_interface: - dependency: transitive - description: - name: device_info_plus_platform_interface - sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 - url: "https://pub.dev" - source: hosted - version: "7.0.0" - dio: - dependency: "direct main" - description: - name: dio - sha256: "797e1e341c3dd2f69f2dad42564a6feff3bfb87187d05abb93b9609e6f1645c3" - url: "https://pub.dev" - source: hosted - version: "5.4.0" - dio_cookie_manager: - dependency: "direct main" - description: - name: dio_cookie_manager - sha256: e79498b0f632897ff0c28d6e8178b4bc6e9087412401f618c31fa0904ace050d - url: "https://pub.dev" - source: hosted - version: "3.1.1" - dots_indicator: - dependency: transitive - description: - name: dots_indicator - sha256: f1599baa429936ba87f06ae5f2adc920a367b16d08f74db58c3d0f6e93bcdb5c - url: "https://pub.dev" - source: hosted - version: "2.1.2" - dynamic_color: - dependency: "direct main" - description: - name: dynamic_color - sha256: "8b8bd1d798bd393e11eddeaa8ae95b12ff028bf7d5998fc5d003488cd5f4ce2f" - url: "https://pub.dev" - source: hosted - version: "1.6.8" - easy_localization: - dependency: "direct main" - description: - name: easy_localization - sha256: de63e3b422adfc97f256cbb3f8cf12739b6a4993d390f3cadb3f51837afaefe5 - url: "https://pub.dev" - source: hosted - version: "3.0.3" - easy_logger: - dependency: transitive - description: - name: easy_logger - sha256: c764a6e024846f33405a2342caf91c62e357c24b02c04dbc712ef232bf30ffb7 - url: "https://pub.dev" - source: hosted - version: "0.0.2" - email_validator: - dependency: "direct main" - description: - name: email_validator - sha256: e9a90f27ab2b915a27d7f9c2a7ddda5dd752d6942616ee83529b686fc086221b - url: "https://pub.dev" - source: hosted - version: "2.1.17" - encrypt: - dependency: "direct main" - description: - name: encrypt - sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2" - url: "https://pub.dev" - source: hosted - version: "5.0.3" - enough_convert: - dependency: transitive - description: - name: enough_convert - sha256: c67d85ca21aaa0648f155907362430701db41f7ec8e6501a58ad9cd9d8569d01 - url: "https://pub.dev" - source: hosted - version: "1.6.0" - enough_mail: - dependency: "direct main" - description: - name: enough_mail - sha256: a88d8c56907caeffdebc4cf34f3a5665c09a8d7496ef5e09b3166f41cf409f81 - url: "https://pub.dev" - source: hosted - version: "2.1.6" - enough_mail_html: - dependency: "direct main" - description: - name: enough_mail_html - sha256: c661c5a1299377682e28edaadbde0fc8cd0d9497abba0824d5d6cda7911a6c83 - url: "https://pub.dev" - source: hosted - version: "2.0.1" - equatable: - dependency: transitive - description: - name: equatable - sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 - url: "https://pub.dev" - source: hosted - version: "2.0.5" - event_bus: - dependency: "direct main" - description: - name: event_bus - sha256: "44baa799834f4c803921873e7446a2add0f3efa45e101a054b1f0ab9b95f8edc" - url: "https://pub.dev" - source: hosted - version: "2.0.0" - fake_async: - dependency: transitive - description: - name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" - url: "https://pub.dev" - source: hosted - version: "1.3.1" - ffi: - dependency: transitive - description: - name: ffi - sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" - url: "https://pub.dev" - source: hosted - version: "2.1.0" - file: - dependency: transitive - description: - name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" - url: "https://pub.dev" - source: hosted - version: "7.0.0" - file_picker: - dependency: "direct main" - description: - name: file_picker - sha256: "4e42aacde3b993c5947467ab640882c56947d9d27342a5b6f2895b23956954a6" - url: "https://pub.dev" - source: hosted - version: "6.1.1" - file_selector_linux: - dependency: transitive - description: - name: file_selector_linux - sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492" - url: "https://pub.dev" - source: hosted - version: "0.9.2+1" - file_selector_macos: - dependency: transitive - description: - name: file_selector_macos - sha256: b15c3da8bd4908b9918111fa486903f5808e388b8d1c559949f584725a6594d6 - url: "https://pub.dev" - source: hosted - version: "0.9.3+3" - file_selector_platform_interface: - dependency: transitive - description: - name: file_selector_platform_interface - sha256: "0aa47a725c346825a2bd396343ce63ac00bda6eff2fbc43eabe99737dede8262" - url: "https://pub.dev" - source: hosted - version: "2.6.1" - file_selector_windows: - dependency: transitive - description: - name: file_selector_windows - sha256: d3547240c20cabf205c7c7f01a50ecdbc413755814d6677f3cb366f04abcead0 - url: "https://pub.dev" - source: hosted - version: "0.9.3+1" - fixnum: - dependency: transitive - description: - name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - fk_user_agent: - dependency: "direct main" - description: - name: fk_user_agent - sha256: fd6c94e120786985a292d12f61422a581f4e851148d5940af38b819357b8ad0d - url: "https://pub.dev" - source: hosted - version: "2.1.0" - fl_chart: - dependency: "direct main" - description: - name: fl_chart - sha256: "5a74434cc83bf64346efb562f1a06eefaf1bcb530dc3d96a104f631a1eff8d79" - url: "https://pub.dev" - source: hosted - version: "0.65.0" - flex_color_picker: - dependency: "direct main" - description: - name: flex_color_picker - sha256: f37476ab3e80dcaca94e428e159944d465dd16312fda9ff41e07e86f04bfa51c - url: "https://pub.dev" - source: hosted - version: "3.3.0" - flex_seed_scheme: - dependency: transitive - description: - name: flex_seed_scheme - sha256: "29c12aba221eb8a368a119685371381f8035011d18de5ba277ad11d7dfb8657f" - url: "https://pub.dev" - source: hosted - version: "1.4.0" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_adaptive_ui: - dependency: "direct main" - description: - name: flutter_adaptive_ui - sha256: de6891c6a97512ca70cc30798a85808dfac834a592f5381220f791628972a804 - url: "https://pub.dev" - source: hosted - version: "0.8.0+1" - flutter_cache_manager: - dependency: transitive - description: - name: flutter_cache_manager - sha256: "8207f27539deb83732fdda03e259349046a39a4c767269285f449ade355d54ba" - url: "https://pub.dev" - source: hosted - version: "3.3.1" - flutter_html: - dependency: "direct main" - description: - name: flutter_html - sha256: "02ad69e813ecfc0728a455e4bf892b9379983e050722b1dce00192ee2e41d1ee" - url: "https://pub.dev" - source: hosted - version: "3.0.0-beta.2" - flutter_lints: - dependency: "direct dev" - description: - name: flutter_lints - sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7 - url: "https://pub.dev" - source: hosted - version: "3.0.1" - flutter_localizations: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - flutter_platform_widgets: - dependency: "direct main" - description: - name: flutter_platform_widgets - sha256: "4970c211af1dad0a161e6379d04de2cace80283da0439f2f87d31a541f9b2b84" - url: "https://pub.dev" - source: hosted - version: "6.0.2" - flutter_plugin_android_lifecycle: - dependency: transitive - description: - name: flutter_plugin_android_lifecycle - sha256: b068ffc46f82a55844acfa4fdbb61fad72fa2aef0905548419d97f0f95c456da - url: "https://pub.dev" - source: hosted - version: "2.0.17" - flutter_riverpod: - dependency: "direct main" - description: - name: flutter_riverpod - sha256: da9591d1f8d5881628ccd5c25c40e74fc3eef50ba45e40c3905a06e1712412d5 - url: "https://pub.dev" - source: hosted - version: "2.4.9" - flutter_screenutil: - dependency: "direct main" - description: - name: flutter_screenutil - sha256: "8cf100b8e4973dc570b6415a2090b0bfaa8756ad333db46939efc3e774ee100d" - url: "https://pub.dev" - source: hosted - version: "5.9.0" - flutter_staggered_grid_view: - dependency: "direct main" - description: - name: flutter_staggered_grid_view - sha256: "19e7abb550c96fbfeb546b23f3ff356ee7c59a019a651f8f102a4ba9b7349395" - url: "https://pub.dev" - source: hosted - version: "0.7.0" - flutter_svg: - dependency: "direct main" - description: - name: flutter_svg - sha256: d39e7f95621fc84376bc0f7d504f05c3a41488c562f4a8ad410569127507402c - url: "https://pub.dev" - source: hosted - version: "2.0.9" - flutter_svg_provider: - dependency: "direct main" - description: - name: flutter_svg_provider - sha256: "13d85033c6ae63c8073268f9c5c3c74e5de178224183eaff1d47f08f70c59fcc" - url: "https://pub.dev" - source: hosted - version: "1.0.6" - flutter_swipe_action_cell: - dependency: "direct main" - description: - name: flutter_swipe_action_cell - sha256: "5cea4d8c3d333c5610ffa4bc39a30561927f90857be19ba33a8322fdc856d335" - url: "https://pub.dev" - source: hosted - version: "3.1.3" - flutter_swipe_detector: - dependency: "direct main" - description: - name: flutter_swipe_detector - sha256: ae6fe331de414632c0122a7047d64dfe2332c95b3f06a05118a7ea538ff19eda - url: "https://pub.dev" - source: hosted - version: "2.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" - flutter_widget_from_html: - dependency: "direct main" - description: - name: flutter_widget_from_html - sha256: "5857f120b08b791a67b065384eaad477badafa3ffb57d6095f28a4f6fd37ead4" - url: "https://pub.dev" - source: hosted - version: "0.14.9" - flutter_widget_from_html_core: - dependency: transitive - description: - name: flutter_widget_from_html_core - sha256: "86d40a9f26d10011664df057c950e9c348ee1a7dbf141f295a07b0075ffd780b" - url: "https://pub.dev" - source: hosted - version: "0.14.9" - format: - dependency: "direct main" - description: - name: format - sha256: "8829452e1f01321dabe734a082674fe32bb0224caacb08d6d2d4b33f7eca3bdd" - url: "https://pub.dev" - source: hosted - version: "1.4.0" - frontend_server_client: - dependency: transitive - description: - name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" - url: "https://pub.dev" - source: hosted - version: "3.2.0" - fwfh_cached_network_image: - dependency: transitive - description: - name: fwfh_cached_network_image - sha256: "952aea958a5fda7d616cc297ba4bc08427e381459e75526fa375d6d8345630d3" - url: "https://pub.dev" - source: hosted - version: "0.14.2" - fwfh_chewie: - dependency: transitive - description: - name: fwfh_chewie - sha256: bbb036cd322ab77dc0edd34cbbf76181681f5e414987ece38745dc4f3d7408ed - url: "https://pub.dev" - source: hosted - version: "0.14.7" - fwfh_just_audio: - dependency: transitive - description: - name: fwfh_just_audio - sha256: "4962bc59cf8bbb0a77a55ff56a7b925612b0d8263bc2ede3636b9c86113cb493" - url: "https://pub.dev" - source: hosted - version: "0.14.2" - fwfh_svg: - dependency: transitive - description: - name: fwfh_svg - sha256: "26df142c1784c29c3675ad0d37f589fc5c2173a14fc002d2c38cde3d0f117302" - url: "https://pub.dev" - source: hosted - version: "0.8.0+4" - fwfh_url_launcher: - dependency: transitive - description: - name: fwfh_url_launcher - sha256: "2a526c9819f74b4106ba2fba4dac79f0082deecd8d2c7011cd0471cb710e3eff" - url: "https://pub.dev" - source: hosted - version: "0.9.0+4" - fwfh_webview: - dependency: transitive - description: - name: fwfh_webview - sha256: b828bb5ddd4361a866cdb8f1b0de4f3348f332915ecf2f4215ba17e46c656adc - url: "https://pub.dev" - source: hosted - version: "0.14.8" - glob: - dependency: transitive - description: - name: glob - sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - go_router: - dependency: "direct main" - description: - name: go_router - sha256: c5fa45fa502ee880839e3b2152d987c44abae26d064a2376d4aad434cf0f7b15 - url: "https://pub.dev" - source: hosted - version: "12.1.3" - graphs: - dependency: transitive - description: - name: graphs - sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 - url: "https://pub.dev" - source: hosted - version: "2.3.1" - gtk: - dependency: transitive - description: - name: gtk - sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c - url: "https://pub.dev" - source: hosted - version: "2.1.0" - hive: - dependency: "direct main" - description: - name: hive - sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" - url: "https://pub.dev" - source: hosted - version: "2.2.3" - hive_flutter: - dependency: "direct main" - description: - name: hive_flutter - sha256: dca1da446b1d808a51689fb5d0c6c9510c0a2ba01e22805d492c73b68e33eecc - url: "https://pub.dev" - source: hosted - version: "1.1.0" - hive_generator: - dependency: "direct dev" - description: - name: hive_generator - sha256: "06cb8f58ace74de61f63500564931f9505368f45f98958bd7a6c35ba24159db4" - url: "https://pub.dev" - source: hosted - version: "2.0.1" - html: - dependency: "direct main" - description: - name: html - sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" - url: "https://pub.dev" - source: hosted - version: "0.15.4" - http: - dependency: transitive - description: - name: http - sha256: d4872660c46d929f6b8a9ef4e7a7eff7e49bbf0c4ec3f385ee32df5119175139 - url: "https://pub.dev" - source: hosted - version: "1.1.2" - http_multi_server: - dependency: transitive - description: - name: http_multi_server - sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" - url: "https://pub.dev" - source: hosted - version: "3.2.1" - http_parser: - dependency: transitive - description: - name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" - url: "https://pub.dev" - source: hosted - version: "4.0.2" - ical: - dependency: "direct main" - description: - name: ical - sha256: "49f7096a0ed9f1abb4ccc690c946e70a7c291d633eb1d09b296eef7877c1ecf3" - url: "https://pub.dev" - source: hosted - version: "0.2.2" - image: - dependency: transitive - description: - name: image - sha256: "028f61960d56f26414eb616b48b04eb37d700cbe477b7fb09bf1d7ce57fd9271" - url: "https://pub.dev" - source: hosted - version: "4.1.3" - image_picker: - dependency: "direct main" - description: - name: image_picker - sha256: fc712337719239b0b6e41316aa133350b078fa39b6cbd706b61f3fd421b03c77 - url: "https://pub.dev" - source: hosted - version: "1.0.5" - image_picker_android: - dependency: transitive - description: - name: image_picker_android - sha256: d6a6e78821086b0b737009b09363018309bbc6de3fd88cc5c26bc2bb44a4957f - url: "https://pub.dev" - source: hosted - version: "0.8.8+2" - image_picker_for_web: - dependency: transitive - description: - name: image_picker_for_web - sha256: "50bc9ae6a77eea3a8b11af5eb6c661eeb858fdd2f734c2a4fd17086922347ef7" - url: "https://pub.dev" - source: hosted - version: "3.0.1" - image_picker_ios: - dependency: transitive - description: - name: image_picker_ios - sha256: "76ec722aeea419d03aa915c2c96bf5b47214b053899088c9abb4086ceecf97a7" - url: "https://pub.dev" - source: hosted - version: "0.8.8+4" - image_picker_linux: - dependency: transitive - description: - name: image_picker_linux - sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa" - url: "https://pub.dev" - source: hosted - version: "0.2.1+1" - image_picker_macos: - dependency: transitive - description: - name: image_picker_macos - sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62" - url: "https://pub.dev" - source: hosted - version: "0.2.1+1" - image_picker_platform_interface: - dependency: transitive - description: - name: image_picker_platform_interface - sha256: ed9b00e63977c93b0d2d2b343685bed9c324534ba5abafbb3dfbd6a780b1b514 - url: "https://pub.dev" - source: hosted - version: "2.9.1" - image_picker_windows: - dependency: transitive - description: - name: image_picker_windows - sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" - url: "https://pub.dev" - source: hosted - version: "0.2.1+1" - intl: - dependency: "direct main" - description: - name: intl - sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf - url: "https://pub.dev" - source: hosted - version: "0.19.0" - io: - dependency: transitive - description: - name: io - sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" - url: "https://pub.dev" - source: hosted - version: "1.0.4" - js: - dependency: transitive - description: - name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 - url: "https://pub.dev" - source: hosted - version: "0.6.7" - json_annotation: - dependency: "direct main" - description: - name: json_annotation - sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 - url: "https://pub.dev" - source: hosted - version: "4.8.1" - json_serializable: - dependency: "direct main" - description: - name: json_serializable - sha256: aa1f5a8912615733e0fdc7a02af03308933c93235bdc8d50d0b0c8a8ccb0b969 - url: "https://pub.dev" - source: hosted - version: "6.7.1" - just_audio: - dependency: transitive - description: - name: just_audio - sha256: b607cd1a43bac03d85c3aaee00448ff4a589ef2a77104e3d409889ff079bf823 - url: "https://pub.dev" - source: hosted - version: "0.9.36" - just_audio_platform_interface: - dependency: transitive - description: - name: just_audio_platform_interface - sha256: c3dee0014248c97c91fe6299edb73dc4d6c6930a2f4f713579cd692d9e47f4a1 - url: "https://pub.dev" - source: hosted - version: "4.2.2" - just_audio_web: - dependency: transitive - description: - name: just_audio_web - sha256: "134356b0fe3d898293102b33b5fd618831ffdc72bb7a1b726140abdf22772b70" - url: "https://pub.dev" - source: hosted - version: "0.4.9" - lints: - dependency: transitive - description: - name: lints - sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 - url: "https://pub.dev" - source: hosted - version: "3.0.0" - list_counter: - dependency: transitive - description: - name: list_counter - sha256: c447ae3dfcd1c55f0152867090e67e219d42fe6d4f2807db4bbe8b8d69912237 - url: "https://pub.dev" - source: hosted - version: "1.0.2" - locale_names: - dependency: "direct main" - description: - name: locale_names - sha256: "7a89ca54072f4f13d0f5df5a9ba69337554bf2fd057d1dd2a238898f3f159374" - url: "https://pub.dev" - source: hosted - version: "1.1.1" - logger: - dependency: "direct main" - description: - name: logger - sha256: "6bbb9d6f7056729537a4309bda2e74e18e5d9f14302489cc1e93f33b3fe32cac" - url: "https://pub.dev" - source: hosted - version: "2.0.2+1" - logging: - dependency: transitive - description: - name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" - url: "https://pub.dev" - source: hosted - version: "1.2.0" - markdown: - dependency: "direct main" - description: - name: markdown - sha256: acf35edccc0463a9d7384e437c015a3535772e09714cf60e07eeef3a15870dcd - url: "https://pub.dev" - source: hosted - version: "7.1.1" - matcher: - dependency: transitive - description: - name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" - url: "https://pub.dev" - source: hosted - version: "0.12.16" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" - url: "https://pub.dev" - source: hosted - version: "0.5.0" - meta: - dependency: transitive - description: - name: meta - sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e - url: "https://pub.dev" - source: hosted - version: "1.10.0" - mime: - dependency: transitive - description: - name: mime - sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e - url: "https://pub.dev" - source: hosted - version: "1.0.4" - mobile_scanner: - dependency: "direct main" - description: - name: mobile_scanner - sha256: c3e5bba1cb626b6ab4fc46610f72a136803f6854267967e19f4a4a6a31ff9b74 - url: "https://pub.dev" - source: hosted - version: "3.5.5" - modal_bottom_sheet: - dependency: "direct main" - description: - name: modal_bottom_sheet - sha256: "3bba63c62d35c931bce7f8ae23a47f9a05836d8cb3c11122ada64e0b2f3d718f" - url: "https://pub.dev" - source: hosted - version: "3.0.0-pre" - nanoid: - dependency: transitive - description: - name: nanoid - sha256: be3f8752d9046c825df2f3914195151eb876f3ad64b9d833dd0b799b77b8759e - url: "https://pub.dev" - source: hosted - version: "1.0.0" - nested: - dependency: transitive - description: - name: nested - sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - nm: - dependency: transitive - description: - name: nm - sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" - url: "https://pub.dev" - source: hosted - version: "0.5.0" - node_preamble: - dependency: transitive - description: - name: node_preamble - sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" - url: "https://pub.dev" - source: hosted - version: "2.0.2" - octo_image: - dependency: transitive - description: - name: octo_image - sha256: "45b40f99622f11901238e18d48f5f12ea36426d8eced9f4cbf58479c7aa2430d" - url: "https://pub.dev" - source: hosted - version: "2.0.0" - open_file: - dependency: "direct main" - description: - name: open_file - sha256: a5a32d44acb7c899987d0999e1e3cbb0a0f1adebbf41ac813ec6d2d8faa0af20 - url: "https://pub.dev" - source: hosted - version: "3.3.2" - package_config: - dependency: transitive - description: - name: package_config - sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" - url: "https://pub.dev" - source: hosted - version: "2.1.0" - package_info_plus: - dependency: "direct main" - description: - name: package_info_plus - sha256: "88bc797f44a94814f2213db1c9bd5badebafdfb8290ca9f78d4b9ee2a3db4d79" - url: "https://pub.dev" - source: hosted - version: "5.0.1" - package_info_plus_platform_interface: - dependency: transitive - description: - name: package_info_plus_platform_interface - sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6" - url: "https://pub.dev" - source: hosted - version: "2.0.1" - path: - dependency: "direct main" - description: - name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" - url: "https://pub.dev" - source: hosted - version: "1.8.3" - path_parsing: - dependency: transitive - description: - name: path_parsing - sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf - url: "https://pub.dev" - source: hosted - version: "1.0.1" - path_provider: - dependency: "direct main" - description: - name: path_provider - sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa - url: "https://pub.dev" - source: hosted - version: "2.1.1" - path_provider_android: - dependency: transitive - description: - name: path_provider_android - sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72 - url: "https://pub.dev" - source: hosted - version: "2.2.1" - path_provider_foundation: - dependency: transitive - description: - name: path_provider_foundation - sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" - url: "https://pub.dev" - source: hosted - version: "2.3.1" - path_provider_linux: - dependency: transitive - description: - name: path_provider_linux - sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 - url: "https://pub.dev" - source: hosted - version: "2.2.1" - path_provider_platform_interface: - dependency: transitive - description: - name: path_provider_platform_interface - sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - path_provider_windows: - dependency: transitive - description: - name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" - url: "https://pub.dev" - source: hosted - version: "2.2.1" - permission_handler: - dependency: "direct main" - description: - name: permission_handler - sha256: "860c6b871c94c78e202dc69546d4d8fd84bd59faeb36f8fb9888668a53ff4f78" - url: "https://pub.dev" - source: hosted - version: "11.1.0" - permission_handler_android: - dependency: transitive - description: - name: permission_handler_android - sha256: "2f1bec180ee2f5665c22faada971a8f024761f632e93ddc23310487df52dcfa6" - url: "https://pub.dev" - source: hosted - version: "12.0.1" - permission_handler_apple: - dependency: transitive - description: - name: permission_handler_apple - sha256: "1a816084338ada8d574b1cb48390e6e8b19305d5120fe3a37c98825bacc78306" - url: "https://pub.dev" - source: hosted - version: "9.2.0" - permission_handler_html: - dependency: transitive - description: - name: permission_handler_html - sha256: d96ff56a757b7f04fa825c469d296c5aebc55f743e87bd639fef91a466a24da8 - url: "https://pub.dev" - source: hosted - version: "0.1.0+1" - permission_handler_platform_interface: - dependency: transitive - description: - name: permission_handler_platform_interface - sha256: d87349312f7eaf6ce0adaf668daf700ac5b06af84338bd8b8574dfbd93ffe1a1 - url: "https://pub.dev" - source: hosted - version: "4.0.2" - permission_handler_windows: - dependency: transitive - description: - name: permission_handler_windows - sha256: "1e8640c1e39121128da6b816d236e714d2cf17fac5a105dd6acdd3403a628004" - url: "https://pub.dev" - source: hosted - version: "0.2.0" - petitparser: - dependency: transitive - description: - name: petitparser - sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 - url: "https://pub.dev" - source: hosted - version: "6.0.2" - platform: - dependency: transitive - description: - name: platform - sha256: "0a279f0707af40c890e80b1e9df8bb761694c074ba7e1d4ab1bc4b728e200b59" - url: "https://pub.dev" - source: hosted - version: "3.1.3" - plugin_platform_interface: - dependency: transitive - description: - name: plugin_platform_interface - sha256: f4f88d4a900933e7267e2b353594774fc0d07fb072b47eedcd5b54e1ea3269f8 - url: "https://pub.dev" - source: hosted - version: "2.1.7" - pointycastle: - dependency: transitive - description: - name: pointycastle - sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" - url: "https://pub.dev" - source: hosted - version: "3.7.3" - pool: - dependency: transitive - description: - name: pool - sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" - url: "https://pub.dev" - source: hosted - version: "1.5.1" - pretty_qr_code: - dependency: "direct main" - description: - name: pretty_qr_code - sha256: "799fa8d5c605028302cb7debbf3f180ce56678c4927fb2ecc4b174a3bee526d6" - url: "https://pub.dev" - source: hosted - version: "3.0.0" - provider: - dependency: transitive - description: - name: provider - sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096" - url: "https://pub.dev" - source: hosted - version: "6.1.1" - pub_semver: - dependency: transitive - description: - name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - pubspec_parse: - dependency: transitive - description: - name: pubspec_parse - sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 - url: "https://pub.dev" - source: hosted - version: "1.2.3" - pull_down_button: - dependency: "direct main" - description: - name: pull_down_button - sha256: "235b302701ce029fd9e9470975069376a6700935bb47a5f1b3ec8a5efba07e6f" - url: "https://pub.dev" - source: hosted - version: "0.9.3" - qr: - dependency: transitive - description: - name: qr - sha256: "64957a3930367bf97cc211a5af99551d630f2f4625e38af10edd6b19131b64b3" - url: "https://pub.dev" - source: hosted - version: "3.0.1" - qr_flutter: - dependency: "direct main" - description: - name: qr_flutter - sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097" - url: "https://pub.dev" - source: hosted - version: "4.1.0" - quick_actions: - dependency: "direct main" - description: - name: quick_actions - sha256: "3930e1cf78a0574495b4ea741ee197323c4a9081321d6ae384b3bfcd84c7ea83" - url: "https://pub.dev" - source: hosted - version: "1.0.6" - quick_actions_android: - dependency: transitive - description: - name: quick_actions_android - sha256: df67c20583e05f5038a24c47bfa1b7b2977703ec2d162663017c5f9ef8707699 - url: "https://pub.dev" - source: hosted - version: "1.0.9" - quick_actions_ios: - dependency: transitive - description: - name: quick_actions_ios - sha256: "5a13ed27b6254184fdd4294e100e3172fa6ebfd8bea03e414634a0f760d49997" - url: "https://pub.dev" - source: hosted - version: "1.0.8" - quick_actions_platform_interface: - dependency: transitive - description: - name: quick_actions_platform_interface - sha256: d2a8566b56eec49f93934528b62033906199c60f4ffaef0cba9ef02fcfed8a81 - url: "https://pub.dev" - source: hosted - version: "1.0.5" - rettulf: - dependency: "direct main" - description: - name: rettulf - sha256: "55fa6eadb2bc2e265d4034efc022687192a8b60df8d58318c38eb74d37e7e393" - url: "https://pub.dev" - source: hosted - version: "2.0.1" - riverpod: - dependency: transitive - description: - name: riverpod - sha256: "942999ee48b899f8a46a860f1e13cee36f2f77609eb54c5b7a669bb20d550b11" - url: "https://pub.dev" - source: hosted - version: "2.4.9" - rxdart: - dependency: transitive - description: - name: rxdart - sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" - url: "https://pub.dev" - source: hosted - version: "0.27.7" - sanitize_filename: - dependency: "direct main" - description: - name: sanitize_filename - sha256: "92fc3859d86ca4d087305a52c6823a2f323fa0935b1d8d17e05e00f80b027b03" - url: "https://pub.dev" - source: hosted - version: "1.0.5" - screen_retriever: - dependency: transitive - description: - name: screen_retriever - sha256: "6ee02c8a1158e6dae7ca430da79436e3b1c9563c8cf02f524af997c201ac2b90" - url: "https://pub.dev" - source: hosted - version: "0.1.9" - screenshot: - dependency: "direct main" - description: - name: screenshot - sha256: "455284ff1f5b911d94a43c25e1385485cf6b4f288293eba68f15dad711c7b81c" - url: "https://pub.dev" - source: hosted - version: "2.1.0" - share_plus: - dependency: "direct main" - description: - name: share_plus - sha256: f74fc3f1cbd99f39760182e176802f693fa0ec9625c045561cfad54681ea93dd - url: "https://pub.dev" - source: hosted - version: "7.2.1" - share_plus_platform_interface: - dependency: transitive - description: - name: share_plus_platform_interface - sha256: df08bc3a07d01f5ea47b45d03ffcba1fa9cd5370fb44b3f38c70e42cced0f956 - url: "https://pub.dev" - source: hosted - version: "3.3.1" - shared_preferences: - dependency: "direct main" - description: - name: shared_preferences - sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" - url: "https://pub.dev" - source: hosted - version: "2.2.2" - shared_preferences_android: - dependency: transitive - description: - name: shared_preferences_android - sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" - url: "https://pub.dev" - source: hosted - version: "2.2.1" - shared_preferences_foundation: - dependency: transitive - description: - name: shared_preferences_foundation - sha256: "7bf53a9f2d007329ee6f3df7268fd498f8373602f943c975598bbb34649b62a7" - url: "https://pub.dev" - source: hosted - version: "2.3.4" - shared_preferences_linux: - dependency: transitive - description: - name: shared_preferences_linux - sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" - url: "https://pub.dev" - source: hosted - version: "2.3.2" - shared_preferences_platform_interface: - dependency: transitive - description: - name: shared_preferences_platform_interface - sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a - url: "https://pub.dev" - source: hosted - version: "2.3.1" - shared_preferences_web: - dependency: transitive - description: - name: shared_preferences_web - sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21" - url: "https://pub.dev" - source: hosted - version: "2.2.2" - shared_preferences_windows: - dependency: transitive - description: - name: shared_preferences_windows - sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" - url: "https://pub.dev" - source: hosted - version: "2.3.2" - shelf: - dependency: transitive - description: - name: shelf - sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 - url: "https://pub.dev" - source: hosted - version: "1.4.1" - shelf_packages_handler: - dependency: transitive - description: - name: shelf_packages_handler - sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - shelf_static: - dependency: transitive - description: - name: shelf_static - sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e - url: "https://pub.dev" - source: hosted - version: "1.1.2" - shelf_web_socket: - dependency: transitive - description: - name: shelf_web_socket - sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" - url: "https://pub.dev" - source: hosted - version: "1.0.4" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.99" - sliver_tools: - dependency: "direct main" - description: - name: sliver_tools - sha256: eae28220badfb9d0559207badcbbc9ad5331aac829a88cb0964d330d2a4636a6 - url: "https://pub.dev" - source: hosted - version: "0.2.12" - source_gen: - dependency: transitive - description: - name: source_gen - sha256: fc0da689e5302edb6177fdd964efcb7f58912f43c28c2047a808f5bfff643d16 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - source_helper: - dependency: transitive - description: - name: source_helper - sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd" - url: "https://pub.dev" - source: hosted - version: "1.3.4" - source_map_stack_trace: - dependency: transitive - description: - name: source_map_stack_trace - sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - source_maps: - dependency: transitive - description: - name: source_maps - sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" - url: "https://pub.dev" - source: hosted - version: "0.10.12" - source_span: - dependency: transitive - description: - name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" - url: "https://pub.dev" - source: hosted - version: "1.10.0" - sprintf: - dependency: transitive - description: - name: sprintf - sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" - url: "https://pub.dev" - source: hosted - version: "7.0.0" - sqflite: - dependency: transitive - description: - name: sqflite - sha256: "591f1602816e9c31377d5f008c2d9ef7b8aca8941c3f89cc5fd9d84da0c38a9a" - url: "https://pub.dev" - source: hosted - version: "2.3.0" - sqflite_common: - dependency: transitive - description: - name: sqflite_common - sha256: bb4738f15b23352822f4c42a531677e5c6f522e079461fd240ead29d8d8a54a6 - url: "https://pub.dev" - source: hosted - version: "2.5.0+2" - stack_trace: - dependency: "direct main" - description: - name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" - url: "https://pub.dev" - source: hosted - version: "1.11.1" - state_notifier: - dependency: transitive - description: - name: state_notifier - sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb - url: "https://pub.dev" - source: hosted - version: "1.0.0" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 - url: "https://pub.dev" - source: hosted - version: "2.1.2" - stream_transform: - dependency: transitive - description: - name: stream_transform - sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" - url: "https://pub.dev" - source: hosted - version: "2.1.0" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" - url: "https://pub.dev" - source: hosted - version: "1.2.0" - synchronized: - dependency: "direct main" - description: - name: synchronized - sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60" - url: "https://pub.dev" - source: hosted - version: "3.1.0" - system_theme: - dependency: "direct main" - description: - name: system_theme - sha256: "1f208db140a3d1e1eac2034b54920d95699c1534df576ced44b3312c5de3975f" - url: "https://pub.dev" - source: hosted - version: "2.3.1" - system_theme_web: - dependency: transitive - description: - name: system_theme_web - sha256: "7566f5a928f6d28d7a60c97bea8a851d1c6bc9b86a4df2366230a97458489219" - url: "https://pub.dev" - source: hosted - version: "0.0.2" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 - url: "https://pub.dev" - source: hosted - version: "1.2.1" - test: - dependency: "direct dev" - description: - name: test - sha256: a1f7595805820fcc05e5c52e3a231aedd0b72972cb333e8c738a8b1239448b6f - url: "https://pub.dev" - source: hosted - version: "1.24.9" - test_api: - dependency: transitive - description: - name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" - url: "https://pub.dev" - source: hosted - version: "0.6.1" - test_core: - dependency: transitive - description: - name: test_core - sha256: a757b14fc47507060a162cc2530d9a4a2f92f5100a952c7443b5cad5ef5b106a - url: "https://pub.dev" - source: hosted - version: "0.5.9" - text_scroll: - dependency: "direct main" - description: - name: text_scroll - sha256: "7869d86a6fdd725dee56bdd150216a99f0372b82fbfcac319214dbd5f36e1908" - url: "https://pub.dev" - source: hosted - version: "0.2.0" - timing: - dependency: transitive - description: - name: timing - sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" - url: "https://pub.dev" - source: hosted - version: "1.0.1" - typed_data: - dependency: transitive - description: - name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c - url: "https://pub.dev" - source: hosted - version: "1.3.2" - unicons: - dependency: "direct main" - description: - name: unicons - sha256: dbfcf93ff4d4ea19b324113857e358e4882115ab85db04417a4ba1c72b17a670 - url: "https://pub.dev" - source: hosted - version: "2.1.1" - universal_io: - dependency: transitive - description: - name: universal_io - sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" - url: "https://pub.dev" - source: hosted - version: "2.2.2" - universal_platform: - dependency: "direct main" - description: - name: universal_platform - sha256: d315be0f6641898b280ffa34e2ddb14f3d12b1a37882557869646e0cc363d0cc - url: "https://pub.dev" - source: hosted - version: "1.0.0+1" - url_launcher: - dependency: "direct main" - description: - name: url_launcher - sha256: e9aa5ea75c84cf46b3db4eea212523591211c3cf2e13099ee4ec147f54201c86 - url: "https://pub.dev" - source: hosted - version: "6.2.2" - url_launcher_android: - dependency: transitive - description: - name: url_launcher_android - sha256: "31222ffb0063171b526d3e569079cf1f8b294075ba323443fdc690842bfd4def" - url: "https://pub.dev" - source: hosted - version: "6.2.0" - url_launcher_ios: - dependency: transitive - description: - name: url_launcher_ios - sha256: bba3373219b7abb6b5e0d071b0fe66dfbe005d07517a68e38d4fc3638f35c6d3 - url: "https://pub.dev" - source: hosted - version: "6.2.1" - url_launcher_linux: - dependency: transitive - description: - name: url_launcher_linux - sha256: "9f2d390e096fdbe1e6e6256f97851e51afc2d9c423d3432f1d6a02a8a9a8b9fd" - url: "https://pub.dev" - source: hosted - version: "3.1.0" - url_launcher_macos: - dependency: transitive - description: - name: url_launcher_macos - sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 - url: "https://pub.dev" - source: hosted - version: "3.1.0" - url_launcher_platform_interface: - dependency: transitive - description: - name: url_launcher_platform_interface - sha256: "980e8d9af422f477be6948bdfb68df8433be71f5743a188968b0c1b887807e50" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - url_launcher_web: - dependency: transitive - description: - name: url_launcher_web - sha256: "138bd45b3a456dcfafc46d1a146787424f8d2edfbf2809c9324361e58f851cf7" - url: "https://pub.dev" - source: hosted - version: "2.2.1" - url_launcher_windows: - dependency: transitive - description: - name: url_launcher_windows - sha256: "7754a1ad30ee896b265f8d14078b0513a4dba28d358eabb9d5f339886f4a1adc" - url: "https://pub.dev" - source: hosted - version: "3.1.0" - uuid: - dependency: "direct main" - description: - name: uuid - sha256: df5a4d8f22ee4ccd77f8839ac7cb274ebc11ef9adcce8b92be14b797fe889921 - url: "https://pub.dev" - source: hosted - version: "4.2.1" - vector_graphics: - dependency: transitive - description: - name: vector_graphics - sha256: "0f0c746dd2d6254a0057218ff980fc7f5670fd0fcf5e4db38a490d31eed4ad43" - url: "https://pub.dev" - source: hosted - version: "1.1.9+1" - vector_graphics_codec: - dependency: transitive - description: - name: vector_graphics_codec - sha256: "0edf6d630d1bfd5589114138ed8fada3234deacc37966bec033d3047c29248b7" - url: "https://pub.dev" - source: hosted - version: "1.1.9+1" - vector_graphics_compiler: - dependency: transitive - description: - name: vector_graphics_compiler - sha256: d24333727332d9bd20990f1483af4e09abdb9b1fc7c3db940b56ab5c42790c26 - url: "https://pub.dev" - source: hosted - version: "1.1.9+1" - vector_math: - dependency: transitive - description: - name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - version: - dependency: "direct main" - description: - name: version - sha256: "3d4140128e6ea10d83da32fef2fa4003fccbf6852217bb854845802f04191f94" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - vibration: - dependency: "direct main" - description: - name: vibration - sha256: "778ace40e84852e6cf6017cdbaf6790a837d73ff3dd50b27da9ac232a19de8fc" - url: "https://pub.dev" - source: hosted - version: "1.8.4" - video_player: - dependency: transitive - description: - name: video_player - sha256: e16f0a83601a78d165dabc17e4dac50997604eb9e4cc76e10fa219046b70cef3 - url: "https://pub.dev" - source: hosted - version: "2.8.1" - video_player_android: - dependency: transitive - description: - name: video_player_android - sha256: "3fe89ab07fdbce786e7eb25b58532d6eaf189ceddc091cb66cba712f8d9e8e55" - url: "https://pub.dev" - source: hosted - version: "2.4.10" - video_player_avfoundation: - dependency: transitive - description: - name: video_player_avfoundation - sha256: bc923884640d6dc403050586eb40713cdb8d1d84e6886d8aca50ab04c59124c2 - url: "https://pub.dev" - source: hosted - version: "2.5.2" - video_player_platform_interface: - dependency: transitive - description: - name: video_player_platform_interface - sha256: be72301bf2c0150ab35a8c34d66e5a99de525f6de1e8d27c0672b836fe48f73a - url: "https://pub.dev" - source: hosted - version: "6.2.1" - video_player_web: - dependency: transitive - description: - name: video_player_web - sha256: ab7a462b07d9ca80bed579e30fb3bce372468f1b78642e0911b10600f2c5cb5b - url: "https://pub.dev" - source: hosted - version: "2.1.2" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 - url: "https://pub.dev" - source: hosted - version: "13.0.0" - wakelock_plus: - dependency: transitive - description: - name: wakelock_plus - sha256: f268ca2116db22e57577fb99d52515a24bdc1d570f12ac18bb762361d43b043d - url: "https://pub.dev" - source: hosted - version: "1.1.4" - wakelock_plus_platform_interface: - dependency: transitive - description: - name: wakelock_plus_platform_interface - sha256: "40fabed5da06caff0796dc638e1f07ee395fb18801fbff3255a2372db2d80385" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - watcher: - dependency: transitive - description: - name: watcher - sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - web: - dependency: transitive - description: - name: web - sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 - url: "https://pub.dev" - source: hosted - version: "0.3.0" - web_socket_channel: - dependency: transitive - description: - name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b - url: "https://pub.dev" - source: hosted - version: "2.4.0" - webkit_inspection_protocol: - dependency: transitive - description: - name: webkit_inspection_protocol - sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" - url: "https://pub.dev" - source: hosted - version: "1.2.1" - webview_flutter: - dependency: "direct main" - description: - name: webview_flutter - sha256: "42393b4492e629aa3a88618530a4a00de8bb46e50e7b3993fedbfdc5352f0dbf" - url: "https://pub.dev" - source: hosted - version: "4.4.2" - webview_flutter_android: - dependency: transitive - description: - name: webview_flutter_android - sha256: "8326ee235f87605a2bfc444a4abc897f4abc78d83f054ba7d3d1074ce82b4fbf" - url: "https://pub.dev" - source: hosted - version: "3.12.1" - webview_flutter_platform_interface: - dependency: transitive - description: - name: webview_flutter_platform_interface - sha256: "68e86162aa8fc646ae859e1585995c096c95fc2476881fa0c4a8d10f56013a5a" - url: "https://pub.dev" - source: hosted - version: "2.8.0" - webview_flutter_wkwebview: - dependency: transitive - description: - name: webview_flutter_wkwebview - sha256: accdaaa49a2aca2dc3c3230907988954cdd23fed0a19525d6c9789d380f4dc76 - url: "https://pub.dev" - source: hosted - version: "3.9.4" - win32: - dependency: transitive - description: - name: win32 - sha256: b0f37db61ba2f2e9b7a78a1caece0052564d1bc70668156cf3a29d676fe4e574 - url: "https://pub.dev" - source: hosted - version: "5.1.1" - win32_registry: - dependency: "direct main" - description: - name: win32_registry - sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a" - url: "https://pub.dev" - source: hosted - version: "1.1.2" - window_manager: - dependency: "direct main" - description: - name: window_manager - sha256: dcc865277f26a7dad263a47d0e405d77e21f12cb71f30333a52710a408690bd7 - url: "https://pub.dev" - source: hosted - version: "0.3.7" - xdg_directories: - dependency: transitive - description: - name: xdg_directories - sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2" - url: "https://pub.dev" - source: hosted - version: "1.0.3" - xml: - dependency: transitive - description: - name: xml - sha256: af5e77e9b83f2f4adc5d3f0a4ece1c7f45a2467b695c2540381bac793e34e556 - url: "https://pub.dev" - source: hosted - version: "6.4.2" - yaml: - dependency: "direct main" - description: - name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" - url: "https://pub.dev" - source: hosted - version: "3.1.2" -sdks: - dart: ">=3.2.2 <4.0.0" - flutter: ">=3.16.3" diff --git a/pubspec.yaml b/pubspec.yaml deleted file mode 100644 index 8b2daceab..000000000 --- a/pubspec.yaml +++ /dev/null @@ -1,176 +0,0 @@ -name: sit -description: "A multiplatform app for SIT students." - -# The build version numbers is incremented automatically. -# DO NOT DIRECTLY CHANGE IT -version: 2.0.0+14 - -homepage: https://github.com/liplum/mimir -repository: https://github.com/liplum/mimir -issue_tracker: https://github.com/liplum/mimir/issues -documentation: https://github.com/liplum/mimir#readme - -publish_to: none -environment: { sdk: '>=3.2.2 <4.0.0', flutter: ^3.16.3 } - -dependencies: - flutter: { sdk: flutter } - - # I18n - easy_localization: ^3.0.3 - locale_names: ^1.1.1 - intl: ^0.17.0 - - # Basic - logger: ^2.0.2 - device_info_plus: ^9.1.0 - event_bus: ^2.0.0 - path_provider: ^2.1.1 - version: ^3.0.2 - yaml: ^3.1.2 - path: ^1.8.3 - collection: ^1.18.0 - shared_preferences: ^2.2.2 - flutter_riverpod: ^2.4.9 - hive: ^2.2.3 - hive_flutter: ^1.1.0 - json_serializable: ^6.7.1 - json_annotation: ^4.8.1 - copy_with_extension: ^5.0.4 - - # String formatting - format: ^1.4.0 - - # Cryptography - # Encryption (AES) - encrypt: ^5.0.1 - # Hash (MD5) - crypto: ^3.0.3 - # UUID generator - uuid: ^4.2.1 - - # HTML parser - beautiful_soup_dart: ^0.3.0 - html: ^0.15.4 - - # Dio (http client) - dio: ^5.4.0 - dio_cookie_manager: ^3.1.1 - - # WebView and browser related - webview_flutter: ^4.4.2 - fk_user_agent: ^2.1.0 - flutter_widget_from_html: ^0.14.9 - chewie: ^1.7.4 - cookie_jar: ^4.0.8 - flutter_html: ^3.0.0-beta.2 - - # Email - enough_mail: ^2.1.6 - enough_mail_html: ^2.0.1 - - # Platform - # Android / iOS / Windows Permission - permission_handler: ^11.1.0 - # Android / iOS Home screen quick actions - quick_actions: ^1.0.6 - # Deep links on Android and custom scheme links on iOS - app_links: ^3.5.0 - # Open with other APP/programs - open_file: ^3.3.2 - url_launcher: ^6.2.2 - # Open Android / iOS system image picker - image_picker: ^1.0.5 - file_picker: ^6.0.0 - share_plus: ^7.2.1 - app_settings: ^5.1.1 - # Desktop support - window_manager: ^0.3.7 - win32_registry: ^1.1.2 - # Get package info (version) - package_info_plus: ^5.0.1 - # Check VPN connection status - check_vpn_connection: ^0.0.2 - connectivity_plus: ^5.0.2 - vibration: ^1.8.4 - # qrcode scanner - mobile_scanner: ^3.5.5 - add_2_calendar: ^3.0.1 - - # UI - go_router: ^12.1.3 - fl_chart: ^0.65.0 - flutter_svg: ^2.0.9 - flutter_svg_provider: ^1.0.6 - # Screen adaptation - flutter_screenutil: ^5.9.0 - flutter_adaptive_ui: ^0.8.0+1 - # Qr code generate - qr_flutter: ^4.1.0 - pretty_qr_code: ^3.0.0 - cached_network_image: ^3.3.0 - modal_bottom_sheet: ^3.0.0-pre - auto_size_text: ^3.0.0 - text_scroll: ^0.2.0 - markdown: ^7.1.1 - animations: ^2.0.10 - dynamic_color: ^1.6.8 - unicons: ^2.1.1 - sliver_tools: ^0.2.12 - flutter_staggered_grid_view: ^0.7.0 - flex_color_picker: ^3.3.0 - flutter_swipe_action_cell: ^3.1.3 - bordered_text: ^2.0.0 - flutter_platform_widgets: ^6.0.2 - pull_down_button: ^0.9.3 - system_theme: ^2.3.1 # read system theme color - cupertino_onboarding: ^1.2.0 - flutter_swipe_detector: ^2.0.0 - # iCalendar file generator - ical: ^0.2.2 - - # Utils - # dart.io.Platform API for Web - universal_platform: ^1.0.0+1 - rettulf: ^2.0.1 - # lock - synchronized: ^3.1.0 - # parse stacktrace - stack_trace: ^1.11.0 - email_validator: ^2.1.17 - sanitize_filename: ^1.0.5 - # Take screenshot of flutter widgets - screenshot: ^2.1.0 - -dependency_overrides: - intl: ^0.19.0 - -dev_dependencies: - flutter_test: { sdk: flutter } - flutter_lints: ^3.0.1 - build_runner: ^2.4.7 - hive_generator: ^2.0.0 - copy_with_extension_gen: ^5.0.4 - test: ^1.24.9 - -# ------------------------------------------------------------------------------ - -flutter: - - uses-material-design: true - fonts: - # Iconfont for ywb.sit.edu.cn - - family: ywb_iconfont - fonts: [ { asset: assets/fonts/ywb_iconfont.ttf } ] - - assets: - - assets/ - - assets/fonts/ - - assets/course/ - - assets/yellow_pages.json - - assets/room_list.json - - assets/webview/ - - assets/l10n/ - -flutter_intl: - enabled: true diff --git a/run_build_runner.bat b/run_build_runner.bat deleted file mode 100644 index acb214db3..000000000 --- a/run_build_runner.bat +++ /dev/null @@ -1 +0,0 @@ -flutter pub run build_runner build --delete-conflicting-outputs \ No newline at end of file diff --git a/run_build_runner.sh b/run_build_runner.sh deleted file mode 100644 index 46a69f7da..000000000 --- a/run_build_runner.sh +++ /dev/null @@ -1 +0,0 @@ -flutter pub run build_runner build --delete-conflicting-outputs && dart format . -l 120 diff --git a/specifications/ABBREVIATION.md b/specifications/ABBREVIATION.md deleted file mode 100644 index 8981506d9..000000000 --- a/specifications/ABBREVIATION.md +++ /dev/null @@ -1,31 +0,0 @@ -# Abbreviations - -This chart is for shortening the names of both variables and Json. - -## General - -| Original | Abbreviation | -|---------------|--------------| -| password | pwd | -| exception | xcp | -| information | info | -| for | 4 | -| to | 2 | -| identity | id | -| description | desc | -| statistics | stats | -| different(ce) | diff | -| evaluate(ion) | eval | - - -## Only for I18n Naming - -| Original | Abbreviation | -|---------------|--------------| -| telephone | tel | -| subtitle | sub | -| button | btn | -| electricity | elec | -| arrangement | arr | -| function | func | -| category(ies) | cat(s) | diff --git a/specifications/CONTRIBUTION_GUIDE.md b/specifications/CONTRIBUTION_GUIDE.md deleted file mode 100644 index bd89c0f4a..000000000 --- a/specifications/CONTRIBUTION_GUIDE.md +++ /dev/null @@ -1,97 +0,0 @@ -# Contribution Guide - -Please also check: - -- [The terms](TERM.md) -- [The project structure](STRUCTURE.MD) -- [The abbreviations](ABBREVIATION.md) -- [The internationalization protocol](I18N_PROTOCOL.md) - -## Getting Started - -Clone the repository to a local folder. -Note: you have to put it in a folder named as `mimir`. - -``` shell -git clone https://github.com/Liplum/Mimir mimir -``` - -Then run the necessary build steps. - -``` shell -flutter pub get -flutter pub run build_runner build -``` - -Finally, build the SIT Life based on your platform. - -```shell -# For Android -flutter build apk # build for Android -# For Windows -flutter build winodws # build for Windows -# For macOS -flutter build macos # build for macOS -flutter build ios # build for iOS -# For Linux -flutter build linux # build for Linux -``` - -### iOS Build - -SIT Life for iOS requires `Xcode 13.4.1`, the latest Xcode 13. -You can download it [here](https://developer.apple.com/download/all/?q=Xcode%2013.4.1). - -Be aware that Xcode 14 or higher doesn't work due to the compatibility issue of some dependencies. - -## Dependency - -## Code Style - -### Dart - -As to formatting, please follow what `dart format` does. -The dedicated configuration for Kite is `line length: 120`. -You can run the command below to format the whole project by this principle. - -```shell -dart format . -l 120 -``` - -As to naming principle, please follow -the [official naming convention](https://dart.dev/guides/language/effective-dart/style). - -To be flexible and easy to reconstruct, -`relative import` should be applied, meanwhile, `absolute import` should be applied outside. - -### Json - -As to formatting, the indent is 2 spaces. - -As to naming, please keep the key `lowerCamelCase`, -which can be mapped to a valid dart variable name. - -### Build Tool - -Build tool always works on the latest python. -Requirements: - -``` -ruamel.yaml -#IF Windows - pywin32 -#ELSE - curses -#ENDIF -``` - -The [entry point](/tool/main.py) is located in [tool folder](/tool). - -If the current working directory is [the root of project](..). - -```shell -python ./tool/main.py -``` - -Build tool will locate the project automatically, -so you can run the [main.py](/tool/main.py) anywhere. diff --git a/static/.DS_Store b/static/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..0142a7863d87bbede4ed69876d4fd9247152b829 GIT binary patch literal 6148 zcmeHK%}T>S5Z<-brW7Fug&r5Y7Hq36ikA@U3mDOZN=-&j_vkp)jJz@#K~~5?ugZSzuOU~ zgZ_Hmv<{DsFRsTg@hp*VnobUEE7><#!aFEtHLu8CNsIIA-SZ@xs;Y;1?>LaYrNd!~ij{$v{n;HlF`i@XJ&_@;6J! zA_jkGzo!i!x{HkLBT6E1*3=L&3ZP6%f!jE&*WRKGIe}9T%uWo@21kh@+ri Rl>^d6KoLS6G4Klvd;x&UNpb)H literal 0 HcmV?d00001 diff --git a/static/bootstrap-4.4.1.min.css b/static/bootstrap-4.4.1.min.css new file mode 100644 index 000000000..86b6845bc --- /dev/null +++ b/static/bootstrap-4.4.1.min.css @@ -0,0 +1,7 @@ +/*! + * Bootstrap v4.4.1 (https://getbootstrap.com/) + * Copyright 2011-2019 The Bootstrap Authors + * Copyright 2011-2019 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */:root{--blue:#007bff;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#dc3545;--orange:#fd7e14;--yellow:#ffc107;--green:#28a745;--teal:#20c997;--cyan:#17a2b8;--white:#fff;--gray:#6c757d;--gray-dark:#343a40;--primary:#007bff;--secondary:#6c757d;--success:#28a745;--info:#17a2b8;--warning:#ffc107;--danger:#dc3545;--light:#f8f9fa;--dark:#343a40;--breakpoint-xs:0;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--font-family-sans-serif:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]){color:inherit;text-decoration:none}a:not([href]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,.1)}.small,small{font-size:80%;font-weight:400}.mark,mark{padding:.2em;background-color:#fcf8e3}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote-footer{display:block;font-size:80%;color:#6c757d}.blockquote-footer::before{content:"\2014\00A0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:90%;color:#6c757d}code{font-size:87.5%;color:#e83e8c;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:87.5%;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:87.5%;color:#212529}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container{max-width:540px}}@media (min-width:768px){.container{max-width:720px}}@media (min-width:992px){.container{max-width:960px}}@media (min-width:1200px){.container{max-width:1140px}}.container-fluid,.container-lg,.container-md,.container-sm,.container-xl{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}.row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-auto,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{position:relative;width:100%;padding-right:15px;padding-left:15px}.col{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-first{-ms-flex-order:-1;order:-1}.order-last{-ms-flex-order:13;order:13}.order-0{-ms-flex-order:0;order:0}.order-1{-ms-flex-order:1;order:1}.order-2{-ms-flex-order:2;order:2}.order-3{-ms-flex-order:3;order:3}.order-4{-ms-flex-order:4;order:4}.order-5{-ms-flex-order:5;order:5}.order-6{-ms-flex-order:6;order:6}.order-7{-ms-flex-order:7;order:7}.order-8{-ms-flex-order:8;order:8}.order-9{-ms-flex-order:9;order:9}.order-10{-ms-flex-order:10;order:10}.order-11{-ms-flex-order:11;order:11}.order-12{-ms-flex-order:12;order:12}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width:576px){.col-sm{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-sm-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-sm-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-sm-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-sm-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-sm-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-sm-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-sm-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-sm-first{-ms-flex-order:-1;order:-1}.order-sm-last{-ms-flex-order:13;order:13}.order-sm-0{-ms-flex-order:0;order:0}.order-sm-1{-ms-flex-order:1;order:1}.order-sm-2{-ms-flex-order:2;order:2}.order-sm-3{-ms-flex-order:3;order:3}.order-sm-4{-ms-flex-order:4;order:4}.order-sm-5{-ms-flex-order:5;order:5}.order-sm-6{-ms-flex-order:6;order:6}.order-sm-7{-ms-flex-order:7;order:7}.order-sm-8{-ms-flex-order:8;order:8}.order-sm-9{-ms-flex-order:9;order:9}.order-sm-10{-ms-flex-order:10;order:10}.order-sm-11{-ms-flex-order:11;order:11}.order-sm-12{-ms-flex-order:12;order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width:768px){.col-md{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-md-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-md-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-md-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-md-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-md-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-md-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-md-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-md-first{-ms-flex-order:-1;order:-1}.order-md-last{-ms-flex-order:13;order:13}.order-md-0{-ms-flex-order:0;order:0}.order-md-1{-ms-flex-order:1;order:1}.order-md-2{-ms-flex-order:2;order:2}.order-md-3{-ms-flex-order:3;order:3}.order-md-4{-ms-flex-order:4;order:4}.order-md-5{-ms-flex-order:5;order:5}.order-md-6{-ms-flex-order:6;order:6}.order-md-7{-ms-flex-order:7;order:7}.order-md-8{-ms-flex-order:8;order:8}.order-md-9{-ms-flex-order:9;order:9}.order-md-10{-ms-flex-order:10;order:10}.order-md-11{-ms-flex-order:11;order:11}.order-md-12{-ms-flex-order:12;order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width:992px){.col-lg{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-lg-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-lg-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-lg-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-lg-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-lg-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-lg-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-lg-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-lg-first{-ms-flex-order:-1;order:-1}.order-lg-last{-ms-flex-order:13;order:13}.order-lg-0{-ms-flex-order:0;order:0}.order-lg-1{-ms-flex-order:1;order:1}.order-lg-2{-ms-flex-order:2;order:2}.order-lg-3{-ms-flex-order:3;order:3}.order-lg-4{-ms-flex-order:4;order:4}.order-lg-5{-ms-flex-order:5;order:5}.order-lg-6{-ms-flex-order:6;order:6}.order-lg-7{-ms-flex-order:7;order:7}.order-lg-8{-ms-flex-order:8;order:8}.order-lg-9{-ms-flex-order:9;order:9}.order-lg-10{-ms-flex-order:10;order:10}.order-lg-11{-ms-flex-order:11;order:11}.order-lg-12{-ms-flex-order:12;order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width:1200px){.col-xl{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-xl-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-xl-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-xl-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-xl-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-xl-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-xl-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-xl-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-xl-first{-ms-flex-order:-1;order:-1}.order-xl-last{-ms-flex-order:13;order:13}.order-xl-0{-ms-flex-order:0;order:0}.order-xl-1{-ms-flex-order:1;order:1}.order-xl-2{-ms-flex-order:2;order:2}.order-xl-3{-ms-flex-order:3;order:3}.order-xl-4{-ms-flex-order:4;order:4}.order-xl-5{-ms-flex-order:5;order:5}.order-xl-6{-ms-flex-order:6;order:6}.order-xl-7{-ms-flex-order:7;order:7}.order-xl-8{-ms-flex-order:8;order:8}.order-xl-9{-ms-flex-order:9;order:9}.order-xl-10{-ms-flex-order:10;order:10}.order-xl-11{-ms-flex-order:11;order:11}.order-xl-12{-ms-flex-order:12;order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.table{width:100%;margin-bottom:1rem;color:#212529}.table td,.table th{padding:.75rem;vertical-align:top;border-top:1px solid #dee2e6}.table thead th{vertical-align:bottom;border-bottom:2px solid #dee2e6}.table tbody+tbody{border-top:2px solid #dee2e6}.table-sm td,.table-sm th{padding:.3rem}.table-bordered{border:1px solid #dee2e6}.table-bordered td,.table-bordered th{border:1px solid #dee2e6}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-borderless tbody+tbody,.table-borderless td,.table-borderless th,.table-borderless thead th{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,.05)}.table-hover tbody tr:hover{color:#212529;background-color:rgba(0,0,0,.075)}.table-primary,.table-primary>td,.table-primary>th{background-color:#b8daff}.table-primary tbody+tbody,.table-primary td,.table-primary th,.table-primary thead th{border-color:#7abaff}.table-hover .table-primary:hover{background-color:#9fcdff}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#9fcdff}.table-secondary,.table-secondary>td,.table-secondary>th{background-color:#d6d8db}.table-secondary tbody+tbody,.table-secondary td,.table-secondary th,.table-secondary thead th{border-color:#b3b7bb}.table-hover .table-secondary:hover{background-color:#c8cbcf}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#c8cbcf}.table-success,.table-success>td,.table-success>th{background-color:#c3e6cb}.table-success tbody+tbody,.table-success td,.table-success th,.table-success thead th{border-color:#8fd19e}.table-hover .table-success:hover{background-color:#b1dfbb}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#b1dfbb}.table-info,.table-info>td,.table-info>th{background-color:#bee5eb}.table-info tbody+tbody,.table-info td,.table-info th,.table-info thead th{border-color:#86cfda}.table-hover .table-info:hover{background-color:#abdde5}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#abdde5}.table-warning,.table-warning>td,.table-warning>th{background-color:#ffeeba}.table-warning tbody+tbody,.table-warning td,.table-warning th,.table-warning thead th{border-color:#ffdf7e}.table-hover .table-warning:hover{background-color:#ffe8a1}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#ffe8a1}.table-danger,.table-danger>td,.table-danger>th{background-color:#f5c6cb}.table-danger tbody+tbody,.table-danger td,.table-danger th,.table-danger thead th{border-color:#ed969e}.table-hover .table-danger:hover{background-color:#f1b0b7}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#f1b0b7}.table-light,.table-light>td,.table-light>th{background-color:#fdfdfe}.table-light tbody+tbody,.table-light td,.table-light th,.table-light thead th{border-color:#fbfcfc}.table-hover .table-light:hover{background-color:#ececf6}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#ececf6}.table-dark,.table-dark>td,.table-dark>th{background-color:#c6c8ca}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#95999c}.table-hover .table-dark:hover{background-color:#b9bbbe}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#b9bbbe}.table-active,.table-active>td,.table-active>th{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,.075)}.table .thead-dark th{color:#fff;background-color:#343a40;border-color:#454d55}.table .thead-light th{color:#495057;background-color:#e9ecef;border-color:#dee2e6}.table-dark{color:#fff;background-color:#343a40}.table-dark td,.table-dark th,.table-dark thead th{border-color:#454d55}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,.05)}.table-dark.table-hover tbody tr:hover{color:#fff;background-color:rgba(255,255,255,.075)}@media (max-width:575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-sm>.table-bordered{border:0}}@media (max-width:767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-md>.table-bordered{border:0}}@media (max-width:991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-lg>.table-bordered{border:0}}@media (max-width:1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:-moz-focusring{color:transparent;text-shadow:0 0 0 #495057}.form-control:focus{color:#495057;background-color:#fff;border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.form-control::-webkit-input-placeholder{color:#6c757d;opacity:1}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control:-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}select.form-control:focus::-ms-value{color:#495057;background-color:#fff}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem;line-height:1.5}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;font-size:1rem;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.form-control-lg{height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}select.form-control[multiple],select.form-control[size]{height:auto}textarea.form-control{height:auto}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*=col-]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:.3rem;margin-left:-1.25rem}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{color:#6c757d}.form-check-label{margin-bottom:0}.form-check-inline{display:-ms-inline-flexbox;display:inline-flex;-ms-flex-align:center;align-items:center;padding-left:0;margin-right:.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#28a745}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(40,167,69,.9);border-radius:.25rem}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#28a745;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-valid,.was-validated .custom-select:valid{border-color:#28a745;padding-right:calc(.75em + 2.3125rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem)}.custom-select.is-valid:focus,.was-validated .custom-select:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#28a745}.form-check-input.is-valid~.valid-feedback,.form-check-input.is-valid~.valid-tooltip,.was-validated .form-check-input:valid~.valid-feedback,.was-validated .form-check-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid~.custom-control-label,.was-validated .custom-control-input:valid~.custom-control-label{color:#28a745}.custom-control-input.is-valid~.custom-control-label::before,.was-validated .custom-control-input:valid~.custom-control-label::before{border-color:#28a745}.custom-control-input.is-valid:checked~.custom-control-label::before,.was-validated .custom-control-input:valid:checked~.custom-control-label::before{border-color:#34ce57;background-color:#34ce57}.custom-control-input.is-valid:focus~.custom-control-label::before,.was-validated .custom-control-input:valid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.custom-control-input.is-valid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:valid:focus:not(:checked)~.custom-control-label::before{border-color:#28a745}.custom-file-input.is-valid~.custom-file-label,.was-validated .custom-file-input:valid~.custom-file-label{border-color:#28a745}.custom-file-input.is-valid:focus~.custom-file-label,.was-validated .custom-file-input:valid:focus~.custom-file-label{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-invalid,.was-validated .custom-select:invalid{border-color:#dc3545;padding-right:calc(.75em + 2.3125rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem)}.custom-select.is-invalid:focus,.was-validated .custom-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-input.is-invalid~.invalid-feedback,.form-check-input.is-invalid~.invalid-tooltip,.was-validated .form-check-input:invalid~.invalid-feedback,.was-validated .form-check-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid~.custom-control-label,.was-validated .custom-control-input:invalid~.custom-control-label{color:#dc3545}.custom-control-input.is-invalid~.custom-control-label::before,.was-validated .custom-control-input:invalid~.custom-control-label::before{border-color:#dc3545}.custom-control-input.is-invalid:checked~.custom-control-label::before,.was-validated .custom-control-input:invalid:checked~.custom-control-label::before{border-color:#e4606d;background-color:#e4606d}.custom-control-input.is-invalid:focus~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.custom-control-input.is-invalid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus:not(:checked)~.custom-control-label::before{border-color:#dc3545}.custom-file-input.is-invalid~.custom-file-label,.was-validated .custom-file-input:invalid~.custom-file-label{border-color:#dc3545}.custom-file-input.is-invalid:focus~.custom-file-label,.was-validated .custom-file-input:invalid:focus~.custom-file-label{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-inline{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center}.form-inline .form-check{width:100%}@media (min-width:576px){.form-inline label{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:-ms-flexbox;display:flex;-ms-flex:0 0 auto;flex:0 0 auto;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .custom-select,.form-inline .input-group{width:auto}.form-inline .form-check{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;-ms-flex-negative:0;flex-shrink:0;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;color:#212529;text-align:center;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;line-height:1.5;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529;text-decoration:none}.btn.focus,.btn:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.btn.disabled,.btn:disabled{opacity:.65}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:hover{color:#fff;background-color:#0069d9;border-color:#0062cc}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#0069d9;border-color:#0062cc;box-shadow:0 0 0 .2rem rgba(38,143,255,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:not(:disabled):not(.disabled).active,.btn-primary:not(:disabled):not(.disabled):active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0062cc;border-color:#005cbf}.btn-primary:not(:disabled):not(.disabled).active:focus,.btn-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(38,143,255,.5)}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5a6268;border-color:#545b62}.btn-secondary.focus,.btn-secondary:focus{color:#fff;background-color:#5a6268;border-color:#545b62;box-shadow:0 0 0 .2rem rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:not(:disabled):not(.disabled).active,.btn-secondary:not(:disabled):not(.disabled):active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#545b62;border-color:#4e555b}.btn-secondary:not(:disabled):not(.disabled).active:focus,.btn-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(130,138,145,.5)}.btn-success{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:hover{color:#fff;background-color:#218838;border-color:#1e7e34}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#218838;border-color:#1e7e34;box-shadow:0 0 0 .2rem rgba(72,180,97,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:not(:disabled):not(.disabled).active,.btn-success:not(:disabled):not(.disabled):active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#1e7e34;border-color:#1c7430}.btn-success:not(:disabled):not(.disabled).active:focus,.btn-success:not(:disabled):not(.disabled):active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(72,180,97,.5)}.btn-info{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:hover{color:#fff;background-color:#138496;border-color:#117a8b}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#138496;border-color:#117a8b;box-shadow:0 0 0 .2rem rgba(58,176,195,.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:not(:disabled):not(.disabled).active,.btn-info:not(:disabled):not(.disabled):active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#117a8b;border-color:#10707f}.btn-info:not(:disabled):not(.disabled).active:focus,.btn-info:not(:disabled):not(.disabled):active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(58,176,195,.5)}.btn-warning{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#212529;background-color:#e0a800;border-color:#d39e00}.btn-warning.focus,.btn-warning:focus{color:#212529;background-color:#e0a800;border-color:#d39e00;box-shadow:0 0 0 .2rem rgba(222,170,12,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:not(:disabled):not(.disabled).active,.btn-warning:not(:disabled):not(.disabled):active,.show>.btn-warning.dropdown-toggle{color:#212529;background-color:#d39e00;border-color:#c69500}.btn-warning:not(:disabled):not(.disabled).active:focus,.btn-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(222,170,12,.5)}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#c82333;border-color:#bd2130}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c82333;border-color:#bd2130;box-shadow:0 0 0 .2rem rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:not(:disabled):not(.disabled).active,.btn-danger:not(:disabled):not(.disabled):active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#bd2130;border-color:#b21f2d}.btn-danger:not(:disabled):not(.disabled).active:focus,.btn-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(225,83,97,.5)}.btn-light{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#212529;background-color:#e2e6ea;border-color:#dae0e5}.btn-light.focus,.btn-light:focus{color:#212529;background-color:#e2e6ea;border-color:#dae0e5;box-shadow:0 0 0 .2rem rgba(216,217,219,.5)}.btn-light.disabled,.btn-light:disabled{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:not(:disabled):not(.disabled).active,.btn-light:not(:disabled):not(.disabled):active,.show>.btn-light.dropdown-toggle{color:#212529;background-color:#dae0e5;border-color:#d3d9df}.btn-light:not(:disabled):not(.disabled).active:focus,.btn-light:not(:disabled):not(.disabled):active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(216,217,219,.5)}.btn-dark{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:hover{color:#fff;background-color:#23272b;border-color:#1d2124}.btn-dark.focus,.btn-dark:focus{color:#fff;background-color:#23272b;border-color:#1d2124;box-shadow:0 0 0 .2rem rgba(82,88,93,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:not(:disabled):not(.disabled).active,.btn-dark:not(:disabled):not(.disabled):active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1d2124;border-color:#171a1d}.btn-dark:not(:disabled):not(.disabled).active:focus,.btn-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(82,88,93,.5)}.btn-outline-primary{color:#007bff;border-color:#007bff}.btn-outline-primary:hover{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary.focus,.btn-outline-primary:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#007bff;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled).active,.btn-outline-primary:not(:disabled):not(.disabled):active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary.focus,.btn-outline-secondary:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled).active,.btn-outline-secondary:not(:disabled):not(.disabled):active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-success{color:#28a745;border-color:#28a745}.btn-outline-success:hover{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success.focus,.btn-outline-success:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#28a745;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled).active,.btn-outline-success:not(:disabled):not(.disabled):active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success:not(:disabled):not(.disabled).active:focus,.btn-outline-success:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-info{color:#17a2b8;border-color:#17a2b8}.btn-outline-info:hover{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info.focus,.btn-outline-info:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#17a2b8;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled).active,.btn-outline-info:not(:disabled):not(.disabled):active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info:not(:disabled):not(.disabled).active:focus,.btn-outline-info:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning.focus,.btn-outline-warning:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled).active,.btn-outline-warning:not(:disabled):not(.disabled):active,.show>.btn-outline-warning.dropdown-toggle{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger.focus,.btn-outline-danger:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled).active,.btn-outline-danger:not(:disabled):not(.disabled):active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light.focus,.btn-outline-light:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled).active,.btn-outline-light:not(:disabled):not(.disabled):active,.show>.btn-outline-light.dropdown-toggle{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:not(:disabled):not(.disabled).active:focus,.btn-outline-light:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-dark{color:#343a40;border-color:#343a40}.btn-outline-dark:hover{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark.focus,.btn-outline-dark:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#343a40;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled).active,.btn-outline-dark:not(:disabled):not(.disabled):active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-link{font-weight:400;color:#007bff;text-decoration:none}.btn-link:hover{color:#0056b3;text-decoration:underline}.btn-link.focus,.btn-link:focus{text-decoration:underline;box-shadow:none}.btn-link.disabled,.btn-link:disabled{color:#6c757d;pointer-events:none}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{position:relative;height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.dropdown,.dropleft,.dropright,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu-left{right:auto;left:0}.dropdown-menu-right{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-left{right:auto;left:0}.dropdown-menu-sm-right{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-left{right:auto;left:0}.dropdown-menu-md-right{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-left{right:auto;left:0}.dropdown-menu-lg-right{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-left{right:auto;left:0}.dropdown-menu-xl-right{right:0;left:auto}}.dropup .dropdown-menu{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-menu{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropright .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropright .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropleft .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropleft .dropdown-toggle::after{display:none}.dropleft .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropleft .dropdown-toggle:empty::after{margin-left:0}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-menu[x-placement^=bottom],.dropdown-menu[x-placement^=left],.dropdown-menu[x-placement^=right],.dropdown-menu[x-placement^=top]{right:auto;bottom:auto}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #e9ecef}.dropdown-item{display:block;width:100%;padding:.25rem 1.5rem;clear:both;font-weight:400;color:#212529;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#16181b;text-decoration:none;background-color:#f8f9fa}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#007bff}.dropdown-item.disabled,.dropdown-item:disabled{color:#6c757d;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1.5rem;color:#212529}.btn-group,.btn-group-vertical{position:relative;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;-ms-flex:1 1 auto;flex:1 1 auto}.btn-group-vertical>.btn:hover,.btn-group>.btn:hover{z-index:1}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus{z-index:1}.btn-toolbar{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropright .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropleft .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{-ms-flex-direction:column;flex-direction:column;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:center;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:not(:first-child){border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type=checkbox],.btn-group-toggle>.btn input[type=radio],.btn-group-toggle>.btn-group>.btn input[type=checkbox],.btn-group-toggle>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:stretch;align-items:stretch;width:100%}.input-group>.custom-file,.input-group>.custom-select,.input-group>.form-control,.input-group>.form-control-plaintext{position:relative;-ms-flex:1 1 0%;flex:1 1 0%;min-width:0;margin-bottom:0}.input-group>.custom-file+.custom-file,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.form-control,.input-group>.custom-select+.custom-file,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.form-control,.input-group>.form-control+.custom-file,.input-group>.form-control+.custom-select,.input-group>.form-control+.form-control,.input-group>.form-control-plaintext+.custom-file,.input-group>.form-control-plaintext+.custom-select,.input-group>.form-control-plaintext+.form-control{margin-left:-1px}.input-group>.custom-file .custom-file-input:focus~.custom-file-label,.input-group>.custom-select:focus,.input-group>.form-control:focus{z-index:3}.input-group>.custom-file .custom-file-input:focus{z-index:4}.input-group>.custom-select:not(:last-child),.input-group>.form-control:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-select:not(:first-child),.input-group>.form-control:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.input-group>.custom-file:not(:last-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label::after{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-file:not(:first-child) .custom-file-label{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-append,.input-group-prepend{display:-ms-flexbox;display:flex}.input-group-append .btn,.input-group-prepend .btn{position:relative;z-index:2}.input-group-append .btn:focus,.input-group-prepend .btn:focus{z-index:3}.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.btn,.input-group-append .input-group-text+.input-group-text,.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-prepend .input-group-text+.input-group-text{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.375rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-text input[type=checkbox],.input-group-text input[type=radio]{margin-top:0}.input-group-lg>.custom-select,.input-group-lg>.form-control:not(textarea){height:calc(1.5em + 1rem + 2px)}.input-group-lg>.custom-select,.input-group-lg>.form-control,.input-group-lg>.input-group-append>.btn,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-prepend>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.input-group-sm>.custom-select,.input-group-sm>.form-control:not(textarea){height:calc(1.5em + .5rem + 2px)}.input-group-sm>.custom-select,.input-group-sm>.form-control,.input-group-sm>.input-group-append>.btn,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-prepend>.input-group-text{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.input-group-lg>.custom-select,.input-group-sm>.custom-select{padding-right:1.75rem}.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child),.input-group>.input-group-append:not(:last-child)>.btn,.input-group>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child),.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text{border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;display:block;min-height:1.5rem;padding-left:1.5rem}.custom-control-inline{display:-ms-inline-flexbox;display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;left:0;z-index:-1;width:1rem;height:1.25rem;opacity:0}.custom-control-input:checked~.custom-control-label::before{color:#fff;border-color:#007bff;background-color:#007bff}.custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-control-input:focus:not(:checked)~.custom-control-label::before{border-color:#80bdff}.custom-control-input:not(:disabled):active~.custom-control-label::before{color:#fff;background-color:#b3d7ff;border-color:#b3d7ff}.custom-control-input:disabled~.custom-control-label,.custom-control-input[disabled]~.custom-control-label{color:#6c757d}.custom-control-input:disabled~.custom-control-label::before,.custom-control-input[disabled]~.custom-control-label::before{background-color:#e9ecef}.custom-control-label{position:relative;margin-bottom:0;vertical-align:top}.custom-control-label::before{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;pointer-events:none;content:"";background-color:#fff;border:#adb5bd solid 1px}.custom-control-label::after{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;content:"";background:no-repeat 50%/50% 50%}.custom-checkbox .custom-control-label::before{border-radius:.25rem}.custom-checkbox .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::before{border-color:#007bff;background-color:#007bff}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-checkbox .custom-control-input:disabled:indeterminate~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-radio .custom-control-label::before{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.custom-radio .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-switch{padding-left:2.25rem}.custom-switch .custom-control-label::before{left:-2.25rem;width:1.75rem;pointer-events:all;border-radius:.5rem}.custom-switch .custom-control-label::after{top:calc(.25rem + 2px);left:calc(-2.25rem + 2px);width:calc(1rem - 4px);height:calc(1rem - 4px);background-color:#adb5bd;border-radius:.5rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-switch .custom-control-label::after{transition:none}}.custom-switch .custom-control-input:checked~.custom-control-label::after{background-color:#fff;-webkit-transform:translateX(.75rem);transform:translateX(.75rem)}.custom-switch .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-select{display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem 1.75rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;vertical-align:middle;background:#fff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px;border:1px solid #ced4da;border-radius:.25rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-select:focus{border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-select:focus::-ms-value{color:#495057;background-color:#fff}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:.75rem;background-image:none}.custom-select:disabled{color:#6c757d;background-color:#e9ecef}.custom-select::-ms-expand{display:none}.custom-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #495057}.custom-select-sm{height:calc(1.5em + .5rem + 2px);padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.custom-select-lg{height:calc(1.5em + 1rem + 2px);padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.custom-file{position:relative;display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(1.5em + .75rem + 2px);margin:0;opacity:0}.custom-file-input:focus~.custom-file-label{border-color:#80bdff;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-file-input:disabled~.custom-file-label,.custom-file-input[disabled]~.custom-file-label{background-color:#e9ecef}.custom-file-input:lang(en)~.custom-file-label::after{content:"Browse"}.custom-file-input~.custom-file-label[data-browse]::after{content:attr(data-browse)}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:calc(1.5em + .75rem);padding:.375rem .75rem;line-height:1.5;color:#495057;content:"Browse";background-color:#e9ecef;border-left:inherit;border-radius:0 .25rem .25rem 0}.custom-range{width:100%;height:1.4rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-range:focus{outline:0}.custom-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#007bff;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.custom-range::-webkit-slider-thumb:active{background-color:#b3d7ff}.custom-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#007bff;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-moz-range-thumb{-moz-transition:none;transition:none}}.custom-range::-moz-range-thumb:active{background-color:#b3d7ff}.custom-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-ms-thumb{width:1rem;height:1rem;margin-top:0;margin-right:.2rem;margin-left:.2rem;background-color:#007bff;border:0;border-radius:1rem;-ms-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-ms-thumb{-ms-transition:none;transition:none}}.custom-range::-ms-thumb:active{background-color:#b3d7ff}.custom-range::-ms-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:transparent;border-color:transparent;border-width:.5rem}.custom-range::-ms-fill-lower{background-color:#dee2e6;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px;background-color:#dee2e6;border-radius:1rem}.custom-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.custom-range:disabled::-webkit-slider-runnable-track{cursor:default}.custom-range:disabled::-moz-range-thumb{background-color:#adb5bd}.custom-range:disabled::-moz-range-track{cursor:default}.custom-range:disabled::-ms-thumb{background-color:#adb5bd}.custom-control-label::before,.custom-file-label,.custom-select{transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-control-label::before,.custom-file-label,.custom-select{transition:none}}.nav{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#007bff}.nav-fill .nav-item{-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between;padding:.5rem 1rem}.navbar .container,.navbar .container-fluid,.navbar .container-lg,.navbar .container-md,.navbar .container-sm,.navbar .container-xl{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{-ms-flex-preferred-size:100%;flex-basis:100%;-ms-flex-positive:1;flex-grow:1;-ms-flex-align:center;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:no-repeat center center;background-size:100% 100%}@media (max-width:575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-xl{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-expand-sm{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-sm .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-sm .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (max-width:767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-md,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-xl{padding-right:0;padding-left:0}}@media (min-width:768px){.navbar-expand-md{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-md .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-md,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-md .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (max-width:991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-xl{padding-right:0;padding-left:0}}@media (min-width:992px){.navbar-expand-lg{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-lg .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-lg .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (max-width:1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-xl{padding-right:0;padding-left:0}}@media (min-width:1200px){.navbar-expand-xl{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-xl .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-xl .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}.navbar-expand{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-lg,.navbar-expand>.container-md,.navbar-expand>.container-sm,.navbar-expand>.container-xl{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-lg,.navbar-expand>.container-md,.navbar-expand>.container-sm,.navbar-expand>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.5)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.5);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba(0, 0, 0, 0.5)' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.5)}.navbar-light .navbar-text a{color:rgba(0,0,0,.9)}.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.5)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.5);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba(255, 255, 255, 0.5)' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.5)}.navbar-dark .navbar-text a{color:#fff}.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group:first-child .list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.card>.list-group:last-child .list-group-item:last-child{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.card-body{-ms-flex:1 1 auto;flex:1 1 auto;min-height:1px;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-header+.list-group .list-group-item:first-child{border-top:0}.card-footer{padding:.75rem 1.25rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img,.card-img-bottom,.card-img-top{-ms-flex-negative:0;flex-shrink:0;width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-deck .card{margin-bottom:15px}@media (min-width:576px){.card-deck{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card{-ms-flex:1 0 0%;flex:1 0 0%;margin-right:15px;margin-bottom:0;margin-left:15px}}.card-group>.card{margin-bottom:15px}@media (min-width:576px){.card-group{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group>.card{-ms-flex:1 0 0%;flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.card-columns .card{margin-bottom:.75rem}@media (min-width:576px){.card-columns{-webkit-column-count:3;-moz-column-count:3;column-count:3;-webkit-column-gap:1.25rem;-moz-column-gap:1.25rem;column-gap:1.25rem;orphans:1;widows:1}.card-columns .card{display:inline-block;width:100%}}.accordion>.card{overflow:hidden}.accordion>.card:not(:last-of-type){border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.accordion>.card:not(:first-of-type){border-top-left-radius:0;border-top-right-radius:0}.accordion>.card>.card-header{border-radius:0;margin-bottom:-1px}.breadcrumb{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#e9ecef;border-radius:.25rem}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:.5rem;color:#6c757d;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#6c757d}.pagination{display:-ms-flexbox;display:flex;padding-left:0;list-style:none;border-radius:.25rem}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#007bff;background-color:#fff;border:1px solid #dee2e6}.page-link:hover{z-index:2;color:#0056b3;text-decoration:none;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.page-item.active .page-link{z-index:3;color:#fff;background-color:#007bff;border-color:#007bff}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;cursor:auto;background-color:#fff;border-color:#dee2e6}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.badge{transition:none}}a.badge:focus,a.badge:hover{text-decoration:none}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-primary{color:#fff;background-color:#007bff}a.badge-primary:focus,a.badge-primary:hover{color:#fff;background-color:#0062cc}a.badge-primary.focus,a.badge-primary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.badge-secondary{color:#fff;background-color:#6c757d}a.badge-secondary:focus,a.badge-secondary:hover{color:#fff;background-color:#545b62}a.badge-secondary.focus,a.badge-secondary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.badge-success{color:#fff;background-color:#28a745}a.badge-success:focus,a.badge-success:hover{color:#fff;background-color:#1e7e34}a.badge-success.focus,a.badge-success:focus{outline:0;box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.badge-info{color:#fff;background-color:#17a2b8}a.badge-info:focus,a.badge-info:hover{color:#fff;background-color:#117a8b}a.badge-info.focus,a.badge-info:focus{outline:0;box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.badge-warning{color:#212529;background-color:#ffc107}a.badge-warning:focus,a.badge-warning:hover{color:#212529;background-color:#d39e00}a.badge-warning.focus,a.badge-warning:focus{outline:0;box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.badge-danger{color:#fff;background-color:#dc3545}a.badge-danger:focus,a.badge-danger:hover{color:#fff;background-color:#bd2130}a.badge-danger.focus,a.badge-danger:focus{outline:0;box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.badge-light{color:#212529;background-color:#f8f9fa}a.badge-light:focus,a.badge-light:hover{color:#212529;background-color:#dae0e5}a.badge-light.focus,a.badge-light:focus{outline:0;box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.badge-dark{color:#fff;background-color:#343a40}a.badge-dark:focus,a.badge-dark:hover{color:#fff;background-color:#1d2124}a.badge-dark.focus,a.badge-dark:focus{outline:0;box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#e9ecef;border-radius:.3rem}@media (min-width:576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:4rem}.alert-dismissible .close{position:absolute;top:0;right:0;padding:.75rem 1.25rem;color:inherit}.alert-primary{color:#004085;background-color:#cce5ff;border-color:#b8daff}.alert-primary hr{border-top-color:#9fcdff}.alert-primary .alert-link{color:#002752}.alert-secondary{color:#383d41;background-color:#e2e3e5;border-color:#d6d8db}.alert-secondary hr{border-top-color:#c8cbcf}.alert-secondary .alert-link{color:#202326}.alert-success{color:#155724;background-color:#d4edda;border-color:#c3e6cb}.alert-success hr{border-top-color:#b1dfbb}.alert-success .alert-link{color:#0b2e13}.alert-info{color:#0c5460;background-color:#d1ecf1;border-color:#bee5eb}.alert-info hr{border-top-color:#abdde5}.alert-info .alert-link{color:#062c33}.alert-warning{color:#856404;background-color:#fff3cd;border-color:#ffeeba}.alert-warning hr{border-top-color:#ffe8a1}.alert-warning .alert-link{color:#533f03}.alert-danger{color:#721c24;background-color:#f8d7da;border-color:#f5c6cb}.alert-danger hr{border-top-color:#f1b0b7}.alert-danger .alert-link{color:#491217}.alert-light{color:#818182;background-color:#fefefe;border-color:#fdfdfe}.alert-light hr{border-top-color:#ececf6}.alert-light .alert-link{color:#686868}.alert-dark{color:#1b1e21;background-color:#d6d8d9;border-color:#c6c8ca}.alert-dark hr{border-top-color:#b9bbbe}.alert-dark .alert-link{color:#040505}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:-ms-flexbox;display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#007bff;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:progress-bar-stripes 1s linear infinite;animation:progress-bar-stripes 1s linear infinite}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.media{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start}.media-body{-ms-flex:1;flex:1}.list-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.75rem 1.25rem;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.list-group-item:last-child{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#007bff;border-color:#007bff}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal .list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal .list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal .list-group-item.active{margin-top:0}.list-group-horizontal .list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal .list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:576px){.list-group-horizontal-sm{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-sm .list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm .list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm .list-group-item.active{margin-top:0}.list-group-horizontal-sm .list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm .list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:768px){.list-group-horizontal-md{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-md .list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md .list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md .list-group-item.active{margin-top:0}.list-group-horizontal-md .list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md .list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:992px){.list-group-horizontal-lg{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-lg .list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg .list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg .list-group-item.active{margin-top:0}.list-group-horizontal-lg .list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg .list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-xl .list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl .list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl .list-group-item.active{margin-top:0}.list-group-horizontal-xl .list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl .list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush .list-group-item{border-right-width:0;border-left-width:0;border-radius:0}.list-group-flush .list-group-item:first-child{border-top-width:0}.list-group-flush:last-child .list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#004085;background-color:#b8daff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#004085;background-color:#9fcdff}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#004085;border-color:#004085}.list-group-item-secondary{color:#383d41;background-color:#d6d8db}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#383d41;background-color:#c8cbcf}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#383d41;border-color:#383d41}.list-group-item-success{color:#155724;background-color:#c3e6cb}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#155724;background-color:#b1dfbb}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#155724;border-color:#155724}.list-group-item-info{color:#0c5460;background-color:#bee5eb}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#0c5460;background-color:#abdde5}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#0c5460;border-color:#0c5460}.list-group-item-warning{color:#856404;background-color:#ffeeba}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#856404;background-color:#ffe8a1}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#856404;border-color:#856404}.list-group-item-danger{color:#721c24;background-color:#f5c6cb}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#721c24;background-color:#f1b0b7}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#721c24;border-color:#721c24}.list-group-item-light{color:#818182;background-color:#fdfdfe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#818182;background-color:#ececf6}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#818182;border-color:#818182}.list-group-item-dark{color:#1b1e21;background-color:#c6c8ca}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#1b1e21;background-color:#b9bbbe}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#1b1e21;border-color:#1b1e21}.close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}.close:hover{color:#000;text-decoration:none}.close:not(:disabled):not(.disabled):focus,.close:not(:disabled):not(.disabled):hover{opacity:.75}button.close{padding:0;background-color:transparent;border:0;-webkit-appearance:none;-moz-appearance:none;appearance:none}a.close.disabled{pointer-events:none}.toast{max-width:350px;overflow:hidden;font-size:.875rem;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .25rem .75rem rgba(0,0,0,.1);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);opacity:0;border-radius:.25rem}.toast:not(:last-child){margin-bottom:.75rem}.toast.showing{opacity:1}.toast.show{display:block;opacity:1}.toast.hide{display:none}.toast-header{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.25rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05)}.toast-body{padding:.75rem}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;left:0;z-index:1050;display:none;width:100%;height:100%;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out;-webkit-transform:translate(0,-50px);transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{-webkit-transform:none;transform:none}.modal.modal-static .modal-dialog{-webkit-transform:scale(1.02);transform:scale(1.02)}.modal-dialog-scrollable{display:-ms-flexbox;display:flex;max-height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 1rem);overflow:hidden}.modal-dialog-scrollable .modal-footer,.modal-dialog-scrollable .modal-header{-ms-flex-negative:0;flex-shrink:0}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;min-height:calc(100% - 1rem)}.modal-dialog-centered::before{display:block;height:calc(100vh - 1rem);content:""}.modal-dialog-centered.modal-dialog-scrollable{-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;height:100%}.modal-dialog-centered.modal-dialog-scrollable .modal-content{max-height:none}.modal-dialog-centered.modal-dialog-scrollable::before{content:none}.modal-content{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:justify;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.modal-header .close{padding:1rem 1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem}.modal-footer{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:end;justify-content:flex-end;padding:.75rem;border-top:1px solid #dee2e6;border-bottom-right-radius:calc(.3rem - 1px);border-bottom-left-radius:calc(.3rem - 1px)}.modal-footer>*{margin:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{max-height:calc(100% - 3.5rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-dialog-centered::before{height:calc(100vh - 3.5rem)}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[x-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[x-placement^=top] .arrow,.bs-tooltip-top .arrow{bottom:0}.bs-tooltip-auto[x-placement^=top] .arrow::before,.bs-tooltip-top .arrow::before{top:0;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[x-placement^=right],.bs-tooltip-right{padding:0 .4rem}.bs-tooltip-auto[x-placement^=right] .arrow,.bs-tooltip-right .arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=right] .arrow::before,.bs-tooltip-right .arrow::before{right:0;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[x-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[x-placement^=bottom] .arrow,.bs-tooltip-bottom .arrow{top:0}.bs-tooltip-auto[x-placement^=bottom] .arrow::before,.bs-tooltip-bottom .arrow::before{bottom:0;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[x-placement^=left],.bs-tooltip-left{padding:0 .4rem}.bs-tooltip-auto[x-placement^=left] .arrow,.bs-tooltip-left .arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=left] .arrow::before,.bs-tooltip-left .arrow::before{left:0;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .arrow{position:absolute;display:block;width:1rem;height:.5rem;margin:0 .3rem}.popover .arrow::after,.popover .arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[x-placement^=top],.bs-popover-top{margin-bottom:.5rem}.bs-popover-auto[x-placement^=top]>.arrow,.bs-popover-top>.arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[x-placement^=top]>.arrow::before,.bs-popover-top>.arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=top]>.arrow::after,.bs-popover-top>.arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[x-placement^=right],.bs-popover-right{margin-left:.5rem}.bs-popover-auto[x-placement^=right]>.arrow,.bs-popover-right>.arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=right]>.arrow::before,.bs-popover-right>.arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=right]>.arrow::after,.bs-popover-right>.arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[x-placement^=bottom],.bs-popover-bottom{margin-top:.5rem}.bs-popover-auto[x-placement^=bottom]>.arrow,.bs-popover-bottom>.arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[x-placement^=bottom]>.arrow::before,.bs-popover-bottom>.arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=bottom]>.arrow::after,.bs-popover-bottom>.arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[x-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f7f7f7}.bs-popover-auto[x-placement^=left],.bs-popover-left{margin-right:.5rem}.bs-popover-auto[x-placement^=left]>.arrow,.bs-popover-left>.arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=left]>.arrow::before,.bs-popover-left>.arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=left]>.arrow::after,.bs-popover-left>.arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:.5rem .75rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{-ms-touch-action:pan-y;touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:-webkit-transform .6s ease-in-out;transition:transform .6s ease-in-out;transition:transform .6s ease-in-out,-webkit-transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-right,.carousel-item-next:not(.carousel-item-left){-webkit-transform:translateX(100%);transform:translateX(100%)}.active.carousel-item-left,.carousel-item-prev:not(.carousel-item-right){-webkit-transform:translateX(-100%);transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;-webkit-transform:none;transform:none}.carousel-fade .carousel-item-next.carousel-item-left,.carousel-fade .carousel-item-prev.carousel-item-right,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:20px;height:20px;background:no-repeat 50%/100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5L4.25 4l2.5-2.5L5.25 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5L3.75 4l-2.5 2.5L2.75 8l4-4-4-4z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:15;display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{box-sizing:content-box;-ms-flex:0 1 auto;flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators li{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}@-webkit-keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:spinner-border .75s linear infinite;animation:spinner-border .75s linear infinite}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1}}@keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:spinner-grow .75s linear infinite;animation:spinner-grow .75s linear infinite}.spinner-grow-sm{width:1rem;height:1rem}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.bg-primary{background-color:#007bff!important}a.bg-primary:focus,a.bg-primary:hover,button.bg-primary:focus,button.bg-primary:hover{background-color:#0062cc!important}.bg-secondary{background-color:#6c757d!important}a.bg-secondary:focus,a.bg-secondary:hover,button.bg-secondary:focus,button.bg-secondary:hover{background-color:#545b62!important}.bg-success{background-color:#28a745!important}a.bg-success:focus,a.bg-success:hover,button.bg-success:focus,button.bg-success:hover{background-color:#1e7e34!important}.bg-info{background-color:#17a2b8!important}a.bg-info:focus,a.bg-info:hover,button.bg-info:focus,button.bg-info:hover{background-color:#117a8b!important}.bg-warning{background-color:#ffc107!important}a.bg-warning:focus,a.bg-warning:hover,button.bg-warning:focus,button.bg-warning:hover{background-color:#d39e00!important}.bg-danger{background-color:#dc3545!important}a.bg-danger:focus,a.bg-danger:hover,button.bg-danger:focus,button.bg-danger:hover{background-color:#bd2130!important}.bg-light{background-color:#f8f9fa!important}a.bg-light:focus,a.bg-light:hover,button.bg-light:focus,button.bg-light:hover{background-color:#dae0e5!important}.bg-dark{background-color:#343a40!important}a.bg-dark:focus,a.bg-dark:hover,button.bg-dark:focus,button.bg-dark:hover{background-color:#1d2124!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.border{border:1px solid #dee2e6!important}.border-top{border-top:1px solid #dee2e6!important}.border-right{border-right:1px solid #dee2e6!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-left{border-left:1px solid #dee2e6!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.border-primary{border-color:#007bff!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#28a745!important}.border-info{border-color:#17a2b8!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#343a40!important}.border-white{border-color:#fff!important}.rounded-sm{border-radius:.2rem!important}.rounded{border-radius:.25rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-right{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-left{border-top-left-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-lg{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-0{border-radius:0!important}.clearfix::after{display:block;clear:both;content:""}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:-ms-flexbox!important;display:flex!important}.d-print-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.857143%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}@media (min-width:576px){.flex-sm-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-sm-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-sm-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-sm-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-sm-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-sm-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-sm-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-sm-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-sm-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-sm-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:768px){.flex-md-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-md-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-md-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-md-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-md-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-md-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-md-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-md-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-md-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-md-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:992px){.flex-lg-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-lg-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-lg-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-lg-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-lg-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-lg-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-lg-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-lg-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-lg-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-lg-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:1200px){.flex-xl-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-xl-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-xl-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-xl-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-xl-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-xl-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-xl-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-xl-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-xl-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-xl-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:576px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:768px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:992px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:1200px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!important}}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports ((position:-webkit-sticky) or (position:sticky)){.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.min-vw-100{min-width:100vw!important}.min-vh-100{min-height:100vh!important}.vw-100{width:100vw!important}.vh-100{height:100vh!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;pointer-events:auto;content:"";background-color:rgba(0,0,0,0)}.m-0{margin:0!important}.mt-0,.my-0{margin-top:0!important}.mr-0,.mx-0{margin-right:0!important}.mb-0,.my-0{margin-bottom:0!important}.ml-0,.mx-0{margin-left:0!important}.m-1{margin:.25rem!important}.mt-1,.my-1{margin-top:.25rem!important}.mr-1,.mx-1{margin-right:.25rem!important}.mb-1,.my-1{margin-bottom:.25rem!important}.ml-1,.mx-1{margin-left:.25rem!important}.m-2{margin:.5rem!important}.mt-2,.my-2{margin-top:.5rem!important}.mr-2,.mx-2{margin-right:.5rem!important}.mb-2,.my-2{margin-bottom:.5rem!important}.ml-2,.mx-2{margin-left:.5rem!important}.m-3{margin:1rem!important}.mt-3,.my-3{margin-top:1rem!important}.mr-3,.mx-3{margin-right:1rem!important}.mb-3,.my-3{margin-bottom:1rem!important}.ml-3,.mx-3{margin-left:1rem!important}.m-4{margin:1.5rem!important}.mt-4,.my-4{margin-top:1.5rem!important}.mr-4,.mx-4{margin-right:1.5rem!important}.mb-4,.my-4{margin-bottom:1.5rem!important}.ml-4,.mx-4{margin-left:1.5rem!important}.m-5{margin:3rem!important}.mt-5,.my-5{margin-top:3rem!important}.mr-5,.mx-5{margin-right:3rem!important}.mb-5,.my-5{margin-bottom:3rem!important}.ml-5,.mx-5{margin-left:3rem!important}.p-0{padding:0!important}.pt-0,.py-0{padding-top:0!important}.pr-0,.px-0{padding-right:0!important}.pb-0,.py-0{padding-bottom:0!important}.pl-0,.px-0{padding-left:0!important}.p-1{padding:.25rem!important}.pt-1,.py-1{padding-top:.25rem!important}.pr-1,.px-1{padding-right:.25rem!important}.pb-1,.py-1{padding-bottom:.25rem!important}.pl-1,.px-1{padding-left:.25rem!important}.p-2{padding:.5rem!important}.pt-2,.py-2{padding-top:.5rem!important}.pr-2,.px-2{padding-right:.5rem!important}.pb-2,.py-2{padding-bottom:.5rem!important}.pl-2,.px-2{padding-left:.5rem!important}.p-3{padding:1rem!important}.pt-3,.py-3{padding-top:1rem!important}.pr-3,.px-3{padding-right:1rem!important}.pb-3,.py-3{padding-bottom:1rem!important}.pl-3,.px-3{padding-left:1rem!important}.p-4{padding:1.5rem!important}.pt-4,.py-4{padding-top:1.5rem!important}.pr-4,.px-4{padding-right:1.5rem!important}.pb-4,.py-4{padding-bottom:1.5rem!important}.pl-4,.px-4{padding-left:1.5rem!important}.p-5{padding:3rem!important}.pt-5,.py-5{padding-top:3rem!important}.pr-5,.px-5{padding-right:3rem!important}.pb-5,.py-5{padding-bottom:3rem!important}.pl-5,.px-5{padding-left:3rem!important}.m-n1{margin:-.25rem!important}.mt-n1,.my-n1{margin-top:-.25rem!important}.mr-n1,.mx-n1{margin-right:-.25rem!important}.mb-n1,.my-n1{margin-bottom:-.25rem!important}.ml-n1,.mx-n1{margin-left:-.25rem!important}.m-n2{margin:-.5rem!important}.mt-n2,.my-n2{margin-top:-.5rem!important}.mr-n2,.mx-n2{margin-right:-.5rem!important}.mb-n2,.my-n2{margin-bottom:-.5rem!important}.ml-n2,.mx-n2{margin-left:-.5rem!important}.m-n3{margin:-1rem!important}.mt-n3,.my-n3{margin-top:-1rem!important}.mr-n3,.mx-n3{margin-right:-1rem!important}.mb-n3,.my-n3{margin-bottom:-1rem!important}.ml-n3,.mx-n3{margin-left:-1rem!important}.m-n4{margin:-1.5rem!important}.mt-n4,.my-n4{margin-top:-1.5rem!important}.mr-n4,.mx-n4{margin-right:-1.5rem!important}.mb-n4,.my-n4{margin-bottom:-1.5rem!important}.ml-n4,.mx-n4{margin-left:-1.5rem!important}.m-n5{margin:-3rem!important}.mt-n5,.my-n5{margin-top:-3rem!important}.mr-n5,.mx-n5{margin-right:-3rem!important}.mb-n5,.my-n5{margin-bottom:-3rem!important}.ml-n5,.mx-n5{margin-left:-3rem!important}.m-auto{margin:auto!important}.mt-auto,.my-auto{margin-top:auto!important}.mr-auto,.mx-auto{margin-right:auto!important}.mb-auto,.my-auto{margin-bottom:auto!important}.ml-auto,.mx-auto{margin-left:auto!important}@media (min-width:576px){.m-sm-0{margin:0!important}.mt-sm-0,.my-sm-0{margin-top:0!important}.mr-sm-0,.mx-sm-0{margin-right:0!important}.mb-sm-0,.my-sm-0{margin-bottom:0!important}.ml-sm-0,.mx-sm-0{margin-left:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1,.my-sm-1{margin-top:.25rem!important}.mr-sm-1,.mx-sm-1{margin-right:.25rem!important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem!important}.ml-sm-1,.mx-sm-1{margin-left:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2,.my-sm-2{margin-top:.5rem!important}.mr-sm-2,.mx-sm-2{margin-right:.5rem!important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem!important}.ml-sm-2,.mx-sm-2{margin-left:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3,.my-sm-3{margin-top:1rem!important}.mr-sm-3,.mx-sm-3{margin-right:1rem!important}.mb-sm-3,.my-sm-3{margin-bottom:1rem!important}.ml-sm-3,.mx-sm-3{margin-left:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4,.my-sm-4{margin-top:1.5rem!important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem!important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem!important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5,.my-sm-5{margin-top:3rem!important}.mr-sm-5,.mx-sm-5{margin-right:3rem!important}.mb-sm-5,.my-sm-5{margin-bottom:3rem!important}.ml-sm-5,.mx-sm-5{margin-left:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0,.py-sm-0{padding-top:0!important}.pr-sm-0,.px-sm-0{padding-right:0!important}.pb-sm-0,.py-sm-0{padding-bottom:0!important}.pl-sm-0,.px-sm-0{padding-left:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1,.py-sm-1{padding-top:.25rem!important}.pr-sm-1,.px-sm-1{padding-right:.25rem!important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem!important}.pl-sm-1,.px-sm-1{padding-left:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2,.py-sm-2{padding-top:.5rem!important}.pr-sm-2,.px-sm-2{padding-right:.5rem!important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem!important}.pl-sm-2,.px-sm-2{padding-left:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3,.py-sm-3{padding-top:1rem!important}.pr-sm-3,.px-sm-3{padding-right:1rem!important}.pb-sm-3,.py-sm-3{padding-bottom:1rem!important}.pl-sm-3,.px-sm-3{padding-left:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4,.py-sm-4{padding-top:1.5rem!important}.pr-sm-4,.px-sm-4{padding-right:1.5rem!important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem!important}.pl-sm-4,.px-sm-4{padding-left:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5,.py-sm-5{padding-top:3rem!important}.pr-sm-5,.px-sm-5{padding-right:3rem!important}.pb-sm-5,.py-sm-5{padding-bottom:3rem!important}.pl-sm-5,.px-sm-5{padding-left:3rem!important}.m-sm-n1{margin:-.25rem!important}.mt-sm-n1,.my-sm-n1{margin-top:-.25rem!important}.mr-sm-n1,.mx-sm-n1{margin-right:-.25rem!important}.mb-sm-n1,.my-sm-n1{margin-bottom:-.25rem!important}.ml-sm-n1,.mx-sm-n1{margin-left:-.25rem!important}.m-sm-n2{margin:-.5rem!important}.mt-sm-n2,.my-sm-n2{margin-top:-.5rem!important}.mr-sm-n2,.mx-sm-n2{margin-right:-.5rem!important}.mb-sm-n2,.my-sm-n2{margin-bottom:-.5rem!important}.ml-sm-n2,.mx-sm-n2{margin-left:-.5rem!important}.m-sm-n3{margin:-1rem!important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem!important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem!important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem!important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem!important}.m-sm-n4{margin:-1.5rem!important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem!important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem!important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem!important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem!important}.m-sm-n5{margin:-3rem!important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem!important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem!important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem!important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto,.my-sm-auto{margin-top:auto!important}.mr-sm-auto,.mx-sm-auto{margin-right:auto!important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto!important}.ml-sm-auto,.mx-sm-auto{margin-left:auto!important}}@media (min-width:768px){.m-md-0{margin:0!important}.mt-md-0,.my-md-0{margin-top:0!important}.mr-md-0,.mx-md-0{margin-right:0!important}.mb-md-0,.my-md-0{margin-bottom:0!important}.ml-md-0,.mx-md-0{margin-left:0!important}.m-md-1{margin:.25rem!important}.mt-md-1,.my-md-1{margin-top:.25rem!important}.mr-md-1,.mx-md-1{margin-right:.25rem!important}.mb-md-1,.my-md-1{margin-bottom:.25rem!important}.ml-md-1,.mx-md-1{margin-left:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2,.my-md-2{margin-top:.5rem!important}.mr-md-2,.mx-md-2{margin-right:.5rem!important}.mb-md-2,.my-md-2{margin-bottom:.5rem!important}.ml-md-2,.mx-md-2{margin-left:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3,.my-md-3{margin-top:1rem!important}.mr-md-3,.mx-md-3{margin-right:1rem!important}.mb-md-3,.my-md-3{margin-bottom:1rem!important}.ml-md-3,.mx-md-3{margin-left:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4,.my-md-4{margin-top:1.5rem!important}.mr-md-4,.mx-md-4{margin-right:1.5rem!important}.mb-md-4,.my-md-4{margin-bottom:1.5rem!important}.ml-md-4,.mx-md-4{margin-left:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5,.my-md-5{margin-top:3rem!important}.mr-md-5,.mx-md-5{margin-right:3rem!important}.mb-md-5,.my-md-5{margin-bottom:3rem!important}.ml-md-5,.mx-md-5{margin-left:3rem!important}.p-md-0{padding:0!important}.pt-md-0,.py-md-0{padding-top:0!important}.pr-md-0,.px-md-0{padding-right:0!important}.pb-md-0,.py-md-0{padding-bottom:0!important}.pl-md-0,.px-md-0{padding-left:0!important}.p-md-1{padding:.25rem!important}.pt-md-1,.py-md-1{padding-top:.25rem!important}.pr-md-1,.px-md-1{padding-right:.25rem!important}.pb-md-1,.py-md-1{padding-bottom:.25rem!important}.pl-md-1,.px-md-1{padding-left:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2,.py-md-2{padding-top:.5rem!important}.pr-md-2,.px-md-2{padding-right:.5rem!important}.pb-md-2,.py-md-2{padding-bottom:.5rem!important}.pl-md-2,.px-md-2{padding-left:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3,.py-md-3{padding-top:1rem!important}.pr-md-3,.px-md-3{padding-right:1rem!important}.pb-md-3,.py-md-3{padding-bottom:1rem!important}.pl-md-3,.px-md-3{padding-left:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4,.py-md-4{padding-top:1.5rem!important}.pr-md-4,.px-md-4{padding-right:1.5rem!important}.pb-md-4,.py-md-4{padding-bottom:1.5rem!important}.pl-md-4,.px-md-4{padding-left:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5,.py-md-5{padding-top:3rem!important}.pr-md-5,.px-md-5{padding-right:3rem!important}.pb-md-5,.py-md-5{padding-bottom:3rem!important}.pl-md-5,.px-md-5{padding-left:3rem!important}.m-md-n1{margin:-.25rem!important}.mt-md-n1,.my-md-n1{margin-top:-.25rem!important}.mr-md-n1,.mx-md-n1{margin-right:-.25rem!important}.mb-md-n1,.my-md-n1{margin-bottom:-.25rem!important}.ml-md-n1,.mx-md-n1{margin-left:-.25rem!important}.m-md-n2{margin:-.5rem!important}.mt-md-n2,.my-md-n2{margin-top:-.5rem!important}.mr-md-n2,.mx-md-n2{margin-right:-.5rem!important}.mb-md-n2,.my-md-n2{margin-bottom:-.5rem!important}.ml-md-n2,.mx-md-n2{margin-left:-.5rem!important}.m-md-n3{margin:-1rem!important}.mt-md-n3,.my-md-n3{margin-top:-1rem!important}.mr-md-n3,.mx-md-n3{margin-right:-1rem!important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem!important}.ml-md-n3,.mx-md-n3{margin-left:-1rem!important}.m-md-n4{margin:-1.5rem!important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem!important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem!important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem!important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem!important}.m-md-n5{margin:-3rem!important}.mt-md-n5,.my-md-n5{margin-top:-3rem!important}.mr-md-n5,.mx-md-n5{margin-right:-3rem!important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem!important}.ml-md-n5,.mx-md-n5{margin-left:-3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto,.my-md-auto{margin-top:auto!important}.mr-md-auto,.mx-md-auto{margin-right:auto!important}.mb-md-auto,.my-md-auto{margin-bottom:auto!important}.ml-md-auto,.mx-md-auto{margin-left:auto!important}}@media (min-width:992px){.m-lg-0{margin:0!important}.mt-lg-0,.my-lg-0{margin-top:0!important}.mr-lg-0,.mx-lg-0{margin-right:0!important}.mb-lg-0,.my-lg-0{margin-bottom:0!important}.ml-lg-0,.mx-lg-0{margin-left:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1,.my-lg-1{margin-top:.25rem!important}.mr-lg-1,.mx-lg-1{margin-right:.25rem!important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem!important}.ml-lg-1,.mx-lg-1{margin-left:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2,.my-lg-2{margin-top:.5rem!important}.mr-lg-2,.mx-lg-2{margin-right:.5rem!important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem!important}.ml-lg-2,.mx-lg-2{margin-left:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3,.my-lg-3{margin-top:1rem!important}.mr-lg-3,.mx-lg-3{margin-right:1rem!important}.mb-lg-3,.my-lg-3{margin-bottom:1rem!important}.ml-lg-3,.mx-lg-3{margin-left:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4,.my-lg-4{margin-top:1.5rem!important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem!important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem!important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5,.my-lg-5{margin-top:3rem!important}.mr-lg-5,.mx-lg-5{margin-right:3rem!important}.mb-lg-5,.my-lg-5{margin-bottom:3rem!important}.ml-lg-5,.mx-lg-5{margin-left:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0,.py-lg-0{padding-top:0!important}.pr-lg-0,.px-lg-0{padding-right:0!important}.pb-lg-0,.py-lg-0{padding-bottom:0!important}.pl-lg-0,.px-lg-0{padding-left:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1,.py-lg-1{padding-top:.25rem!important}.pr-lg-1,.px-lg-1{padding-right:.25rem!important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem!important}.pl-lg-1,.px-lg-1{padding-left:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2,.py-lg-2{padding-top:.5rem!important}.pr-lg-2,.px-lg-2{padding-right:.5rem!important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem!important}.pl-lg-2,.px-lg-2{padding-left:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3,.py-lg-3{padding-top:1rem!important}.pr-lg-3,.px-lg-3{padding-right:1rem!important}.pb-lg-3,.py-lg-3{padding-bottom:1rem!important}.pl-lg-3,.px-lg-3{padding-left:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4,.py-lg-4{padding-top:1.5rem!important}.pr-lg-4,.px-lg-4{padding-right:1.5rem!important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem!important}.pl-lg-4,.px-lg-4{padding-left:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5,.py-lg-5{padding-top:3rem!important}.pr-lg-5,.px-lg-5{padding-right:3rem!important}.pb-lg-5,.py-lg-5{padding-bottom:3rem!important}.pl-lg-5,.px-lg-5{padding-left:3rem!important}.m-lg-n1{margin:-.25rem!important}.mt-lg-n1,.my-lg-n1{margin-top:-.25rem!important}.mr-lg-n1,.mx-lg-n1{margin-right:-.25rem!important}.mb-lg-n1,.my-lg-n1{margin-bottom:-.25rem!important}.ml-lg-n1,.mx-lg-n1{margin-left:-.25rem!important}.m-lg-n2{margin:-.5rem!important}.mt-lg-n2,.my-lg-n2{margin-top:-.5rem!important}.mr-lg-n2,.mx-lg-n2{margin-right:-.5rem!important}.mb-lg-n2,.my-lg-n2{margin-bottom:-.5rem!important}.ml-lg-n2,.mx-lg-n2{margin-left:-.5rem!important}.m-lg-n3{margin:-1rem!important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem!important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem!important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem!important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem!important}.m-lg-n4{margin:-1.5rem!important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem!important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem!important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem!important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem!important}.m-lg-n5{margin:-3rem!important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem!important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem!important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem!important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto,.my-lg-auto{margin-top:auto!important}.mr-lg-auto,.mx-lg-auto{margin-right:auto!important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto!important}.ml-lg-auto,.mx-lg-auto{margin-left:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0!important}.mt-xl-0,.my-xl-0{margin-top:0!important}.mr-xl-0,.mx-xl-0{margin-right:0!important}.mb-xl-0,.my-xl-0{margin-bottom:0!important}.ml-xl-0,.mx-xl-0{margin-left:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1,.my-xl-1{margin-top:.25rem!important}.mr-xl-1,.mx-xl-1{margin-right:.25rem!important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem!important}.ml-xl-1,.mx-xl-1{margin-left:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2,.my-xl-2{margin-top:.5rem!important}.mr-xl-2,.mx-xl-2{margin-right:.5rem!important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem!important}.ml-xl-2,.mx-xl-2{margin-left:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3,.my-xl-3{margin-top:1rem!important}.mr-xl-3,.mx-xl-3{margin-right:1rem!important}.mb-xl-3,.my-xl-3{margin-bottom:1rem!important}.ml-xl-3,.mx-xl-3{margin-left:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4,.my-xl-4{margin-top:1.5rem!important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem!important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem!important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5,.my-xl-5{margin-top:3rem!important}.mr-xl-5,.mx-xl-5{margin-right:3rem!important}.mb-xl-5,.my-xl-5{margin-bottom:3rem!important}.ml-xl-5,.mx-xl-5{margin-left:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0,.py-xl-0{padding-top:0!important}.pr-xl-0,.px-xl-0{padding-right:0!important}.pb-xl-0,.py-xl-0{padding-bottom:0!important}.pl-xl-0,.px-xl-0{padding-left:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1,.py-xl-1{padding-top:.25rem!important}.pr-xl-1,.px-xl-1{padding-right:.25rem!important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem!important}.pl-xl-1,.px-xl-1{padding-left:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2,.py-xl-2{padding-top:.5rem!important}.pr-xl-2,.px-xl-2{padding-right:.5rem!important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem!important}.pl-xl-2,.px-xl-2{padding-left:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3,.py-xl-3{padding-top:1rem!important}.pr-xl-3,.px-xl-3{padding-right:1rem!important}.pb-xl-3,.py-xl-3{padding-bottom:1rem!important}.pl-xl-3,.px-xl-3{padding-left:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4,.py-xl-4{padding-top:1.5rem!important}.pr-xl-4,.px-xl-4{padding-right:1.5rem!important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem!important}.pl-xl-4,.px-xl-4{padding-left:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5,.py-xl-5{padding-top:3rem!important}.pr-xl-5,.px-xl-5{padding-right:3rem!important}.pb-xl-5,.py-xl-5{padding-bottom:3rem!important}.pl-xl-5,.px-xl-5{padding-left:3rem!important}.m-xl-n1{margin:-.25rem!important}.mt-xl-n1,.my-xl-n1{margin-top:-.25rem!important}.mr-xl-n1,.mx-xl-n1{margin-right:-.25rem!important}.mb-xl-n1,.my-xl-n1{margin-bottom:-.25rem!important}.ml-xl-n1,.mx-xl-n1{margin-left:-.25rem!important}.m-xl-n2{margin:-.5rem!important}.mt-xl-n2,.my-xl-n2{margin-top:-.5rem!important}.mr-xl-n2,.mx-xl-n2{margin-right:-.5rem!important}.mb-xl-n2,.my-xl-n2{margin-bottom:-.5rem!important}.ml-xl-n2,.mx-xl-n2{margin-left:-.5rem!important}.m-xl-n3{margin:-1rem!important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem!important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem!important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem!important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem!important}.m-xl-n4{margin:-1.5rem!important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem!important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem!important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem!important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem!important}.m-xl-n5{margin:-3rem!important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem!important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem!important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem!important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto,.my-xl-auto{margin-top:auto!important}.mr-xl-auto,.mx-xl-auto{margin-right:auto!important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto!important}.ml-xl-auto,.mx-xl-auto{margin-left:auto!important}}.text-monospace{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace!important}.text-justify{text-align:justify!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:576px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-light{font-weight:300!important}.font-weight-lighter{font-weight:lighter!important}.font-weight-normal{font-weight:400!important}.font-weight-bold{font-weight:700!important}.font-weight-bolder{font-weight:bolder!important}.font-italic{font-style:italic!important}.text-white{color:#fff!important}.text-primary{color:#007bff!important}a.text-primary:focus,a.text-primary:hover{color:#0056b3!important}.text-secondary{color:#6c757d!important}a.text-secondary:focus,a.text-secondary:hover{color:#494f54!important}.text-success{color:#28a745!important}a.text-success:focus,a.text-success:hover{color:#19692c!important}.text-info{color:#17a2b8!important}a.text-info:focus,a.text-info:hover{color:#0f6674!important}.text-warning{color:#ffc107!important}a.text-warning:focus,a.text-warning:hover{color:#ba8b00!important}.text-danger{color:#dc3545!important}a.text-danger:focus,a.text-danger:hover{color:#a71d2a!important}.text-light{color:#f8f9fa!important}a.text-light:focus,a.text-light:hover{color:#cbd3da!important}.text-dark{color:#343a40!important}a.text-dark:focus,a.text-dark:hover{color:#121416!important}.text-body{color:#212529!important}.text-muted{color:#6c757d!important}.text-black-50{color:rgba(0,0,0,.5)!important}.text-white-50{color:rgba(255,255,255,.5)!important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.text-decoration-none{text-decoration:none!important}.text-break{word-break:break-word!important;overflow-wrap:break-word!important}.text-reset{color:inherit!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media print{*,::after,::before{text-shadow:none!important;box-shadow:none!important}a:not(.btn){text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap!important}blockquote,pre{border:1px solid #adb5bd;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}body{min-width:992px!important}.container{min-width:992px!important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #dee2e6!important}.table-dark{color:inherit}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#dee2e6}.table .thead-dark th{color:inherit;border-color:#dee2e6}} +/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/static/img/.DS_Store b/static/img/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 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 Jld40^{{Tp!_q_lB literal 0 HcmV?d00001 diff --git a/test/exam_result.dart b/test/exam_result.dart deleted file mode 100644 index 337eb20a2..000000000 --- a/test/exam_result.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:sit/school/exam_result/entity/result.ug.dart'; - -void main() { - test("Test $ExamResultUg equality", () { - // final a = ExamResultUg( - // score: score, - // courseName: courseName, - // courseCode: courseCode, - // innerClassId: innerClassId, - // year: year, - // semester: semester, - // credit: credit, - // classCode: classCode, - // time: time, - // courseCat: courseCat, - // examType: examType, - // teachers: teachers, - // ); - }); -} diff --git a/tool/args.py b/tool/args.py deleted file mode 100644 index 077f3ac3c..000000000 --- a/tool/args.py +++ /dev/null @@ -1,654 +0,0 @@ -import re -import shlex -from io import StringIO -from typing import Sequence, Iterable, Optional, Union, TypeVar, Callable, Generic - -from indexed import IndexedOrderedDict - -_empty_args = () -T = TypeVar("T") - - -class Arg: - def __init__(self, full: str, key: str, value: str = None): - self.full = full - self.key = key.strip() - if value is not None: - value = value.strip() - self.value = value - self.parent: Optional["Args"] = None - self.parent_index = 0 - - def __copy__(self) -> "Arg": - new = Arg(full=self.full, key=self.key, value=self.value) - new.parent = self.parent - new.parent_index = self.parent_index - return new - - def copy(self, **kwargs) -> "Arg": - cloned = self.__copy__() - for k, v in kwargs.items(): - setattr(cloned, k, v) - return cloned - - @property - def name(self) -> str: - return self.key - - @property - def ispair(self) -> bool: - return self.value is not None - - def startswith(self, prefix: str) -> bool: - return self.key.startswith(prefix) - - def endswith(self, suffix: str) -> bool: - return self.key.endswith(suffix) - - def removeprefix(self, prefix) -> str: - return self.key.removeprefix(prefix) - - def removesuffix(self, suffix) -> str: - return self.key.removesuffix(suffix) - - @staticmethod - def by(arg: str) -> "Arg": - parts = arg.split("=") - if len(parts) == 1: - return Arg(full=arg, key=parts[0]) - else: - if len(parts[0]) == 0: - return Arg(full=arg, key=parts[1]) - else: - return Arg(full=arg, key=parts[0], value=parts[1]) - - def __str__(self): - return self.full - - def __repr__(self): - return str(self) - - @property - def root(self) -> Optional["Args"]: - if self.parent is None: - return None - return self.parent.root - - @property - def raw_index(self) -> int: - if self.parent is None: - return 0 - else: - return self.parent_index + self.parent.total_loffset - - # noinspection PyProtectedMember - def __add__(self, args: "Args") -> "Args": - """ - plus operator overloading - :param args: added at last - :return: a new Args object with no parent - """ - inner = list(args._args) - inner.insert(0, self) - res = Args.lateinit() - res._args = res.copy_args(inner) - return res - - def __eq__(self, b): - if isinstance(b, Arg): - return self.key == b.key and \ - self.value == b.value and \ - self.parent == b.parent and \ - self.parent_index == b.parent_index - else: - return False - - -class ArgPosition: - def __init__(self, start: int, end: int): - self.start = start - self.end = end - - -TArg = TypeVar("TArg", bound=Arg) - - -# noinspection SpellCheckingInspection -class Args(Iterable[TArg]): - """ - Args is a pure data class whose fields are immutable - and all methods are pure that return a new Args object. - - You should never change the inner list [Args.ordered]. - """ - - def __init__(self, args: Sequence[Arg]): - """ - :param args: [lateinit] whose parent and parent_index should be initialized to this. - """ - self._args = args - self.parent: Args | None = None - self.loffset = 0 - """ - the left offset relative to the parent - """ - self.roffset = 0 - """ - the right offset relative to the parent - """ - - @staticmethod - def empty() -> "Args": - return Args(_empty_args) - - def sub(self, start: int, end: int) -> "Args": - """ - :param start: included - :param end: excluded - """ - subargs = [] - sub = Args.lateinit() - for i, arg in enumerate(self._args[start:end]): - subargs.append(arg.copy(parent=sub, parent_index=i)) - sub._args = subargs - sub.parent = self - sub.loffset = start - sub.roffset = len(self._args) - end - return sub - - def __getitem__(self, item: slice | int) -> Union["Args", Arg, None]: - size = len(self._args) - if size == 0: - if isinstance(item, slice) and item.start is None and item.stop is None and item.step is None: - return self.sub_empty() - elif isinstance(item, int): - return None - else: - return self.sub_empty() - if isinstance(item, slice): - start = 0 if item.start is None else item.start - start = max(0, start) - end = size if item.stop is None else item.stop - end = min(end, size) - if start < end: - return self.sub(start, end) - else: - return self.sub_empty() - elif isinstance(item, int): - index = item % size - return self._args[index] - raise Exception(f"unsupported type {type(item)}") - - @property - def total_loffset(self) -> int: - total = 0 - cur = self - while cur is not None: - total += cur.loffset - cur = cur.parent - return total - - @property - def total_roffset(self) -> int: - total = 0 - cur = self - while cur is not None: - total += cur.roffset - cur = cur.parent - return total - - @staticmethod - def by(*, full: str = None, seq: Sequence[str] = None) -> "Args": - if full is not None: - args = shlex.split(full) - elif seq is not None: - args = seq - else: - raise ValueError("neither full nor seq is given") - res = Args.lateinit() - res._args = res.gen_args(args) - return res - - def sub_empty(self) -> "Args": - sub = Args(_empty_args) - sub.parent = self - return sub - - @staticmethod - def lateinit() -> "Args": - return Args(_empty_args) - - def gen_args(self, raw: Sequence[str]) -> Sequence[Arg]: - res = [] - for i, s in enumerate(raw): - arg = Arg.by(s) - arg.parent = self - arg.parent_index = i - res.append(arg) - return res - - def copy_args(self, args: Sequence[Arg]) -> Sequence[Arg]: - res = [] - for i, arg in enumerate(args): - res.append(arg.copy(parent=self, parent_index=i)) - return res - - @property - def size(self): - return len(self) - - @property - def isempty(self) -> bool: - return len(self) == 0 - - def __len__(self): - return len(self._args) - - @property - def hasmore(self): - return self.size > 0 - - def poll(self) -> tuple[Arg | None, "Args"]: - """ - consume the head - """ - if len(self._args) == 0: - return None, self.sub_empty() - else: - return self[0], self[1:] - - def polling(self) -> Iterable[tuple[Arg, "Args"]]: - """ - consuming the head - """ - for i in range(len(self._args)): - yield self[i], self[i + 1:] - - def pop(self) -> tuple[Arg | None, "Args"]: - """ - consume the last - """ - if len(self._args) == 0: - return None, self.sub_empty() - else: - return self[-1], self[0:-1] - - def popping(self) -> Iterable[tuple[Arg, "Args"]]: - """ - consuming the last - """ - for i in range(len(self._args)): - yield self[-i], self[0:-i] - - def __add__(self, arg: Arg | str) -> "Args": - """ - plus operator overloading - :param arg: added at last - :return: a new Args object with no parent - """ - if isinstance(arg, str): - arg = Arg.by(arg) - inner = list(self._args) - inner.insert(0, arg) - res = Args.lateinit() - res._args = res.copy_args(inner) - return res - - def __iter__(self): - return iter(self._args) - - def peekhead(self) -> Arg | None: - if self.hasmore: - return self[0] - else: - return None - - def located_full(self, target: int) -> tuple[str, ArgPosition]: - if self.isroot: - return _join_pos(self._args, target, mapping=str) - else: - raise Exception(f"{self} isn't a root args") - - def full(self) -> str: - if self.isroot: - return _join(self._args, mapping=str) - else: - raise Exception(f"{self} isn't a root args") - - def compose(self) -> Sequence[str]: - return tuple(arg.full for arg in self._args) - - def __str__(self): - return _join(self._args, mapping=str) - - def __repr__(self): - return str(self) - - @property - def isroot(self): - return self.parent is None - - @property - def root(self) -> Optional["Args"]: - cur = self - while cur.parent is not None: - cur = cur.parent - return cur - - -def _join(split_command, mapping: Callable[[T], str] = None): - """Return a shell-escaped string from *split_command*.""" - return ' '.join(_quote(arg) if mapping is None else _quote(mapping(arg)) for arg in split_command) - - -def _join_pos(split_command: Sequence[T], target: int, mapping: Callable[[T], str] = None) -> tuple[str, ArgPosition]: - """ - Return a tuple: - - [0] = shell-escaped string from *split_command* - - [1] = the *target* argument position - """ - size = len(split_command) - start = 0 - end = 0 - counter = 0 - with StringIO() as s: - for i, arg in enumerate(split_command): - if i == target: - start = counter - if mapping is not None: - arg = mapping(arg) - quoted = _quote(arg) - counter += len(quoted) - if i == target: - end = counter - s.write(quoted) - if i < size - 1: - s.write(' ') - counter += 1 - return s.getvalue(), ArgPosition(start, end) - - -_find_unsafe = re.compile(r'[^\w@%+=:,./-]', re.ASCII).search - - -def _quote(s): - """Return a shell-escaped version of the string *s*.""" - if not s: - return "''" - if _find_unsafe(s) is None: - return s - - # use single quotes, and put single quotes into double quotes - # the string $'b is then quoted as '$'"'"'b' - return "'" + s.replace("'", "'\"'\"'") + "'" - - -def split_multicmd(full: Args, separator="+") -> Sequence[Args]: - """ - split multi-cmd by [separator]. - :return: a list of cmd args with no parent - """ - res = [] - queue = [] - args = Args.lateinit() - total = 0 - for i, arg in enumerate(full): - if not arg.ispair and arg.key == separator: - if len(queue) > 0: - args._args = queue - res.append(args) - args = Args.lateinit() - queue = [] - else: - queue.append(arg.copy(parent=args, parent_inex=total - i)) - total += 1 - if len(queue) > 0: - args._args = queue - res.append(args) - return res - - -def _get_or(di: dict, key, fallback) -> T: - if key in di: - return di[key] - else: - v = fallback() - di[key] = v - return v - - -def _append_group(di, group, args): - grouped: list[Args] = _get_or(di, group, fallback=list) - grouped.append(args) - - -class ArgsList: - """ - ArgsList can't be empty. - """ - - def __init__(self, *, first: Args): - """ - :param first: inevitably initialize the ArgsList with at least one Args - """ - self.argslist: list[Args] = [first] - """ - all args in the list. - """ - - def __getitem__(self, index: int) -> Args: - return self.argslist[index] - - def add(self, args: Args): - """ - add the args only if it's not empty - """ - if not args.isempty: - self.argslist.append(args) - - @property - def not_only_one(self) -> Arg | None: - """ - check whether the argslist has 2+ args. - - You can directly check the return value in the if-statement as a bool - :return: the first arg in the second args if argslist has 2+, - otherwise, None will be returned. - """ - if len(self.argslist) == 1: - return None - else: - return self.argslist[1][0] - - def __str__(self): - return str(self.argslist) - - def __repr__(self): - return repr(self.argslist) - - def compose(self) -> list[Arg]: - return flatten_args(self.argslist) - - -class ArgGroup: - def __init__(self, name: str, raw: Arg): - self.name = name - self.raw = raw - - @staticmethod - def by(raw: Arg, prefix: str = "--") -> "ArgGroup": - return ArgGroup(raw.key.removeprefix(prefix), raw) - - -class ArgsGroups: - def __init__(self): - self.name2argslist: dict[str, ArgsList] = IndexedOrderedDict() - """ - group name corresponds to all args in the group. - """ - self.name2group: dict[str, Arg] = IndexedOrderedDict() - """ - group name corresponds to its arg. - Only the first-come group leader arg is recorded. - """ - self.ungrouped: Args | None = None - """ - None means there is nothing ungrouped. - """ - - def add_group(self, leader: Arg, body: Args, head: str = "--"): - name = leader.key.removeprefix(head) - if name not in self.name2group: - self.name2group[name] = leader - if not body.isempty: - if name not in self.name2argslist: - argslist = ArgsList(first=body) - self.name2argslist[name] = argslist - else: - self.name2argslist[name].add(body) - - def has_ungrouped(self) -> bool: - return self.ungrouped is not None - - @property - def groups_size(self) -> int: - return len(self.name2group) - - @property - def argslist_size(self) -> int: - - return len(self.name2argslist) - - def get_args(self, name: str) -> Sequence[Arg]: - if name in self.name2argslist: - return self.name2argslist[name].compose() - else: - return () - - # noinspection PyUnresolvedReferences - def __getitem__(self, item: str | int) -> Arg: - if isinstance(item, str): - return self.name2group[item] - else: - return self.name2group.values()[item] - - def has(self, *, group: str = None, args: str = None) -> bool: - if group is not None: - return group in self.name2group - elif args is not None: - return args in self.name2argslist - else: - raise Exception('no "group" or "args" given') - - def __str__(self): - with StringIO() as s: - s.write("[") - if self.ungrouped is not None: - s.write("ungrouped") - s.write(",") - size = len(self.name2group) - for i, group_name in enumerate(self.name2group.keys()): - s.write(group_name) - if i < size - 1: - s.write(",") - s.write("]") - return s.getvalue() - - def __repr__(self): - return self.__str__() - - -def it_ungrouped(args: Args, group_head: str = "--") -> Iterable[Arg]: - for arg in args: - if not arg.startswith(group_head): - yield arg - else: - break - - -def it_grouped(args: Args, group_head: str = "--") -> Iterable[Arg]: - enable = False - for arg in args: - if enable or arg.startswith(group_head): - enable = True - yield arg - - -def separate_grouped_or_not(args: Args, group_head: str = "--") -> tuple[Args, Args]: - """ - :return: grouped, ungrouped - """ - for i, arg in enumerate(args): - if not arg.ispair and arg.startswith(group_head): - return args[i:], args[0:i] - return args.sub_empty(), args[0:] - - -def indexing_groups(grouped: Args, group_head: str = "--") -> tuple[int, int]: - start = 0 - init_group = grouped[0] - cur_group = init_group - for i, arg in enumerate(grouped): - if not arg.ispair and arg.startswith(group_head): - if cur_group != arg: - cur_group = arg - yield start, i - start = i - if init_group == cur_group: - yield 0, grouped.size - else: - yield start, grouped.size - - -def group_args2(args: Args, group_head: str = "--") -> ArgsGroups: - """ - grouped by "--xxx" as default. - """ - groups = ArgsGroups() - grouped, ungrouped = separate_grouped_or_not(args) - if ungrouped.size > 0: - groups.ungrouped = ungrouped - if grouped.size > 0: - for start, end in indexing_groups(grouped): - group_leader = grouped[start] - group_body = grouped[start + 1:end] - groups.add_group(group_leader, group_body, group_head) - return groups - - -def group_args(args: Args, group_head: str = "--") -> dict[str | None, list[Args]]: - """ - DEPRECATED, see [group_args2] - - grouped by "--xxx" as default. - :return: {*group_name:[*matched_args],None:[ungrouped]} - """ - res = {} - group = None - grouped_start = -1 - cur_group_start = 0 - for i, arg in enumerate(args): - if not arg.ispair and arg.startswith(group_head): - if grouped_start < 0: - grouped_start = i - # it's a group. now group up the former. - if group is not None: - # plus 1 to ignore group name itself - _append_group(res, group, args[cur_group_start + 1:i]) - group = arg.removeprefix(group_head) - cur_group_start = i - if group is not None: - _append_group(res, group, args[cur_group_start + 1:args.size]) - if grouped_start < 0: - grouped_start = args.size - _append_group(res, None, args[0:grouped_start]) - return res - - -def flatten_args( - argslist: list[Args], - mapping: Callable[[Arg], T] = lambda arg: arg -) -> list[T]: - return [(item if mapping is None else mapping(item)) for sublist in argslist for item in sublist] diff --git a/tool/build.py b/tool/build.py deleted file mode 100644 index f9bbee998..000000000 --- a/tool/build.py +++ /dev/null @@ -1,327 +0,0 @@ -from io import StringIO -from typing import Iterator, Any, Callable, TypeVar - -import convert -from convert import Type2Converters -import fuzzy -from cmd import CommandLike, CmdContext -from coroutine import Task, STOP -from utils import cast_int, cast_bool, useRef, Ref - -T = TypeVar("T") - - -def await_input( - ctx: CmdContext, prompt: str, *, ref: useRef, - abort_sign="#" -) -> Task: - """ - when input is a hash sign "#", the coroutine will abort instantly - """ - - def task(): - inputted = ctx.term.input(ctx.style.inputting(prompt)) - if inputted == abort_sign: - yield STOP - else: - ref.obj = inputted - yield - - return task - - -def tint_cmdname(ctx: CmdContext, cmd: CommandLike) -> str: - if hasattr(cmd, "created_by_user"): - if getattr(cmd, "created_by_user"): - return ctx.style.usrname(cmd.name) - return ctx.style.name(cmd.name) - - -_TintFunc = Callable[[str], str] - - -def _build_contents( - ctx: CmdContext, candidates: dict[str, Any], row: int, - tint_num: _TintFunc, tint_name: _TintFunc, -): - s = StringIO() - t = ctx.term - for i, pair in enumerate(candidates.items()): - key, value = pair - if i != 0 and i % row == 0: - t << f"👀 {s.getvalue()}" - s.close() - s = StringIO() - s.write(f"{tint_num(f'#{i}')}={tint_name(key)}\t") - if s.readable(): - t << f"👀 {s.getvalue()}" - s.close() - - -def select_many( - ctx: CmdContext, - candidates: dict[str, Any], - prompt: str, - *, ignore_case=True, - row=4, ref: Ref | Any -) -> Task: - return _select_many( - ctx, candidates, - prompt, ignore_case=ignore_case, - row=row, ref=ref, - tint_num=lambda num: ctx.style.number(num), - tint_name=lambda name: ctx.style.name(name) - ) - - -def select_many_cmds( - ctx: CmdContext, - candidates: dict[str, CommandLike], - prompt: str, - *, ignore_case=True, - row=4, ref: Ref | Any -) -> Task: - return _select_many( - ctx, candidates, - prompt, ignore_case=ignore_case, - row=row, ref=ref, - tint_num=lambda num: ctx.style.number(num), - tint_name=lambda cmd: tint_cmdname(ctx, candidates[cmd]) if cmd in candidates else ctx.style.name(cmd) - ) - - -def _select_many( - ctx: CmdContext, - candidates: dict[str, Any], - prompt: str, - *, ignore_case=True, - row=4, ref: Ref | Any, - tint_num: _TintFunc, tint_name: _TintFunc, -) -> Task: - li = list(candidates.items()) - t = ctx.term - while True: - _build_contents(ctx, candidates, row, tint_num, tint_name) - t << '[multi-select] enter "*" to select all or split each by ",".' - inputted: str = useRef() - yield await_input(ctx, prompt, ref=inputted) - inputted = inputted.strip() - if inputted == "*": - ref.obj = candidates.values() - yield - res = [] - failed = False - for entry in inputted.split(","): - entry = entry.strip() - if entry.startswith("#"): - # numeric mode - entry = entry.removeprefix("#") - num = cast_int(entry) - if num is None: - t << f"❗ {entry} isn't an integer, plz try again." - failed = True - break - else: - key, value = li[num] - res.append(value) - else: - # name mode - if ignore_case: - entry = entry.lower() - # name mode - if entry in candidates: - res.append(candidates[entry]) - else: - t << f'❗ "{entry}" not found, plz try again.' - failed = True - break - if not failed: - ref.obj = res - yield - - -def select_one_cmd( - ctx: CmdContext, - candidates: dict[str, Any], - prompt: str, - *, ignore_case=True, - fuzzy_match=False, - row=4, ref: Ref | Any -) -> Task: - return _select_one( - ctx, candidates, - prompt, ignore_case=ignore_case, - fuzzy_match=fuzzy_match, - row=row, ref=ref, - tint_num=lambda num: ctx.style.number(num), - tint_name=lambda cmd: tint_cmdname(ctx, candidates[cmd]) if cmd in candidates else ctx.style.name(cmd) - ) - - -def select_one( - ctx: CmdContext, - candidates: dict[str, Any], - prompt: str, - *, ignore_case=True, - fuzzy_match=False, - row=4, ref: Ref | Any -) -> Task: - return _select_one( - ctx, candidates, - prompt, ignore_case=ignore_case, - fuzzy_match=fuzzy_match, - row=row, ref=ref, - tint_num=lambda num: ctx.style.number(num), - tint_name=lambda cmd: ctx.style.name(cmd) - ) - - -def _select_one( - ctx: CmdContext, - candidates: dict[str, Any], - prompt: str, - *, ignore_case=True, - fuzzy_match=False, - row=4, ref: Ref | Any, - tint_num: _TintFunc, tint_name: _TintFunc, -) -> Task: - def task() -> Iterator: - t = ctx.term - li = list(candidates.items()) - while True: - _build_contents(ctx, candidates, row, tint_num, tint_name) - inputted: str = useRef() - yield await_input(ctx, prompt, ref=inputted) - inputted = inputted.strip() - if inputted.startswith("#"): - # numeric mode - inputted = inputted.removeprefix("#") - num = cast_int(inputted) - if num is None: - t << f"❗ {inputted} isn't an integer, plz try again." - else: - if 0 <= num < len(li): - key, value = li[num] - ref.obj = value - yield - else: - t << f"❗ {num} is out of range[0,{len(li)}), plz try again." - else: - if ignore_case: - inputted = inputted.lower() - # name mode - if inputted in candidates: - ref.obj = candidates[inputted] - yield - elif fuzzy_match: - matched, radio = fuzzy.match(inputted, candidates.keys()) - if radio >= fuzzy.at_least: - t << f'do you mean "{matched}"?' - inputted = useRef() - yield await_input(ctx, prompt="y/n=", ref=inputted) - inputted = inputted.strip() - if inputted == "" or cast_bool(inputted): - ref.obj = candidates[matched] - yield - else: - t << f"alright, let's start all over again." - else: - t << f'❗ "{inputted}" not found, plz try again.' - else: - t << f'❗ "{inputted}" not found, plz try again.' - - return task - - -def input_multiline( - ctx: CmdContext, prompt: Callable[[list[str]], str], - end_sign="#END", *, ref: Ref | Any -) -> Task: - lines = [] - - def task() -> Iterator: - while True: - end_sign_tip = ctx.style.highlight("#END") - ctx.term << f'enter "{end_sign_tip}" to end multi-line' - yield await_input(ctx, prompt=prompt(lines), ref=(line := useRef())) - line = line.strip() - if line == end_sign: - break - lines.append(line) - yield - - ref.obj = lines - return task - - -def yes_no( - ctx: CmdContext, *, ref: Ref | Any -) -> Task: - def task() -> Iterator: - inputted: str = useRef() - yield await_input(ctx, prompt="y/n=", ref=inputted) - reply = inputted.strip() - ref.obj = cast_bool(reply) - yield - - return task - - -AttrViewer = Callable[[T], Iterator[tuple[str, Any]]] - - -def respect_private_viewer(obj: Any) -> Iterator[tuple[str, Any]]: - for name, value in vars(obj).items(): - if not name.startswith("_"): - yield name, value - - -def replace_settings( - ctx: CmdContext, *, - obj: T, viewer: AttrViewer = respect_private_viewer, - converters: Type2Converters | Callable[[], Type2Converters] = lambda: convert.builtins -) -> Task: - def task() -> Iterator: - s = ctx.style - skip_sign_tip = s.highlight("#SKIP") - end_sign_tip = s.highlight("#END") - ctx.term << f'enter "{skip_sign_tip}" to skip, "{end_sign_tip}" to interrupt' - type2cnvt = converters if isinstance(converters, dict) else converters() - end = False - for name, value in viewer(obj): - t = type(value) - cnvt = type2cnvt[t] - ctx.term << f"{s.name(name)}:{s.type(t.__name__)}={s.value(cnvt.to_str(value))}" - while True: - yield await_input(ctx, prompt=f"{name}=", ref=(ref := useRef())) - new_raw = ref.deref() - if new_raw == "#SKIP": - break - elif new_raw == "#END": - end = True - break - else: - new_value = cnvt.from_str(new_raw) - if new_value is None: - ctx.term << "failed to convert, plz try again." - continue - else: - setattr(obj, name, new_value) - break - if end: - break - yield - - return task - - -def settings_from_str( - obj: Any, settings: dict[str, str], - converters: Type2Converters | Callable[[], Type2Converters] = lambda: convert.builtins, -): - type2cnvt = converters if isinstance(converters, dict) else converters() - for name, value in settings.items(): - if hasattr(obj, name): - original = getattr(obj, name) - cnvt = type2cnvt[type(original)] - setattr(obj, name, cnvt.from_str(value)) diff --git a/tool/cmd.py b/tool/cmd.py deleted file mode 100644 index 78257e9ca..000000000 --- a/tool/cmd.py +++ /dev/null @@ -1,280 +0,0 @@ -import traceback -from io import StringIO -from typing import Iterable, runtime_checkable, Protocol, Iterator, Any, Callable - -import fuzzy -import strings -from args import Args, Arg, split_multicmd -from project import Proj -from style import Style -from ui import Terminal - -_default_style = Style() - -max_ctx_depth = 64 - - -class CmdContext: - def __init__( - self, proj: Proj, terminal: Terminal, cmdlist: "CommandList", - args: Args = None, style: Style = _default_style): - self.proj = proj - self.term = terminal - self.cmdlist = cmdlist - self.args = args - self.style = style - self.depth = 0 - - @property - def is_cli(self) -> bool: - return self.args is not None - - def __str__(self): - return f"{self.proj},{self.args}" - - def __repr__(self): - return str(self) - - def __copy__(self) -> "CmdContext": - return CmdContext(self.proj, self.term, self.cmdlist, self.args) - - def copy(self, **kwargs) -> "CmdContext": - """ - return a recursion-aware copy - """ - cloned = self.__copy__() - for k, v in kwargs.items(): - setattr(cloned, k, v) - cloned.depth = self.depth + 1 - return cloned - - -@runtime_checkable -class CommandLike(Protocol): - """ - optional: - - created_by_user: bool -- whether this command is created by user, [False] as default. - """ - name: str - """the name of command""" - - def execute_cli(self, ctx: CmdContext): - """execute the command in cli mode""" - pass - - def execute_interactive(self, ctx: CmdContext) -> Iterator: - """execute the command in interactive mode. return an Iterator as a coroutine""" - pass - - def help(self, ctx: CmdContext): - """help info of command""" - pass - - -# noinspection SpellCheckingInspection -class CommandList: - name2cmd: dict[str, CommandLike] - - def __init__(self, logger=None): - self.name2cmd = {} - self.builtins = set() - self.logger = logger - - def log(self, *args): - if self.logger is not None: - self.logger.log(*args) - - def add_cmd(self, name: str, cmd: CommandLike): - name = name.lower() - if name in self.name2cmd: - raise Exception(f"{name} command has already registered") - self.log(f"command<{name}> loaded.") - self.name2cmd[name] = cmd - - def is_builtin(self, name: str) -> bool: - return name in self.builtins - - def __setitem__(self, key: str, cmd: CommandLike): - self.add_cmd(key, cmd) - - def __getitem__(self, name: str) -> CommandLike | None: - if name not in self.name2cmd: - return None - else: - return self.name2cmd[name] - - def add(self, cmd: CommandLike | Any): - self.add_cmd(cmd.name, cmd) - - def __lshift__(self, cmd: CommandLike | Any): - self.add(cmd) - - @property - def size(self): - return len(self.name2cmd) - - @property - def isempty(self): - return self.size > 0 - - def __contains__(self, name: str) -> bool: - return name in self.name2cmd - - def __iter__(self): - return iter(self.name2cmd.items()) - - def keys(self): - return iter(self.name2cmd.keys()) - - def values(self): - return iter(self.name2cmd.values()) - - def items(self): - return iter(self.name2cmd.items()) - - def fuzzy_match(self, name: str, threshold: float) -> CommandLike | None: - candidate, radio = fuzzy.match(name, self.name2cmd.keys()) - if radio < threshold: - return None - else: - return self.name2cmd[candidate] - - def __str__(self): - return f"[{', '.join(self.name2cmd.keys())}]" - - def __repr__(self): - return str(self) - - def __len__(self) -> int: - return self.size - - def browse_by_page(self, cmd_per_page: int) -> Iterable[tuple[int, Iterable[CommandLike]]]: - cur = [] - for i, cmd in enumerate(self.name2cmd.values()): - if i % cmd_per_page == 0 and i != 0: - yield i // cmd_per_page, cur - cur.clear() - else: - cur.append(cmd) - if len(cur) > 0: - yield (self.size - 1) // cmd_per_page, cur - - def calc_total_page(self, cmd_per_page: int) -> int: - return self.size // cmd_per_page - - -class CommandArgError(Exception): - def __init__(self, cmd: CommandLike | Any, arg: Arg | None, *more): - super(CommandArgError, self).__init__(*more) - self.arg = arg - self.cmd = cmd - - -class CommandEmptyArgsError(Exception): - def __init__(self, cmd: CommandLike | Any, cmdargs: Args, *more): - super(CommandEmptyArgsError, self).__init__(*more) - self.cmdargs = cmdargs - self.cmd = cmd - - -class CommandExecuteError(Exception): - def __init__(self, cmd: CommandLike | Any, *args): - super(CommandExecuteError, self).__init__(*args) - self.cmd = cmd - - -def print_cmdarg_error(ctx: CmdContext, e: CommandArgError): - index = e.arg.raw_index - full, pos = e.arg.root.located_full(index) - t = ctx.term - _er0 = ctx.style.error('×') - _er1 = ctx.style.error('│') - _er2 = ctx.style.error('╰─>') - - t.both << f"{_er0} {full[:pos.start]}{ctx.style.highlight(full[pos.start:pos.end])}{full[pos.end:]}" - with StringIO() as s: - s.write(_er1) - s.write(" ") - s.write(strings.repeat(pos.start)) - s.write(ctx.style.arrow(strings.repeat(pos.end - pos.start, "^"))) - t.both << s.getvalue() - t.both << ctx.style.error(f'╰─> {type(e).__name__}: {e}') - - -def print_cmdargs_empty_error(ctx: CmdContext, e: CommandEmptyArgsError): - full = e.cmdargs.root.full() - t = ctx.term - _er0 = ctx.style.error('×') - _er1 = ctx.style.error('│') - _er2 = ctx.style.error('╰─>') - - _arrow = ctx.style.arrow("^") - - t.both << f"{_er0} {full}" - with StringIO() as s: - s.write(strings.repeat(len(full))) - s.write(_arrow) - t.both << f"{_er1} {s.getvalue()}" - t.both << ctx.style.error(f'╰─> {type(e).__name__}: {e}') - - -class CommandDelegate(CommandLike): - created_by_user = True - - def __init__(self, name: str, fullargs: str, helpinfo: str): - self.name = name - self.fullargs = fullargs - self.helpinfo = helpinfo - - def execute(self, ctx: CmdContext): - if ctx.depth >= max_ctx_depth: - raise CommandExecuteError(self, "recursive executing detected") - args = Args.by(full=self.fullargs) - all_cmdargs = split_multicmd(args) - # prepare commands to run - exe_args = [] - # check if all of them are executable - for command, args in (args.poll() for args in all_cmdargs): - cmdname = command.full - executable = ctx.cmdlist[cmdname] - if executable is None: - raise CommandArgError(self, command, f"command not found") - exe_args.append((executable, args)) - for i, pair in enumerate(exe_args): - executable, args = pair - subctx = ctx.copy(args=args) - executable.execute_cli(subctx) - - def execute_cli(self, ctx: CmdContext): - self.execute(ctx) - - def execute_interactive(self, ctx: CmdContext) -> Iterator: - self.execute(ctx) - yield - - def help(self, ctx: CmdContext): - for line in self.helpinfo.splitlines(): - ctx.term << line - - -def log_traceback(t: Terminal): - if t.has_logger: - t.logging << traceback.format_exc() - t << "ℹ️ full traceback was printed into log." - - -def catch_executing( - ctx: CmdContext, - executing: Callable[[], Any] -) -> Any: - try: - return executing() - except CommandArgError as e: - print_cmdarg_error(ctx, e) - log_traceback(ctx.term) - except CommandEmptyArgsError as e: - print_cmdargs_empty_error(ctx, e) - log_traceback(ctx.term) - except CommandExecuteError as e: - ctx.term.both << ctx.style.error(f"{type(e).__name__}: {e}") - log_traceback(ctx.term) diff --git a/tool/cmds/__init__.py b/tool/cmds/__init__.py deleted file mode 100644 index 25111cc24..000000000 --- a/tool/cmds/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -from cmds.add_module import AddModuleCmd -from cmds.lint import LintCmd -from cmds.run import RunCmd -from cmds.alias import AliasCmd -from cmds.l10n import L10nCmd -from cmds.cli import CliCmd -from cmds.native_cmd import NativeCmd -from cmd import CommandList - - -def load_static_cmd(cmdlist: CommandList): - cmdlist << AddModuleCmd - # cmdlist << RunCmd - cmdlist << LintCmd - cmdlist << AliasCmd - cmdlist << L10nCmd - cmdlist << CliCmd - cmdlist << NativeCmd("git") - cmdlist << NativeCmd("flutter") - cmdlist << NativeCmd("dart") diff --git a/tool/cmds/add_module.py b/tool/cmds/add_module.py deleted file mode 100644 index 2c377ed4c..000000000 --- a/tool/cmds/add_module.py +++ /dev/null @@ -1,128 +0,0 @@ -from typing import Callable, TypeVar, Sequence, Iterable - -from args import Args, Arg, group_args, flatten_args -from build import await_input, select_many -from cmd import CmdContext, CommandEmptyArgsError, CommandArgError -from project import ModuleCreation, CompType, UsingDeclare -from utils import useRef - -Mode = Callable[[Arg], None] - - -def _check_name(ctx: CmdContext, name_argslist: list[Args]) -> tuple[Arg, str]: - size = len(name_argslist) - if size == 0: - raise CommandEmptyArgsError(AddModuleCmd, ctx.args, "no arg given") - elif size == 1: - name_args = name_argslist[0] - if name_args.size > 1: - raise CommandArgError(AddModuleCmd, name_args[1], "redundant name specified") - else: - name_arg = name_args[0] - if name_arg.ispair: - raise CommandArgError(AddModuleCmd, name_arg, "arg can't be a pair") - else: - return name_arg, name_arg.key - else: - raise CommandArgError(AddModuleCmd, name_argslist[1][0], "redundant name specified") - - -def _get_list(ctx: CmdContext, name: str, grouped: dict[str, list[Args]], optional=False) -> Sequence[Arg]: - if name not in grouped: - if optional: - return () - raise CommandEmptyArgsError(AddModuleCmd, ctx.args, f"no arg<{name}> given") - else: - return flatten_args(grouped[name]) - - -T = TypeVar("T") - - -def _resolve(total: dict[str, T], args: Sequence[Arg], kind: str) -> tuple[T]: - included = set() - excluded = set() - - def try_find() -> T: - if name not in total: - raise CommandArgError(AddModuleCmd, arg, f"{name} isn't a {kind}") - else: - return total[name] - - for arg in args: - name = arg.full - if name == "*": - for c in total.values(): - included.add(c) - else: - if name.startswith("~"): - name = name.removeprefix("~") - excluded.add(try_find()) - else: - included.add(try_find()) - return tuple(included - excluded) - - -class AddModuleCmd: - name = "addmodule" - - @staticmethod - def execute_cli(ctx: CmdContext): - grouped = group_args(ctx.args) - # find name - if "n" in grouped: - name_arg, name = _check_name(ctx, grouped["n"]) - else: - ungrouped = grouped[None] - name_arg, name = _check_name(ctx, ungrouped) - if name in ctx.proj.modules: - raise CommandArgError(AddModuleCmd, name_arg, f"module<{name}> already exists") - # get names - component_names = _get_list(ctx, "c", grouped) - using_names = _get_list(ctx, "u", grouped, optional=True) - # resolve - components = _resolve(ctx.proj.comps, component_names, "component") - usings = _resolve(ctx.proj.usings, using_names, "using") - # creating - res = ModuleCreation(name, components, usings) - ctx.term << f"{name=}." - ctx.term << f"{components=}." - ctx.term << f"{usings=}." - ctx.proj.modules.create(res) - ctx.term << f"module<{name}> added." - - @staticmethod - def execute_interactive(ctx: CmdContext) -> Iterable: - t = ctx.term - t << "plz enter a unique module name" - while True: - nameRef: str = useRef() - yield await_input(ctx, "name=", ref=nameRef) - name = nameRef.strip() - if name in ctx.proj.modules: - t << f"module<{name}> already exists, plz select another one" - else: - break - t << "plz enter what components to add" - components: Sequence[CompType] = useRef() - yield select_many(ctx, ctx.proj.comps, prompt="comps=", ref=components) - components = tuple(components) - t << "plz enter what features to import" - usings: Sequence[UsingDeclare] = useRef() - yield select_many(ctx, ctx.proj.usings, prompt="import ", ref=usings) - usings = tuple(usings) - # creating - res = ModuleCreation(name, components, usings) - ctx.term << f"{name=}." - ctx.term << f"{components=}." - ctx.term << f"{usings=}." - ctx.proj.modules.create(res) - ctx.term << f"module<{name}> added." - - @staticmethod - def help(ctx: CmdContext): - t = ctx.term - t << "addmodule --n --c <..components> --u <..using>" - t << '|-- ..components: *=all, "~"-prefix=exclude' - t << '|-- ..using: *=all, "~"-prefix=exclude' - t << '|-- eg: addmodule --n test --c entity service dao --u l10n' diff --git a/tool/cmds/alias.py b/tool/cmds/alias.py deleted file mode 100644 index da47c8552..000000000 --- a/tool/cmds/alias.py +++ /dev/null @@ -1,107 +0,0 @@ -from typing import Iterable - -import project -from args import group_args, Args -from build import input_multiline, yes_no, await_input -from cmd import CmdContext, CommandEmptyArgsError, CommandArgError -from project import ExtraCommandEntry, ExtraCommandsConf -from utils import Ref, useRef - - -def _get_arg(grouped: dict[str | None, list[Args]], argname: str, allow_empty=False) -> str | None: - if argname not in grouped and allow_empty: - return None - n_argslist = grouped[argname] - if len(n_argslist) > 1: - raise CommandArgError(AliasCmd, n_argslist[1][0], f"redundant arg<{argname}> provided") - n_args = n_argslist[0] - if n_args.size == 0: - if allow_empty: - return None - raise CommandEmptyArgsError(AliasCmd, n_args, f"arg<{argname}> is empty") - elif n_args.size == 1: - n_arg = n_args[0] - return n_arg.full - else: - return n_args.full() - - -class AliasCmd: - name = "alias" - - @staticmethod - def add_cmd(conf: ExtraCommandsConf, name: str, fullargs: str, helpinfo: str): - entry = ExtraCommandEntry(name=name, fullargs=fullargs, helpinfo=helpinfo) - conf[name] = entry - - @staticmethod - def execute_cli(ctx: CmdContext): - if ctx.args.isempty: - raise CommandEmptyArgsError(AliasCmd, ctx.args, "no command name given") - t = ctx.term - grouped = group_args(ctx.args) - ungrouped_args = grouped[None][0] - if not ungrouped_args.isempty: - alias_arg = ungrouped_args[0] - if not alias_arg.ispair: - raise CommandArgError(AliasCmd, alias_arg, 'plz match format') - else: - name = alias_arg.key - args = alias_arg.value - else: - name = _get_arg(grouped, argname="n", allow_empty=False) - args = _get_arg(grouped, argname="args", allow_empty=False) - info = _get_arg(grouped, argname="info", allow_empty=True) - if info is None: - info = "" - conf = ctx.proj.settings.get(project.extra_commands, settings_type=ExtraCommandsConf) - AliasCmd.add_cmd(conf, name, args, info) - t.both << f'command<{name}> added.' - ctx.proj.kernel.reloader.reload_cmds() - - @staticmethod - def execute_interactive(ctx: CmdContext) -> Iterable: - t = ctx.term - # Enter name - t << f"plz enter a unique name." - while True: - inputted: str = useRef() - yield await_input(ctx, prompt="name=", ref=inputted) - name = inputted.strip() - if ctx.cmdlist.is_builtin(name): - t << f"❌ {name} is a builtin command." - else: - break - t.logging << f"name is {name}" - t << f'plz enter cmd&args.' - # Enter args - inputted: Ref = useRef() - yield input_multiline(ctx, prompt=lambda res: "+ " if len(res) > 0 else "args=", ref=inputted) - lines: list[str] = inputted.deref() - fullargs = " + ".join(lines) - t.logging << f"{fullargs=}" - # Enter help into - t << f'plz enter help info.' - inputted: Ref = useRef() - yield input_multiline(ctx, prompt=lambda res: "\\n " if len(res) > 0 else "info=", ref=inputted) - info: list[str] = inputted.deref() - helpinfo = "\n".join(info) - t.logging << f"{helpinfo=}" - conf = ctx.proj.settings.get(project.extra_commands, settings_type=ExtraCommandsConf) - if name in conf: - confirm: bool = useRef() - t << f"{ctx.style.usrname(name)} already exists, confirm to override it?" - yield yes_no(ctx, ref=confirm) - if not confirm: - t.both << f"adding command<{name}> aborted" - return - AliasCmd.add_cmd(conf, name, fullargs, helpinfo) - t.both << f'command<{name}> added.' - ctx.proj.kernel.reloader.reload_cmds() - - @staticmethod - def help(ctx: CmdContext): - t = ctx.term - t << "add an alias of commands" - t << "| alias --n --args [--info ]" - t << '| alias cmd_name="full args" [--info ]' diff --git a/tool/cmds/bg.py b/tool/cmds/bg.py deleted file mode 100644 index a1b52506c..000000000 --- a/tool/cmds/bg.py +++ /dev/null @@ -1,22 +0,0 @@ -from typing import Iterator - -from cmd import CmdContext - - -class BgCmd: - name = "bg" - - @staticmethod - def execute_cli(ctx: CmdContext): - pass - - @staticmethod - def execute_interactive(ctx: CmdContext) -> Iterator: - yield - - @staticmethod - def help(ctx: CmdContext): - t = ctx.term - t << "bg <..args>: run a command in background" - t << "bg --l: list all background tasks" - t << "bg --kill : kill all tasks matched" diff --git a/tool/cmds/cli.py b/tool/cmds/cli.py deleted file mode 100644 index 188f179d9..000000000 --- a/tool/cmds/cli.py +++ /dev/null @@ -1,78 +0,0 @@ -from typing import Iterator - -import fuzzy -from args import Args, split_multicmd -from build import await_input -from cmd import CmdContext, CommandExecuteError, CommandArgError, catch_executing -from style import Style -from tui.colortxt import FG -from tui.colortxt.txt import Palette -from utils import useRef, cast_bool - -_cli_style = Style({ - "inputting": Palette(fg=FG.Magenta) -}) - - -class CliCmd: - name = "cli" - - @staticmethod - def execute_cli(ctx: CmdContext): - raise CommandExecuteError(CliCmd, "only admit interactive mode") - - @staticmethod - def execute_interactive(ctx: CmdContext) -> Iterator: - t = ctx.term - t << "enter the command prompts to run." - while True: - yield await_input(ctx, prompt="/", ref=(cmdargs := useRef())) - cmdargs = Args.by(full=cmdargs.deref()) - all_cmdargs = split_multicmd(cmdargs) - cmd_size = len(all_cmdargs) - if cmd_size == 0: - continue - elif cmd_size == 1: - args = all_cmdargs[0] - command, args = args.poll() - if command.ispair: - raise CommandArgError(CliCmd, command, f'invalid command format') - cmdname = command.key - executable = ctx.cmdlist[cmdname] - if executable is None: - matched, ratio = fuzzy.match(cmdname, ctx.cmdlist.name2cmd.keys()) - if matched is not None and ratio > fuzzy.at_least: - t << f'👀 command<{cmdname}> not found, do you mean command<{matched}>?' - yield await_input(ctx, prompt="y/n=", ref=(reply := useRef())) - inputted = reply.strip() - if inputted == "" or cast_bool(inputted): - executable = ctx.cmdlist[matched] - else: - t << f"alright, let's start all over again." - continue - else: - raise CommandArgError(CliCmd, command, "command not found") - subctx = ctx.copy(args=args, style=_cli_style) - catch_executing(ctx, executing=lambda: executable.execute_cli(subctx)) - else: - # prepare commands to run - exe_args = [] - # check if all of them are executable - for command, args in (args.poll() for args in all_cmdargs): - if command.ispair: - raise CommandArgError(CliCmd, command, f'invalid command format') - cmdname = command.key - executable = ctx.cmdlist[cmdname] - if executable is None: - raise CommandArgError(CliCmd, command, "command not found") - exe_args.append((executable, args)) - for i, pair in enumerate(exe_args): - executable, args = pair - subctx = ctx.copy(args=args, style=_cli_style) - catch_executing(ctx, executing=lambda: executable.execute_cli(subctx)) - - @staticmethod - def help(ctx: CmdContext): - t = ctx.term - t << "cli" - t << "|-- enter the cli mode" diff --git a/tool/cmds/help.py b/tool/cmds/help.py deleted file mode 100644 index 036454ed2..000000000 --- a/tool/cmds/help.py +++ /dev/null @@ -1,107 +0,0 @@ -from typing import Iterable, Iterator - -import build -from args import group_args, Args -from cmd import CmdContext, CommandList, CommandArgError, CommandEmptyArgsError, CommandLike -from ui import Terminal -from utils import useRef, cast_int - - -class HelpBoxTerminal(Terminal): - - def __init__(self, inner: Terminal): - super().__init__() - self.inner = inner - - def print(self, *args): - self.inner.print("|", *args) - - def log(self, *args): - self.inner.log(*args) - - def input(self, prompt: str) -> str: - return self.inner.input(f"| {prompt}") - - def print_log(self, *args): - self.print(*args) - self.log(*args) - - -def create_page4show(cmdlist: CommandList, page: set[int], cmd_per_page: int) -> Iterable[CommandLike]: - for pagenum, cmds in cmdlist.browse_by_page(cmd_per_page): - if pagenum in page: - for cmd in cmds: - yield cmd - - -class HelpCmd(CommandLike): - def __init__(self, cmdlist: CommandList): - self.name = "help" - self.cmdlist = cmdlist - self.cmd_per_page = 5 - - def execute_interactive(self, ctx: CmdContext) -> Iterator: - # all_cmd = ', '.join(ctx.cmdlist.keys()) - # ctx.term << f"all commands = [{all_cmd}]" - while True: - ctx.term << f'plz select commands to show info.' - selected: list[CommandLike] = useRef() - yield build.select_many_cmds(ctx, ctx.cmdlist.name2cmd, prompt="I want=", ref=selected) - ctx.term.line(48) - help_ctx = ctx.copy(term=HelpBoxTerminal(ctx.term)) - for cmd in selected: - HelpCmd.show_help_info(cmd, ctx, help_ctx) - ctx.term.line(48) - - def execute_cli(self, ctx: CmdContext): - grouped = group_args(ctx.args) - cmd_args: Args = grouped[None][0] - if cmd_args.isempty: - raise CommandEmptyArgsError(self, cmd_args, "no command name given") - cmdname_arg = cmd_args[0] - if cmdname_arg.ispair and cmdname_arg.key == "name": - cmdname = cmdname_arg.value - else: - cmdname = cmdname_arg.key - # Display board - help_ctx = ctx.copy(term=HelpBoxTerminal(ctx.term)) - if cmdname == "*": # show all - if "p" in grouped: - page_group = grouped["p"] - pages = set() - for page in page_group: - pagenum_arg = page[0] - if pagenum_arg is not None: - if pagenum_arg.ispair: - raise CommandArgError(self, pagenum_arg, "❌ arg

can't be a pair") - pagenum = cast_int(pagenum_arg.key) - if pagenum is None: - raise CommandArgError( - self, pagenum_arg, f"❌ {pagenum_arg.key} isn't valid page number") - pages.add(pagenum) - else: - raise CommandArgError(self, pagenum_arg, "❌ arg

is empty") - for cmd_obj in create_page4show(ctx.cmdlist, pages, self.cmd_per_page): - HelpCmd.show_help_info(cmd_obj, ctx, help_ctx) - ctx.term << f"total page: {ctx.cmdlist.calc_total_page(self.cmd_per_page)}" - else: - for cmd_obj in ctx.cmdlist.values(): - HelpCmd.show_help_info(cmd_obj, ctx, help_ctx) - - else: # show specified - cmd_obj = ctx.cmdlist[cmdname] - if cmd_obj is None: - raise CommandArgError(self, cmdname_arg, f"❌ command<{cmdname}> not found") - HelpCmd.show_help_info(cmd_obj, ctx, help_ctx) - - @staticmethod - def show_help_info(cmd: CommandLike, ctx: CmdContext, help_box: CmdContext): - ctx.term << build.tint_cmdname(ctx, cmd) - cmd.help(help_box) - - def help(self, ctx: CmdContext): - ctx.term << 'help ' - ctx.term << "| show command's info" - ctx.term << 'help *' - ctx.term << '| show all commands info' - ctx.term << '| --p [int]: specify the page to show' diff --git a/tool/cmds/l10n.py b/tool/cmds/l10n.py deleted file mode 100644 index 31de4aef1..000000000 --- a/tool/cmds/l10n.py +++ /dev/null @@ -1,142 +0,0 @@ -from threading import Thread -from typing import Iterable, Callable - -import fuzzy -from build import select_one, await_input -from cmd import CmdContext, CommandArgError, CommandEmptyArgsError, CommandLike -from cmds.shared import print_stdout -from filesystem import File -from project import Proj -from utils import useRef, cast_bool - - -class ServeTask: - name = "l10n_serve" - - def __init__(self): - self.running = True - - def terminate(self): - self.running = False - - -def template_and_others(proj: Proj) -> tuple[File, list[File]]: - template = proj.template_arb_fi - template_name = template.name - others = [] - for fi in proj.l10n_dir.listing_fis(): - if fi.name != template_name: - others.append(fi) - return template, others - - -def resort(ctx: CmdContext): - import l10n.resort as res - for fi in ctx.proj.l10n_dir.listing_fis(): - if fi.extendswith("arb"): - new = res.resort(fi.read(), res.methods[res.Alphabetical]) - fi.write(new) - ctx.term << f"{fi.path} resorted." - - -def rename(ctx: CmdContext) -> Iterable: - import l10n.rename as res - from l10n.arb import load_arb_from - template, others = template_and_others(ctx.proj) - template_arb = load_arb_from(path=str(template.path)) - while True: - while True: - yield await_input(ctx, "old=", ref=(inputted := useRef())) - old = inputted.obj.strip() - if old in template_arb.pmap: - break - else: - matched, ratio = fuzzy.match(old, template_arb.pmap.items()) - if ratio > fuzzy.at_least: - ctx.term << f'"{old}" not found, do you mean "{matched}"?' - yield await_input(ctx, "y/n=", ref=inputted) - if cast_bool(inputted): - old = matched - break - else: - ctx.term << "alright, let's start all over again." - - yield await_input(ctx, "new=", ref=inputted) - new = inputted.obj.strip() - res.rename_key_by(template=template_arb, others=[load_arb_from(path=str(f.path)) for f in others], - old=old, new=new, - terminal=ctx.term, - auto_add=True) - - -def serve(ctx: CmdContext): - import l10n.serve as ser - template, others = template_and_others(ctx.proj) - task = ServeTask() - - def run(): - try: - ser.start( - str(template.path), - [str(f.path) for f in others], - terminal=ctx.term, - is_running=lambda: task.running) - except Exception as e: - ctx.term << str(e) - - thread = Thread(target=run) - thread.daemon = True - thread.start() - ctx.proj.kernel.background << task - ctx.term << "l10n is serving in background" - - -def gen(ctx: CmdContext): - proc = ctx.proj.dartRunner.flutter(["gen-l10n"]) - print_stdout(ctx, proc) - - -name2function: dict[str, Callable[[CmdContext], None]] = { - "resort": resort, - "serve": serve, - "gen": gen -} - - -class L10nCmd: - name = "l10n" - - @staticmethod - def execute_cli(ctx: CmdContext): - args = ctx.args - if len(args) == 0: # only None - raise CommandEmptyArgsError(L10nCmd, args, "no function specified") - elif len(args) == 1: - func_name = args[0].full.removeprefix("--") - if func_name not in name2function: - raise CommandArgError(L10nCmd, args[0], f"function<{func_name}> not found") - else: - func = name2function[func_name] - func(ctx) - else: # including None - raise CommandArgError(L10nCmd, args[1], "only allow one function") - - @staticmethod - def execute_interactive(ctx: CmdContext) -> Iterable: - while True: - selected = useRef() - funcs = dict(name2function) - funcs.update({ - "rename": rename - }) - yield select_one(ctx, funcs, prompt="func=", fuzzy_match=True, ref=selected) - if selected == rename: - yield rename(ctx) - else: - selected(ctx) - - @staticmethod - def help(ctx: CmdContext): - t = ctx.term - t << "l10 resort: resort .arb files alphabetically" - t << "l10 serve: watch the change of .arb files" diff --git a/tool/cmds/lint.py b/tool/cmds/lint.py deleted file mode 100644 index af311444f..000000000 --- a/tool/cmds/lint.py +++ /dev/null @@ -1,29 +0,0 @@ -from typing import Iterable - -from cmd import CmdContext -from cmds.shared import print_stdout -from dart import DartFormatConf - - -def lint(ctx: CmdContext): - conf = ctx.proj.settings.get("cmd.lint.conf", DartFormatConf) - proc = ctx.proj.dartRunner.format(conf) - print_stdout(ctx, proc) - - -class LintCmd: - name = "lint" - - @staticmethod - def execute_cli(ctx: CmdContext): - lint(ctx) - - @staticmethod - def execute_interactive(ctx: CmdContext) -> Iterable: - lint(ctx) - yield - - @staticmethod - def help(ctx: CmdContext): - t = ctx.term - t << "lint: format .dart files" diff --git a/tool/cmds/native_cmd.py b/tool/cmds/native_cmd.py deleted file mode 100644 index 375dadd3e..000000000 --- a/tool/cmds/native_cmd.py +++ /dev/null @@ -1,33 +0,0 @@ -from typing import Iterable - -import multiplatform -from args import Args, Arg -from build import await_input -from cmd import CmdContext -from cmds.shared import print_stdout -from utils import useRef - - -def run_native_cmd(ctx: CmdContext, args: Args): - proc = ctx.proj.runner.run(seq=args.compose()) - print_stdout(ctx, proc) - - -class NativeCmd: - def __init__(self, name: str): - self.name = name - - def execute_cli(self, ctx: CmdContext): - git_args = Arg.by(self.name) + ctx.args - run_native_cmd(ctx, git_args) - - def execute_interactive(self, ctx: CmdContext) -> Iterable: - while True: - yield await_input(ctx, prompt=f"{self.name} ", ref=(argsRef := useRef())) - args = Args.by(full=argsRef.deref()) - git_args = Arg.by(self.name) + args - run_native_cmd(ctx, git_args) - - def help(self, ctx: CmdContext): - t = ctx.term - t << f"the same as the native command<{self.name}>, and plz ensure it in your {multiplatform.envvar('PATH')}." diff --git a/tool/cmds/run.py b/tool/cmds/run.py deleted file mode 100644 index 80791700b..000000000 --- a/tool/cmds/run.py +++ /dev/null @@ -1,83 +0,0 @@ -from typing import Iterable, Any - -from args import Args -from cmd import CmdContext, CommandLike, CommandArgError, CommandExecuteError, CommandEmptyArgsError -from project import KiteScript -from ui import Terminal - - -class RunTerminal(Terminal): - - def __init__(self, inner: Terminal): - super().__init__() - self.inner = inner - - def print(self, *args): - self.inner.print("| ", *args) - - def log(self, *args): - self.inner.log(*args) - - def input(self, prompt: str) -> str: - return self.inner.input(f"| {prompt}") - - def print_log(self, *args): - self.print(*args) - self.log(*args) - - -def run_cmd(parent: CmdContext, raw_args: str): - args = Args.by(full=raw_args) - if args.isempty: - raise CommandExecuteError(RunCmd, "no command specified") - arg0, args = args.poll() - cmd = parent.cmdlist[arg0.name] - if cmd is None: - raise CommandArgError(RunCmd, arg0, f"command<{arg0.name}> not found") - subctx = parent.copy(args=args) - parent.term << f"-->> [{cmd.name}]" - cmd.execute_cli(subctx) - parent.term << f"--<< [{cmd.name}]" - - -def execute_kitescript(ctx: CmdContext, script: KiteScript): - cmds = script.file.read().splitlines() - outer = ctx.copy(term=RunTerminal(ctx.term)) - for i, cmd in enumerate(cmds): - raw = cmd.strip() - if len(raw) > 0: - run_cmd(outer, raw) - - -class RunCmd: - name = "run" - - @staticmethod - def execute_cli(ctx: CmdContext): - if ctx.args.isempty: - raise CommandEmptyArgsError(RunCmd, ctx.args, "no script name given") - name_arg = ctx.args[0] - name = name_arg.full - script = ctx.proj.scripts[name] - if script is None: - raise CommandArgError(RunCmd, name_arg, f"script<{name}> not found") - if isinstance(script, KiteScript): - execute_kitescript(ctx, script) - - @staticmethod - def execute_interactive(ctx: CmdContext) -> Iterable: - pass - - @staticmethod - def help(ctx: CmdContext): - t = ctx.term - t << "run - - - - - - - diff --git a/web/manifest.json b/web/manifest.json deleted file mode 100644 index 683d492b1..000000000 --- a/web/manifest.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "mimir", - "short_name": "mimir", - "start_url": ".", - "display": "standalone", - "background_color": "#0175C2", - "theme_color": "#0175C2", - "description": "A new Flutter project.", - "orientation": "portrait-primary", - "prefer_related_applications": false, - "icons": [ - { - "src": "icons/Icon-192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "icons/Icon-512.png", - "sizes": "512x512", - "type": "image/png" - }, - { - "src": "icons/Icon-maskable-192.png", - "sizes": "192x192", - "type": "image/png", - "purpose": "maskable" - }, - { - "src": "icons/Icon-maskable-512.png", - "sizes": "512x512", - "type": "image/png", - "purpose": "maskable" - } - ] -} diff --git a/windows/.gitignore b/windows/.gitignore deleted file mode 100644 index d492d0d98..000000000 --- a/windows/.gitignore +++ /dev/null @@ -1,17 +0,0 @@ -flutter/ephemeral/ - -# Visual Studio user-specific files. -*.suo -*.user -*.userosscache -*.sln.docstates - -# Visual Studio build-related files. -x64/ -x86/ - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt deleted file mode 100644 index 37a68a0d1..000000000 --- a/windows/CMakeLists.txt +++ /dev/null @@ -1,95 +0,0 @@ -cmake_minimum_required(VERSION 3.15) -project(sit_life LANGUAGES CXX) - -set(BINARY_NAME "sit_life") - -cmake_policy(SET CMP0063 NEW) - -set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") - -# Configure build options. -get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) -if(IS_MULTICONFIG) - set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" - CACHE STRING "" FORCE) -else() - if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) - set(CMAKE_BUILD_TYPE "Debug" CACHE - STRING "Flutter build mode" FORCE) - set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS - "Debug" "Profile" "Release") - endif() -endif() - -set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") -set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") -set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") -set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") - -# Use Unicode for all projects. -add_definitions(-DUNICODE -D_UNICODE) - -# Compilation settings that should be applied to most targets. -function(APPLY_STANDARD_SETTINGS TARGET) - target_compile_features(${TARGET} PUBLIC cxx_std_17) - target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") - target_compile_options(${TARGET} PRIVATE /EHsc) - target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") - target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") -endfunction() - -set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") - -# Flutter library and tool build rules. -add_subdirectory(${FLUTTER_MANAGED_DIR}) - -# Application build -add_subdirectory("runner") - -# Generated plugin build rules, which manage building the plugins and adding -# them to the application. -include(flutter/generated_plugins.cmake) - - -# === Installation === -# Support files are copied into place next to the executable, so that it can -# run in place. This is done instead of making a separate bundle (as on Linux) -# so that building and running from within Visual Studio will work. -set(BUILD_BUNDLE_DIR "$") -# Make the "install" step default, as it's required to run. -set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) -if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) - set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) -endif() - -set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") -set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") - -install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" - COMPONENT Runtime) - -install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" - COMPONENT Runtime) - -install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) - -if(PLUGIN_BUNDLED_LIBRARIES) - install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" - DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) -endif() - -# Fully re-copy the assets directory on each build to avoid having stale files -# from a previous install. -set(FLUTTER_ASSET_DIR_NAME "flutter_assets") -install(CODE " - file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") - " COMPONENT Runtime) -install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" - DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) - -# Install the AOT library on non-Debug builds only. -install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" - CONFIGURATIONS Profile;Release - COMPONENT Runtime) diff --git a/windows/flutter/CMakeLists.txt b/windows/flutter/CMakeLists.txt deleted file mode 100644 index 86edc67b9..000000000 --- a/windows/flutter/CMakeLists.txt +++ /dev/null @@ -1,108 +0,0 @@ -cmake_minimum_required(VERSION 3.15) - -set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") - -# Configuration provided via flutter tool. -include(${EPHEMERAL_DIR}/generated_config.cmake) - -# TODO: Move the rest of this into files in ephemeral. See -# https://github.com/flutter/flutter/issues/57146. -set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") - -# Set fallback configurations for older versions of the flutter tool. -if (NOT DEFINED FLUTTER_TARGET_PLATFORM) - set(FLUTTER_TARGET_PLATFORM "windows-x64") -endif() - -# === Flutter Library === -set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") - -# Published to parent scope for install step. -set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) -set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) -set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) -set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) - -list(APPEND FLUTTER_LIBRARY_HEADERS - "flutter_export.h" - "flutter_windows.h" - "flutter_messenger.h" - "flutter_plugin_registrar.h" - "flutter_texture_registrar.h" -) -list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") -add_library(flutter INTERFACE) -target_include_directories(flutter INTERFACE - "${EPHEMERAL_DIR}" -) -target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") -add_dependencies(flutter flutter_assemble) - -# === Wrapper === -list(APPEND CPP_WRAPPER_SOURCES_CORE - "core_implementations.cc" - "standard_codec.cc" -) -list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") -list(APPEND CPP_WRAPPER_SOURCES_PLUGIN - "plugin_registrar.cc" -) -list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") -list(APPEND CPP_WRAPPER_SOURCES_APP - "flutter_engine.cc" - "flutter_view_controller.cc" -) -list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") - -# Wrapper sources needed for a plugin. -add_library(flutter_wrapper_plugin STATIC - ${CPP_WRAPPER_SOURCES_CORE} - ${CPP_WRAPPER_SOURCES_PLUGIN} -) -apply_standard_settings(flutter_wrapper_plugin) -set_target_properties(flutter_wrapper_plugin PROPERTIES - POSITION_INDEPENDENT_CODE ON) -set_target_properties(flutter_wrapper_plugin PROPERTIES - CXX_VISIBILITY_PRESET hidden) -target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) -target_include_directories(flutter_wrapper_plugin PUBLIC - "${WRAPPER_ROOT}/include" -) -add_dependencies(flutter_wrapper_plugin flutter_assemble) - -# Wrapper sources needed for the runner. -add_library(flutter_wrapper_app STATIC - ${CPP_WRAPPER_SOURCES_CORE} - ${CPP_WRAPPER_SOURCES_APP} -) -apply_standard_settings(flutter_wrapper_app) -target_link_libraries(flutter_wrapper_app PUBLIC flutter) -target_include_directories(flutter_wrapper_app PUBLIC - "${WRAPPER_ROOT}/include" -) -add_dependencies(flutter_wrapper_app flutter_assemble) - -# === Flutter tool backend === -# _phony_ is a non-existent file to force this command to run every time, -# since currently there's no way to get a full input/output list from the -# flutter tool. -set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") -set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) -add_custom_command( - OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} - ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} - ${CPP_WRAPPER_SOURCES_APP} - ${PHONY_OUTPUT} - COMMAND ${CMAKE_COMMAND} -E env - ${FLUTTER_TOOL_ENVIRONMENT} - "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - ${FLUTTER_TARGET_PLATFORM} $ - VERBATIM -) -add_custom_target(flutter_assemble DEPENDS - "${FLUTTER_LIBRARY}" - ${FLUTTER_LIBRARY_HEADERS} - ${CPP_WRAPPER_SOURCES_CORE} - ${CPP_WRAPPER_SOURCES_PLUGIN} - ${CPP_WRAPPER_SOURCES_APP} -) diff --git a/windows/runner/CMakeLists.txt b/windows/runner/CMakeLists.txt deleted file mode 100644 index 394917c05..000000000 --- a/windows/runner/CMakeLists.txt +++ /dev/null @@ -1,40 +0,0 @@ -cmake_minimum_required(VERSION 3.14) -project(runner LANGUAGES CXX) - -# Define the application target. To change its name, change BINARY_NAME in the -# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer -# work. -# -# Any new source files that you add to the application should be added here. -add_executable(${BINARY_NAME} WIN32 - "flutter_window.cpp" - "main.cpp" - "utils.cpp" - "win32_window.cpp" - "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" - "Runner.rc" - "runner.exe.manifest" -) - -# Apply the standard set of build settings. This can be removed for applications -# that need different build settings. -apply_standard_settings(${BINARY_NAME}) - -# Add preprocessor definitions for the build version. -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") - -# Disable Windows macros that collide with C++ standard library functions. -target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") - -# Add dependency libraries and include directories. Add any application-specific -# dependencies here. -target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) -target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") -target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") - -# Run the Flutter tool portions of the build. This must not be removed. -add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc deleted file mode 100644 index a756f8dd4..000000000 --- a/windows/runner/Runner.rc +++ /dev/null @@ -1,121 +0,0 @@ -// Microsoft Visual C++ generated resource script. -// -#pragma code_page(65001) -#include "resource.h" - -#define APSTUDIO_READONLY_SYMBOLS -///////////////////////////////////////////////////////////////////////////// -// -// Generated from the TEXTINCLUDE 2 resource. -// -#include "winres.h" - -///////////////////////////////////////////////////////////////////////////// -#undef APSTUDIO_READONLY_SYMBOLS - -///////////////////////////////////////////////////////////////////////////// -// English (United States) resources - -#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) -LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US - -#ifdef APSTUDIO_INVOKED -///////////////////////////////////////////////////////////////////////////// -// -// TEXTINCLUDE -// - -1 TEXTINCLUDE -BEGIN - "resource.h\0" -END - -2 TEXTINCLUDE -BEGIN - "#include ""winres.h""\r\n" - "\0" -END - -3 TEXTINCLUDE -BEGIN - "\r\n" - "\0" -END - -#endif // APSTUDIO_INVOKED - - -///////////////////////////////////////////////////////////////////////////// -// -// Icon -// - -// Icon with lowest ID value placed first to ensure application icon -// remains consistent on all systems. -IDI_APP_ICON ICON "resources\\app_icon.ico" - - -///////////////////////////////////////////////////////////////////////////// -// -// Version -// - -#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) -#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD -#else -#define VERSION_AS_NUMBER 1,0,0,0 -#endif - -#if defined(FLUTTER_VERSION) -#define VERSION_AS_STRING FLUTTER_VERSION -#else -#define VERSION_AS_STRING "1.0.0" -#endif - -VS_VERSION_INFO VERSIONINFO - FILEVERSION VERSION_AS_NUMBER - PRODUCTVERSION VERSION_AS_NUMBER - FILEFLAGSMASK VS_FFI_FILEFLAGSMASK -#ifdef _DEBUG - FILEFLAGS VS_FF_DEBUG -#else - FILEFLAGS 0x0L -#endif - FILEOS VOS__WINDOWS32 - FILETYPE VFT_APP - FILESUBTYPE 0x0L -BEGIN - BLOCK "StringFileInfo" - BEGIN - BLOCK "040904e4" - BEGIN - VALUE "CompanyName", "life.mysit" "\0" - VALUE "FileDescription", "SIT Life" "\0" - VALUE "FileVersion", VERSION_AS_STRING "\0" - VALUE "InternalName", "life.mysit.SITLife" "\0" - VALUE "LegalCopyright", "Copyright (C) 2023 SIT Life(mysit.life). All rights reserved." "\0" - VALUE "OriginalFilename", "SIT_Life.exe" "\0" - VALUE "ProductName", "SIT Life" "\0" - VALUE "ProductVersion", VERSION_AS_STRING "\0" - END - END - BLOCK "VarFileInfo" - BEGIN - VALUE "Translation", 0x409, 1252 - END -END - -#endif // English (United States) resources -///////////////////////////////////////////////////////////////////////////// - - - -#ifndef APSTUDIO_INVOKED -///////////////////////////////////////////////////////////////////////////// -// -// Generated from the TEXTINCLUDE 3 resource. -// - - -///////////////////////////////////////////////////////////////////////////// -#endif // not APSTUDIO_INVOKED diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp deleted file mode 100644 index b43b9095e..000000000 --- a/windows/runner/flutter_window.cpp +++ /dev/null @@ -1,61 +0,0 @@ -#include "flutter_window.h" - -#include - -#include "flutter/generated_plugin_registrant.h" - -FlutterWindow::FlutterWindow(const flutter::DartProject& project) - : project_(project) {} - -FlutterWindow::~FlutterWindow() {} - -bool FlutterWindow::OnCreate() { - if (!Win32Window::OnCreate()) { - return false; - } - - RECT frame = GetClientArea(); - - // The size here must match the window dimensions to avoid unnecessary surface - // creation / destruction in the startup path. - flutter_controller_ = std::make_unique( - frame.right - frame.left, frame.bottom - frame.top, project_); - // Ensure that basic setup of the controller was successful. - if (!flutter_controller_->engine() || !flutter_controller_->view()) { - return false; - } - RegisterPlugins(flutter_controller_->engine()); - SetChildContent(flutter_controller_->view()->GetNativeWindow()); - return true; -} - -void FlutterWindow::OnDestroy() { - if (flutter_controller_) { - flutter_controller_ = nullptr; - } - - Win32Window::OnDestroy(); -} - -LRESULT -FlutterWindow::MessageHandler(HWND hwnd, UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept { - // Give Flutter, including plugins, an opportunity to handle window messages. - if (flutter_controller_) { - std::optional result = - flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, - lparam); - if (result) { - return *result; - } - } - - switch (message) { - case WM_FONTCHANGE: - flutter_controller_->engine()->ReloadSystemFonts(); - break; - } - - return Win32Window::MessageHandler(hwnd, message, wparam, lparam); -} diff --git a/windows/runner/flutter_window.h b/windows/runner/flutter_window.h deleted file mode 100644 index 6da0652f0..000000000 --- a/windows/runner/flutter_window.h +++ /dev/null @@ -1,33 +0,0 @@ -#ifndef RUNNER_FLUTTER_WINDOW_H_ -#define RUNNER_FLUTTER_WINDOW_H_ - -#include -#include - -#include - -#include "win32_window.h" - -// A window that does nothing but host a Flutter view. -class FlutterWindow : public Win32Window { - public: - // Creates a new FlutterWindow hosting a Flutter view running |project|. - explicit FlutterWindow(const flutter::DartProject& project); - virtual ~FlutterWindow(); - - protected: - // Win32Window: - bool OnCreate() override; - void OnDestroy() override; - LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, - LPARAM const lparam) noexcept override; - - private: - // The project to run. - flutter::DartProject project_; - - // The Flutter instance hosted by this window. - std::unique_ptr flutter_controller_; -}; - -#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp deleted file mode 100644 index d8ddb173b..000000000 --- a/windows/runner/main.cpp +++ /dev/null @@ -1,48 +0,0 @@ -#include -#include -#include - -#include "flutter_window.h" -#include "utils.h" - -int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, - _In_ wchar_t *command_line, _In_ int show_command) { - // Attach to console when present (e.g., 'flutter run') or create a - // new console when running with a debugger. - if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { - CreateAndAttachConsole(); - } - - // Initialize COM, so that it is available for use in the library and/or - // plugins. - ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); - - flutter::DartProject project(L"data"); - - std::vector command_line_arguments = - GetCommandLineArguments(); - - project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); - - FlutterWindow window(project); - const int defaultWidth = 500, defaultHeight = 800; - Win32Window::Size size(defaultWidth, defaultHeight); - int screenWidth = GetSystemMetrics(SM_CXSCREEN); - int screenHeight = GetSystemMetrics(SM_CYSCREEN); - // Perform a default center and size before loaded. - Win32Window::Point origin((screenWidth - defaultWidth) / 2 , (screenHeight - defaultHeight) / 2); - // Use the escape Unicode to perform a wide string. - if (!window.CreateAndShow(L"SIT Life", origin, size)) { - return EXIT_FAILURE; - } - window.SetQuitOnClose(true); - - ::MSG msg; - while (::GetMessage(&msg, nullptr, 0, 0)) { - ::TranslateMessage(&msg); - ::DispatchMessage(&msg); - } - - ::CoUninitialize(); - return EXIT_SUCCESS; -} diff --git a/windows/runner/resource.h b/windows/runner/resource.h deleted file mode 100644 index 66a65d1e4..000000000 --- a/windows/runner/resource.h +++ /dev/null @@ -1,16 +0,0 @@ -//{{NO_DEPENDENCIES}} -// Microsoft Visual C++ generated include file. -// Used by Runner.rc -// -#define IDI_APP_ICON 101 - -// Next default values for new objects -// -#ifdef APSTUDIO_INVOKED -#ifndef APSTUDIO_READONLY_SYMBOLS -#define _APS_NEXT_RESOURCE_VALUE 102 -#define _APS_NEXT_COMMAND_VALUE 40001 -#define _APS_NEXT_CONTROL_VALUE 1001 -#define _APS_NEXT_SYMED_VALUE 101 -#endif -#endif diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico deleted file mode 100644 index 20aed3c236ddded9f73ef46aa88a1e7b60aeec9d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10246 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 z 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 diff --git a/windows/runner/runner.exe.manifest b/windows/runner/runner.exe.manifest deleted file mode 100644 index c977c4a42..000000000 --- a/windows/runner/runner.exe.manifest +++ /dev/null @@ -1,20 +0,0 @@ - - - - - PerMonitorV2 - - - - - - - - - - - - - - - diff --git a/windows/runner/utils.cpp b/windows/runner/utils.cpp deleted file mode 100644 index d19bdbbcc..000000000 --- a/windows/runner/utils.cpp +++ /dev/null @@ -1,64 +0,0 @@ -#include "utils.h" - -#include -#include -#include -#include - -#include - -void CreateAndAttachConsole() { - if (::AllocConsole()) { - FILE *unused; - if (freopen_s(&unused, "CONOUT$", "w", stdout)) { - _dup2(_fileno(stdout), 1); - } - if (freopen_s(&unused, "CONOUT$", "w", stderr)) { - _dup2(_fileno(stdout), 2); - } - std::ios::sync_with_stdio(); - FlutterDesktopResyncOutputStreams(); - } -} - -std::vector GetCommandLineArguments() { - // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. - int argc; - wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); - if (argv == nullptr) { - return std::vector(); - } - - std::vector command_line_arguments; - - // Skip the first argument as it's the binary name. - for (int i = 1; i < argc; i++) { - command_line_arguments.push_back(Utf8FromUtf16(argv[i])); - } - - ::LocalFree(argv); - - return command_line_arguments; -} - -std::string Utf8FromUtf16(const wchar_t* utf16_string) { - if (utf16_string == nullptr) { - return std::string(); - } - int target_length = ::WideCharToMultiByte( - CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, - -1, nullptr, 0, nullptr, nullptr); - if (target_length == 0) { - return std::string(); - } - std::string utf8_string; - utf8_string.resize(target_length); - int converted_length = ::WideCharToMultiByte( - CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, - -1, utf8_string.data(), - target_length, nullptr, nullptr); - if (converted_length == 0) { - return std::string(); - } - return utf8_string; -} diff --git a/windows/runner/utils.h b/windows/runner/utils.h deleted file mode 100644 index 3879d5475..000000000 --- a/windows/runner/utils.h +++ /dev/null @@ -1,19 +0,0 @@ -#ifndef RUNNER_UTILS_H_ -#define RUNNER_UTILS_H_ - -#include -#include - -// Creates a console for the process, and redirects stdout and stderr to -// it for both the runner and the Flutter library. -void CreateAndAttachConsole(); - -// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string -// encoded in UTF-8. Returns an empty std::string on failure. -std::string Utf8FromUtf16(const wchar_t* utf16_string); - -// Gets the command line arguments passed in as a std::vector, -// encoded in UTF-8. Returns an empty std::vector on failure. -std::vector GetCommandLineArguments(); - -#endif // RUNNER_UTILS_H_ diff --git a/windows/runner/win32_window.cpp b/windows/runner/win32_window.cpp deleted file mode 100644 index b961b3e53..000000000 --- a/windows/runner/win32_window.cpp +++ /dev/null @@ -1,286 +0,0 @@ -#include "win32_window.h" - -#include - -#include "resource.h" - -#include "app_links/app_links_plugin_c_api.h" - -namespace { - -constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; - -// The number of Win32Window objects that currently exist. -static int g_active_window_count = 0; - -using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); - -// Scale helper to convert logical scaler values to physical using passed in -// scale factor -int Scale(int source, double scale_factor) { - return static_cast(source * scale_factor); -} - -// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. -// This API is only needed for PerMonitor V1 awareness mode. -void EnableFullDpiSupportIfAvailable(HWND hwnd) { - HMODULE user32_module = LoadLibraryA("User32.dll"); - if (!user32_module) { - return; - } - auto enable_non_client_dpi_scaling = - reinterpret_cast( - GetProcAddress(user32_module, "EnableNonClientDpiScaling")); - if (enable_non_client_dpi_scaling != nullptr) { - enable_non_client_dpi_scaling(hwnd); - FreeLibrary(user32_module); - } -} - -} // namespace - -// Manages the Win32Window's window class registration. -class WindowClassRegistrar { - public: - ~WindowClassRegistrar() = default; - - // Returns the singleton registar instance. - static WindowClassRegistrar* GetInstance() { - if (!instance_) { - instance_ = new WindowClassRegistrar(); - } - return instance_; - } - - // Returns the name of the window class, registering the class if it hasn't - // previously been registered. - const wchar_t* GetWindowClass(); - - // Unregisters the window class. Should only be called if there are no - // instances of the window. - void UnregisterWindowClass(); - - private: - WindowClassRegistrar() = default; - - static WindowClassRegistrar* instance_; - - bool class_registered_ = false; -}; - -WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; - -const wchar_t* WindowClassRegistrar::GetWindowClass() { - if (!class_registered_) { - WNDCLASS window_class{}; - window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); - window_class.lpszClassName = kWindowClassName; - window_class.style = CS_HREDRAW | CS_VREDRAW; - window_class.cbClsExtra = 0; - window_class.cbWndExtra = 0; - window_class.hInstance = GetModuleHandle(nullptr); - window_class.hIcon = - LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); - window_class.hbrBackground = 0; - window_class.lpszMenuName = nullptr; - window_class.lpfnWndProc = Win32Window::WndProc; - RegisterClass(&window_class); - class_registered_ = true; - } - return kWindowClassName; -} - -void WindowClassRegistrar::UnregisterWindowClass() { - UnregisterClass(kWindowClassName, nullptr); - class_registered_ = false; -} - -Win32Window::Win32Window() { - ++g_active_window_count; -} - -Win32Window::~Win32Window() { - --g_active_window_count; - Destroy(); -} - -bool Win32Window::CreateAndShow(const std::wstring& title, - const Point& origin, - const Size& size) { - if (SendAppLinkToInstance(title)) { - return false; - } - - Destroy(); - - const wchar_t* window_class = - WindowClassRegistrar::GetInstance()->GetWindowClass(); - - const POINT target_point = {static_cast(origin.x), - static_cast(origin.y)}; - HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); - UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); - double scale_factor = dpi / 96.0; - - HWND window = CreateWindow( - window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, - Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), - Scale(size.width, scale_factor), Scale(size.height, scale_factor), - nullptr, nullptr, GetModuleHandle(nullptr), this); - - if (!window) { - return false; - } - - return OnCreate(); -} - -bool Win32Window::SendAppLinkToInstance(const std::wstring& title) { - // Find our exact window - HWND hwnd = ::FindWindow(kWindowClassName, title.c_str()); - - if (hwnd) { - // Dispatch new link to current window - SendAppLink(hwnd); - - // (Optional) Restore our window to front in same state - WINDOWPLACEMENT place = { sizeof(WINDOWPLACEMENT) }; - GetWindowPlacement(hwnd, &place); - - switch(place.showCmd) { - case SW_SHOWMAXIMIZED: - ShowWindow(hwnd, SW_SHOWMAXIMIZED); - break; - case SW_SHOWMINIMIZED: - ShowWindow(hwnd, SW_RESTORE); - break; - default: - ShowWindow(hwnd, SW_NORMAL); - break; - } - - SetWindowPos(0, HWND_TOP, 0, 0, 0, 0, SWP_SHOWWINDOW | SWP_NOSIZE | SWP_NOMOVE); - SetForegroundWindow(hwnd); - // END Restore - - // Window has been found, don't create another one. - return true; - } - - return false; -} - -// static -LRESULT CALLBACK Win32Window::WndProc(HWND const window, - UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept { - if (message == WM_NCCREATE) { - auto window_struct = reinterpret_cast(lparam); - SetWindowLongPtr(window, GWLP_USERDATA, - reinterpret_cast(window_struct->lpCreateParams)); - - auto that = static_cast(window_struct->lpCreateParams); - EnableFullDpiSupportIfAvailable(window); - that->window_handle_ = window; - } else if (Win32Window* that = GetThisFromHandle(window)) { - return that->MessageHandler(window, message, wparam, lparam); - } - - return DefWindowProc(window, message, wparam, lparam); -} - -LRESULT -Win32Window::MessageHandler(HWND hwnd, - UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept { - switch (message) { - case WM_DESTROY: - window_handle_ = nullptr; - Destroy(); - if (quit_on_close_) { - PostQuitMessage(0); - } - return 0; - - case WM_DPICHANGED: { - auto newRectSize = reinterpret_cast(lparam); - LONG newWidth = newRectSize->right - newRectSize->left; - LONG newHeight = newRectSize->bottom - newRectSize->top; - - SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, - newHeight, SWP_NOZORDER | SWP_NOACTIVATE); - - return 0; - } - case WM_SIZE: { - RECT rect = GetClientArea(); - if (child_content_ != nullptr) { - // Size and position the child window. - MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, - rect.bottom - rect.top, TRUE); - } - return 0; - } - - case WM_ACTIVATE: - if (child_content_ != nullptr) { - SetFocus(child_content_); - } - return 0; - } - - return DefWindowProc(window_handle_, message, wparam, lparam); -} - -void Win32Window::Destroy() { - OnDestroy(); - - if (window_handle_) { - DestroyWindow(window_handle_); - window_handle_ = nullptr; - } - if (g_active_window_count == 0) { - WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); - } -} - -Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { - return reinterpret_cast( - GetWindowLongPtr(window, GWLP_USERDATA)); -} - -void Win32Window::SetChildContent(HWND content) { - child_content_ = content; - SetParent(content, window_handle_); - RECT frame = GetClientArea(); - - MoveWindow(content, frame.left, frame.top, frame.right - frame.left, - frame.bottom - frame.top, true); - - SetFocus(child_content_); -} - -RECT Win32Window::GetClientArea() { - RECT frame; - GetClientRect(window_handle_, &frame); - return frame; -} - -HWND Win32Window::GetHandle() { - return window_handle_; -} - -void Win32Window::SetQuitOnClose(bool quit_on_close) { - quit_on_close_ = quit_on_close; -} - -bool Win32Window::OnCreate() { - // No-op; provided for subclasses. - return true; -} - -void Win32Window::OnDestroy() { - // No-op; provided for subclasses. -} diff --git a/windows/runner/win32_window.h b/windows/runner/win32_window.h deleted file mode 100644 index 9cfea10a2..000000000 --- a/windows/runner/win32_window.h +++ /dev/null @@ -1,103 +0,0 @@ -#ifndef RUNNER_WIN32_WINDOW_H_ -#define RUNNER_WIN32_WINDOW_H_ - -#include - -#include -#include -#include - -// A class abstraction for a high DPI-aware Win32 Window. Intended to be -// inherited from by classes that wish to specialize with custom -// rendering and input handling -class Win32Window { - public: - struct Point { - unsigned int x; - unsigned int y; - Point(unsigned int x, unsigned int y) : x(x), y(y) {} - }; - - struct Size { - unsigned int width; - unsigned int height; - Size(unsigned int width, unsigned int height) - : width(width), height(height) {} - }; - - Win32Window(); - virtual ~Win32Window(); - - // Creates and shows a win32 window with |title| and position and size using - // |origin| and |size|. New windows are created on the default monitor. Window - // sizes are specified to the OS in physical pixels, hence to ensure a - // consistent size to will treat the width height passed in to this function - // as logical pixels and scale to appropriate for the default monitor. Returns - // true if the window was created successfully. - bool CreateAndShow(const std::wstring& title, - const Point& origin, - const Size& size); - - // Release OS resources associated with window. - void Destroy(); - - // Inserts |content| into the window tree. - void SetChildContent(HWND content); - - // Returns the backing Window handle to enable clients to set icon and other - // window properties. Returns nullptr if the window has been destroyed. - HWND GetHandle(); - - // If true, closing this window will quit the application. - void SetQuitOnClose(bool quit_on_close); - - // Return a RECT representing the bounds of the current client area. - RECT GetClientArea(); - - protected: - // Processes and route salient window messages for mouse handling, - // size change and DPI. Delegates handling of these to member overloads that - // inheriting classes can handle. - virtual LRESULT MessageHandler(HWND window, - UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept; - - // Called when CreateAndShow is called, allowing subclass window-related - // setup. Subclasses should return false if setup fails. - virtual bool OnCreate(); - - // Called when Destroy is called. - virtual void OnDestroy(); - - private: - friend class WindowClassRegistrar; - - // OS callback called by message pump. Handles the WM_NCCREATE message which - // is passed when the non-client area is being created and enables automatic - // non-client DPI scaling so that the non-client area automatically - // responsponds to changes in DPI. All other messages are handled by - // MessageHandler. - static LRESULT CALLBACK WndProc(HWND const window, - UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept; - - // Retrieves a class instance pointer for |window| - static Win32Window* GetThisFromHandle(HWND const window) noexcept; - - bool quit_on_close_ = false; - - // window handle for top level window. - HWND window_handle_ = nullptr; - - // window handle for hosted content. - HWND child_content_ = nullptr; - - // Dispatches link if any. - // This method enables our app to be with a single instance too. - // This is mandatory if you want to catch further links in same app. - bool SendAppLinkToInstance(const std::wstring& title); -}; - -#endif // RUNNER_WIN32_WINDOW_H_

*mq1}* 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 L#z$F#!rT5InNuv` diff --git a/assets/icon.svg b/assets/icon.svg deleted file mode 100644 index 039e278c1..000000000 --- a/assets/icon.svg +++ /dev/null @@ -1 +0,0 @@ -LOGO \ No newline at end of file diff --git a/assets/l10n/en.yaml b/assets/l10n/en.yaml deleted file mode 100644 index 3adde9269..000000000 --- a/assets/l10n/en.yaml +++ /dev/null @@ -1,697 +0,0 @@ -timetable: - navigation: Timetable - jump: Jump - findToday: Find today - startWith: "Start with" - weekOrderedName: "Week {}" - focusTimetable: Focus timetable - signature: Signature - signaturePlaceholder: Your name - mine: - title: My Timetables - exportCalendar: Export calendar - deleteRequest: Confirm to delete? - deleteRequestDesc: You will lose this timetable permanently - emptyTip: Let's import a timetable! - details: Details - edit: - name: Name - import: - title: Import timetable - import: Import - fromFile: From File - fromFileBtn: From file - tryImportBtn: Try import - connectivityCheckerDesc: You need to check the access to school server before importing a timetable. - selectSemesterTip: Please select a semester first - endTip: Please enter the info - failed: Import Failed - failedDesc: Can't import the timetable, please try again - failedTip: Failed to import, please try again - importing: Trying to fetch the timetable... - timetableInfo: Timetable Info - defaultName: "{semester}, {yearStart} Timetable" - export: - title: Export to calendar - export: Export - lessonMode: - title: Lesson mode - merged: Full - mergedTip: Several parts of the same class will be combined into one event - separate: Separate - separateTip: Several parts of the same class will be separated into multiple events - enableAlarm: - title: Enable alarm - desc: Add alarm before class - alarmMode: - title: Alarm mode - sound: Sound - display: Display - alarmDuration: Alarm duration - alarmBeforeClassBegins: - title: Alarm before class begins - desc: "{duration} before" - screenshot: - screenshot: Screenshot - take: Take - title: Screenshot - enableBackground: - title: Show wallpaper - desc: Show timetable wallpaper in the screenshot - displayMode: - daily: Daily - weekly: Weekly - weekIndexType: - single: "Week No.{}" - all: "Weeks from No.{start} to No.{end}" - odd: "Odd weeks from No.{start} to No.{end}" - even: "Even weeks from No.{start} to No.{end}" - freeTip: - dayTip: It's a free day! - isTodayTip: "Have a nice day! 🎉" - weekTip: Wow, no class this week! - isThisWeekTip: "Enjoy this week! 🎉" - termTip: "Cool! You even have a free term! 🎉🎉🎉" - findNearestWeekWithClass: Find a week with class - findNearestDayWithClass: Find a day with class - p13n: - title: Palettes - builtinPalette: - classic: - name: Classic - americano: - name: Americano - candy: - name: Candy - sprint: - name: Sprint - summary: - name: Summary - fall: - name: Fall - winter: - name: Winter - thicket: - name: Thicket - creeksideBreeze: - name: Creekside Breeze - palette: - title: Palette - name: Name - tab: - custom: Custom - builtin: Builtin - info: Info - colors: Colors - namePlaceholder: Palette name - author: Author - authorPlaceholder: Your name, email, or url - color: color - details: Details - fab: Palette - shareQrCode: Share QR code - newPaletteName: New palette - deleteRequest: Conform to delete? - deleteRequestDesc: The palette will be deleted permanently without unless a QR code backup is saved - addFromQrCode: Palette was added from QR code - addColor: Add a color - cellStyle: - title: Cell Style - showTeachers: - title: Teachers - desc: Show teachers in cell - grayOut: - title: Gray out taken lessons - desc: All lessons before now - harmonize: - title: Harmonize with theme color - desc: Mix the cell color with theme color - alpha: Cell alpha - background: - title: Wallpaper - opacity: Wallpaper opacity - pickTip: Pick your favorite image - repeat: - title: Repeat image - desc: Repeat image as a pattern to fill wallpaper - antialias: - title: Anti-alias - desc: Turn off this if image is low in pixels - livePreview: - 0: - name: Taken - place: Stadium - teachers: John - 1: - name: Taken - place: Classroom - teachers: Martin - 2: - name: Today - place: Church - teachers: James - 3: - name: Tomorrow - place: Lab - teachers: Thomas -school: - navigation: School - schoolYear: School year - semester: - title: Semester - all: Full Year - term1: Fall - term2: Spring - course: - classCode: Class Code - courseCode: Course Code - teacher: Teacher - credit: Credit -life: - navigation: Life -me: - navigation: Me -class2nd: - title: Second class - attended: - title: Attended - tab: - info: Info - description: Description - activity: Activity - refreshSuccessTip: Second class score refreshed successfully - refreshFailedTip: Failed to refresh second class score - noActivities: No activities - noAttendedActivities: No attended activities - viewDetails: View details - apply: - btn: Apply - replyTip: Reply from school - applyRequest: Apply - applyRequestDesc: Confirm to apply this activity? - info: - activityId: Activity ID - applicationId: Application ID - activityOf: "Activity #{}" - applicationOf: "Application #{}" - name: Name - tags: Tags - category: Activity category - scoreType: Score type - honestyPoints: Honesty points - totalPoints: Total points - applicationTime: Application time - status: Status - contactInfo: Contact Info - organizer: Organizer - principal: Principal - signInTime: Sign-in - signOutTime: Sign-out - startTime: Start time - undertaker: Undertaker - duration: Duration - location: Location - activityCat: - all: All - lecture: Lecture - thematicEdu: Thematic Edu - creation: Creation - schoolCultureActivity: Culture ACT - schoolCivilization: Civilization - practice: Practice - voluntary: Voluntary - onlineSafetyEdu: Safety Edu - conference: Conf - schoolCultureCompetition: Culture COMP - paperAndPatent: Paper&Patent - scoreType: - all: All - thematicReport: - full: Thematic Report - short: Report - creation: - full: Creation - short: Creat. - schoolCulture: - full: School Culture - short: Cult. - practice: - full: Practice - short: Pract. - voluntary: - full: Voluntary - short: Vol. - schoolSafetyCivilization: - full: Safety&Civil. - short: Civil. - noDetails: No details given - details: Activity eetails -ywb: - title: SIT YWB - info: | - This function is based on "SIT YWB". - For most of types, you have to do the necessary procedure on the spot although you submitted an application online. - apply: Apply - noServicesTip: No services - mine: - title: Mine - noApplicationsTip: No applications - details: - apply: Apply - type: - todo: Todo - running: Running - complete: Complete -captcha: - title: Captcha - enterHint: Please enter captcha - emptyInputError: The captcha is empty -credentials: - studentId: Student ID - account: Account - password: Password - oaAccount: OA account - oaPwd: OA password - savedOaPwd: Saved OA password - savedOaPwdDesc: Used to auto-login OA - pwd: Password - error: Credentials error - login: Login - forgotPwd: Forgot password? -easyconnect: - launchBtn: Launch EasyConnect - launchFailed: Launch Failed - launchFailedDesc: Failed to launch EasyConnect, have you installed it or just download it now? -electricity: - title: Electricity balance - balance: Balance - remainingPower: Remaining - searchInvalidTip: Please enter or select an existing room number - refreshSuccessTip: Electricity balance refreshed successfully - refreshFailedTip: Failed to refresh electricity balance - searchRoom: Search Room -eduEmail: - title: Edu email - action: - login: Login - inbox: Inbox - outbox: Outbox - login: - title: Login Edu email - addressHint: Student ID/Work number - passwordHint: Your email password - invalidEmailAddressFormatTip: Invalid email address format - failedWarn: - title: Login failed - desc: Please check the password - info: - emailAddress: Email address - inbox: - title: Inbox - outbox: - title: Outbox - noContent: No content - noSubject: No subject - pluralSenderTailing: ... - text: Text -examResult: - title: Exam result - check: Check - teacherEval: Teacher Eval - noResultsTip: No results - lessonNotEvaluated: Not-eval - teacherEvalTitle: Teacher evaluation - compulsory: Compulsory - elective: Elective - gpa: - lessonSelected: "{} Selected" - gpaResult: "GPA: {}" -examArrange: - title: Exams - check: Check - date: Date - time: Time - retake: Retake - location: Location - noExamsTip: No exams - seatNumber: Seat No. - addCalendarEvent: Add event -expenseRecords: - title: Expense records - check: Check - statistics: Statistics - balanceInCard: "¥{} in Card" - lastTransaction: "¥{amount} {place}" - income: "Income: ¥{}" - outcome: "Outcome: ¥{}" - noTransactionsTip: No transactions - refreshSuccessTip: Expense records refreshed successfully - refreshFailedTip: Failed to refresh expense records - statsMode: - week: Week - month: Month - year: Year - view: - balance: Balance - rmb: RMB - stats: - title: Statistics - categories: Categories - total: Total - type: - consume: Consume - coffee: Café - food: Canteen - store: Grocery Shopping - water: Hot Water - library: Library - shower: Shower - topUp: Top Up - subsidy: Subsidy - other: Stuff -homepage: - campusNetworkConnected: Campus network available - campusNetworkUnconnected: Campus network unavailable -networkChecker: - testConnection: - title: Test connection - desc: Test connecting to school server - button: - connected: Continue - connecting: Checking... - disconnected: Retry - none: Check my Connection - status: - connected: Right! You have connected to school server! - connecting: It will take a few seconds, please ensure you have turned on the network on your device. - disconnected: Oops! You can't access the school server. Please check your network settings or use Easy Connect, then try again. - none: You need the access to school server to continue. Let's check it now. -network: - error: Network error - ipAddress: IP Address - connectionTimeoutError: Network timeout - connectionTimeoutErrorDesc: "This function requires access to the campus network. NOTE: It also occurs when the school server is down for maintenance." - openToolBtn: Open network tool -networkTool: - title: Network tool - subtitle: Test connection to the school server - openWlanSettingsBtn: Open WLAN settings - noAccessTip: Unable to access network. Please check network settings on your device, then try again. - connectionFailedError: | - Unable to connect to campus network. Troubleshooting: - 1. Attempt to connect to the school's Wi-Fi, "i-SIT","i-SIT-1x" or "eduroam". - 2. Attempt to launch "EasyConnect" VPN to connect to school. - 3. Attempt to use a self-built proxy server to connect to campus network via HTTP proxy. - If they all didn't work, it's probably due to school server downtime for maintenance or even crash, please try again later. - connectionFailedButCampusNetworkConnected: | - The campus network has been connected, but school server is still inaccessible. - It's probably due to school server downtime for maintenance or even crash, please try again later. - connectedByProxy: Connected to campus network by HTTP proxy - connectedByVpn: Connected to campus network by VPN - connectedByWlan: Connected to campus network by WLAN - connectedByEthernet: Connected to campus network by Ethernet -login: - welcomeHeader: Welcome! - loginOa: Login OA - validateInputAccountPwdRequest: Please check the account and password you entered - credentialsValidatedTip: Account and password are correct - accountOrPwdErrorTip: The account or password is incorrect - captchaErrorTip: The captcha is incorrect - accountFrozenTip: The account is frozen - schoolServerUnconnectedTip: Can't connect to school server - unknownAuthErrorTip: Unknown authentication error - accountHint: Student ID/Work number - loggedInTip: Logged in - notLoggedIn: Not logged In - invalidAccountFormat: Invalid account format - offlineModeBtn: Offline mode - oaPwdHint: Enter your OA password - failedWarn: Login Failed - formatError: Format error - loginRequired: Login required - neverLoggedInTip: You have to login to perform this -library: - title: Library - hotPost: HOT - readerId: Reader Id - noBooks: No books - collectionStatus: Collection status - action: - searchBooks: Search books - login: Login - borrowing: Borrowing - login: - title: Login library - readerIdHint: your Student ID - passwordHint: 888888 by default - failedWarn: - title: Login failed - desc: Please check the password - search: - searchHistory: Search history - trending: Trending - mostPopular: Most popular - borrowing: - title: Borrowing - history: History - renew: Renew - history: - title: History - operation: - borrowing: Borrowing - returning: Returning - searchMethod: - any: Any - title: Title - author: Author - isbn: ISBN - publisher: Publisher - callNumber: Call No. - primaryTitle: Primary title - subject: Subject - $class: Class - bookId: Book ID - orderNumber: Order No. - info: - title: Title - author: Author - isbn: ISBN - publisher: Publisher - publishDate: Publish date - callNumber: Call Number - bookId: Book ID - barcode: Barcode - availableCollection: "Available({available}) / Collection({collection})" -settings: - title: Settings - version: Version - themeColor: Theme color - fromSystem: From system - credentials: - testLoginOa: - title: Test login OA - desc: Try to login OA with current credentials - dev: - title: Developer options - devMode: - title: Developer mode - localStorage: - title: Local storage - desc: Manage storage on your device - selectBoxTip: Please select a box first - clearBoxDesc: Confirm to clear this box? - deleteItemDesc: Confirm to delete this key-value pair? - emptyValueDesc: Confirm to set this key-value pair to null? - reload: - title: Reload - desc: Reload all modules - timetable: - title: Timetable - autoUseImported: - title: Auto-use imported - desc: Use the timetable just imported - palette: - title: Palettes - desc: Customize timetable colors - cellStyle: - title: Edit cell style - desc: How course cell looks like - background: - title: Wallpaper - desc: The background of timetable - school: - title: School - class2nd: - autoRefresh: - title: Auto-refresh 2nd class scores - desc: Refresh scores when app is launched - life: - title: Life - electricity: - autoRefresh: - title: Auto-refresh electricity - desc: Refresh electricity when app is launched - expenseRecords: - autoRefresh: - title: Auto-refresh expense records - desc: Refresh expense records when app is launched - clearCache: - title: Clear cache - desc: Clear the offline cache during use - request: Confirm to clear the cache? To reload them, you need connect to campus network again. - themeMode: - title: Brightness - proxy: - title: Proxy - desc: Proxy the access to campus network - authentication: Authentication - protocol: Protocol - hostname: Hostname - port: Port - username: Username - password: Password - invalidProxyFormatTip: Invalid proxy format - proxyChangedTip: Proxy was changed - shareQrCode: - title: Share QR code - desc: Share proxy as QR code - enableProxy: - title: Enable proxy - desc: Proxy your requests - proxyMode: - title: Proxy mode - global: - name: Global - tip: Access all networks by proxy - schoolOnly: - name: School-only - tip: Access non-school networks bypass proxy - proxyType: - http: HTTP - https: HTTPS - all: All - language: Language - wipeData: - title: Wipe data & log-out - desc: Wipe all data and log out - request: Wipe ALL Data - requestDesc: ALL data, such as login credentials and timetable imported, will be wiped permanently. -oaAnnounce: - title: OA announcement - tab: - info: Info - content: Content - noOaAnnouncementsTip: No announcements - downloadCompleted: Download completed - downloadFailed: Download failed - downloading: Downloading - info: - title: Title - publishTime: Publish Time - department: Department - author: Author - tags: Tags - attachmentHeader: - one: Attachment - other: Attachments - oaAnnounceCat: - studentAffairs: Student Affairs - learning: Learning - collegeNotification: College Notif. - culture: Culture - announcement: Announcement - life: Life - download: Download - training: Training - academicReport: Report -yellowPages: - title: Yellow pages -scanner: - barcodeNotRecognized: No Barcode or QR code -greeting: - headerA: SIT has accompanied you - headerB: - one: 1 Day - other: "{} Days" -qrCode: - hint: Please navigate to Me {me}, and click the Scanner {scan} on the right corner -404: - title: Not Found - subtitle: The requested page was not found -weekday: - 0: Monday - 1: Tuesday - 2: Wednesday - 3: Thursday - 4: Friday - 5: Saturday - 6: Sunday -weekdayShort: - 0: MON - 1: TUE - 2: WED - 3: THU - 4: FRI - 5: SAT - 6: SUN -campus: - xuhui: Xuhui - fengxian: Fengxian -unit: - rmb: "¥{}" - powerKwh: "{} kW·h" -time: - minute: min - hour: hour - hourMinuteFormat: "{hour} h {minute} min" - minuteFormat: "{} min" - hourFormat: "{} h" -brightness: - light: Light - dark: Dark -themeMode: - dark: Dark - light: Light - system: System -OaUserType: - undergraduate: Undergraduate - postgraduate: Postgraduate - other: Other -dormitoryRoom: "Building {building} #{room}" -open: Open -delete: Delete -confirm: Confirm -notNow: Not Now -error: Error -ok: OK -close: Close -submit: Submit -cancel: Cancel -save: Save -yes: Yes -refresh: Refresh -back: Back -clear: Clear -continue: Continue -unknown: Unknown -failed: Failed -download: Download -fetching: Fetching -warning: Warning -untitled: Untitled -congratulations: Congratulations -search: Search -seeAll: See all -select: Select -unselect: Unselect -share: Share -use: Use -used: Used -edit: Edit -copy: Copy -preview: Preview -upload: Upload -pick: Pick -duplicate: Duplicate -copyTip: "{} was copied" -done: Done diff --git a/assets/l10n/zh-Hans.yaml b/assets/l10n/zh-Hans.yaml deleted file mode 100644 index 096eaab9f..000000000 --- a/assets/l10n/zh-Hans.yaml +++ /dev/null @@ -1,697 +0,0 @@ -timetable: - navigation: 课程表 - jump: 跳转 - findToday: 找到今天 - startWith: "起始于" - weekOrderedName: "第 {} 周" - focusTimetable: 课程表模式 - signature: 签名 - signaturePlaceholder: 你的名字 - mine: - title: 我的课程表 - exportCalendar: 导出日历 - deleteRequest: 确认删除? - deleteRequestDesc: 这个课程表将被永久删除 - emptyTip: 快去导入一个课程表吧! - details: 详情 - details: - classCode: 教学班 - courseCode: 课程代码 - teacher: 教师 - edit: - name: 名称 - import: - title: 导入课程表 - import: 导入 - fromFile: 从文件 - tryImportBtn: 尝试导入 - fromFileBtn: 从文件导入 - connectivityCheckerDesc: 在导入课程表之前,需要先检查你是否能访问校园网。 - selectSemesterTip: 请先选择学年与学期 - endTip: 请完善课程表的信息。 - failed: 导入失败 - failedDesc: 无法导入这个课程表, 请稍后再试 - failedTip: 导入失败, 请稍后再试 - importing: 正在尝试获取课程表的数据…… - timetableInfo: 课程表信息 - defaultName: "{yearStart}年 {semester} 课程表" - export: - title: 导出选项 - export: 导出 - lessonMode: - title: 课程模式 - merged: 完整 - mergedTip: 同属一节课的几个部分会被合并成为一个日程 - separate: 分离 - separateTip: 同属一节课的几个部分会被分离为多个日程 - enableAlarm: - title: 启用提醒 - desc: 在课前添加提醒 - alarmMode: - title: 提醒模式 - sound: 声音 - display: 显示 - alarmDuration: 提醒时长 - alarmBeforeClassBegins: - title: 在上课前提醒 - desc: "提前 {duration}" - screenshot: - screenshot: 截图 - take: 截图 - title: 截图 - enableBackground: - title: 显示壁纸 - desc: 在截图中显示课程表壁纸 - displayMode: - daily: 日 - weekly: 周 - weekIndexType: - single: "第 {} 周" - all: "第 {start} 周到第 {end} 周" - odd: "第 {start} 周到第 {end} 周中的单周" - even: "第 {start} 周到第 {end} 周中的双周" - freeTip: - dayTip: 好耶,没课的一天! - isTodayTip: 尽情享受今天吧! - weekTip: 哇,这周没课! - isThisWeekTip: 尽情享受这周吧! - termTip: 不可思议,这个学期你都在放假! - findNearestWeekWithClass: 找到有课的一周 - findNearestDayWithClass: 找到有课的一天 - p13n: - title: 配色方案 - builtinPalette: - classic: - name: 经典 - americano: - name: 美式复古 - candy: - name: 糖果幻境 - sprint: - name: 兰时 - summary: - name: 时夏 - fall: - name: 萧辰 - winter: - name: 北陆 - thicket: - name: 小森林 - creeksideBreeze: - name: 溪畔听风 - palette: - title: 配色方案 - name: 名称 - tab: - custom: 自定义 - builtin: 内置 - info: 信息 - colors: 颜色 - namePlaceholder: 配色方案的名称 - author: 作者 - authorPlaceholder: 你的姓名,电子邮箱,或 url - color: 颜色 - details: 详情 - fab: 配色方案 - shareQrCode: 分享二维码 - newPaletteName: 新的配色方案 - deleteRequest: 确认删除? - deleteRequestDesc: 推荐先保存二维码备份再删除 - addFromQrCode: 已从二维码添加配色方案 - addColor: 添加一种配色 - cellStyle: - title: 单元格风格 - showTeachers: - title: 教师 - desc: 在单元格内显示教师 - grayOut: - title: 上过的课程变灰 - desc: 现在之前的所有课程 - harmonize: - title: 与主题色相协调 - desc: 将单元格颜色与主题色混合 - alpha: 单元格 alpha 通道 - background: - title: 壁纸 - opacity: 背景不透明度 - pickTip: 选择你喜欢的图片 - repeat: - title: 重复图像 - desc: 重复图像作为填充壁纸的图案 - antialias: - title: 抗锯齿 - desc: 如果图像像素较低,请关闭此功能 - livePreview: - 0: - name: 上过的 - place: 体育馆 - teachers: 李老师 - 1: - name: 上过的 - place: 教室 - teachers: 王老师 - 2: - name: 今天的 - place: 活动中心 - teachers: 张老师 - 3: - name: 明天的 - place: 实验室 - teachers: 刘老师 -school: - navigation: 学校 - schoolYear: 学年 - semester: - title: 学期 - all: 全学年 - term1: 第一学期 - term2: 第二学期 -life: - navigation: 生活 -me: - navigation: 个人 -class2nd: - title: 第二课堂 - activity: 活动 - attended: - title: 已参与 - tab: - info: 信息 - description: 描述 - refreshSuccessTip: 第二课堂分刷新成功 - refreshFailedTip: 第二课堂分刷新失败 - noActivities: 没有活动 - noAttendedActivities: 没有参加过的活动 - viewDetails: 查看详情 - apply: - btn: 报名 - replyTip: 校方回复 - applyRequest: 报名 - applyRequestDesc: 确认报名这个活动吗? - info: - activityId: 活动编号 - applicationId: 申请编号 - activityOf: "活动 #{}" - applicationOf: "申请 #{}" - name: 活动名称 - tags: 标签 - category: 活动类别 - scoreType: 加分类型 - honestyPoints: 诚信分 - totalPoints: 总分 - applicationTime: 申请时间 - status: 状态 - contactInfo: 联系方式 - duration: 活动时长 - location: 活动地点 - organizer: 主办方 - principal: 负责人 - signInTime: 签到时间 - signOutTime: 签退时间 - startTime: 开始时间 - undertaker: 承办方 - activityCat: - all: 全部 - lecture: 讲座报告 - thematicEdu: 主题教育 - creation: 创新创业创意 - schoolCultureActivity: 校园文化 - schoolCivilization: 校园文明 - practice: 社会实践 - voluntary: 志愿公益 - onlineSafetyEdu: 安全教育网络教学 - conference: 会议 - schoolCultureCompetition: 校园文化竞赛 - paperAndPatent: 论文专利 - scoreType: - all: 全部 - thematicReport: - full: 主题报告 - short: 讲座 - creation: - full: 创新创业创意 - short: 三创 - schoolCulture: - full: 校园文化 - short: 文化 - practice: - full: 社会实践 - short: 实践 - voluntary: - full: 志愿公益 - short: 志愿 - schoolSafetyCivilization: - full: 校园安全文明 - short: 文明 - noDetails: 暂无资料 - details: 活动资料 -ywb: - title: 应网办 - info: | - 本功能基于 "上应一网通办"。 - 对于绝大多数业务,即使你在平台上已完成申请,仍要去现场办理。 - apply: 申请 - noServicesTip: 没有可办事项 - mine: - title: 我的申请 - noApplicationsTip: 没有申请 - details: - apply: 申请 - type: - todo: 待办 - running: 在途 - complete: 办结 -captcha: - title: 验证码 - enterHint: 请输入验证码 - emptyInputError: 你输入的验证码为空 -credentials: - studentId: 学号 - account: 账号 - password: 密码 - oaAccount: OA 账号 - oaPwd: OA 密码 - savedOaPwd: 保存的 OA 密码 - savedOaPwdDesc: 用于自动登录 OA - pwd: 密码 - error: 登录凭据错误 - login: 登录 - forgotPwd: 忘记密码? -easyconnect: - launchBtn: 启动 EasyConnect - launchFailed: 启动失败 - launchFailedDesc: 无法启动 EasyConnect,请确定你是否安装,或者立即下载? -electricity: - title: 电费余额 - balance: 可用余额 - remainingPower: 剩余电量 - searchInvalidTip: 请输入或选择一个存在的房间号 - refreshSuccessTip: 电费余额刷新成功 - refreshFailedTip: 电费余额刷新失败 - searchRoom: 搜索房间 -eduEmail: - title: 教育邮箱 - action: - login: 登录 - inbox: 收件箱 - outbox: 发件箱 - login: - title: 登录教育邮箱 - addressHint: 学号/工号 - passwordHint: 你的邮箱密码 - invalidEmailAddressFormatTip: 无效的邮箱地址格式 - failedWarn: - title: 登录失败 - desc: 请检查密码 - info: - emailAddress: 邮箱地址 - inbox: - title: 收件箱 - outbox: - title: 发件箱 - noContent: 无内容 - noSubject: 无主题 - pluralSenderTailing: 等 - text: 正文 -examResult: - title: 考试成绩 - check: 查询 - teacherEval: 评教 - noResultsTip: 无考试结果记录 - lessonNotEvaluated: 未评教 - teacherEvalTitle: 评教 - compulsory: 必修 - credit: 学分 - elective: 选修 - gpa: - lessonSelected: "已选 {} 门" - gpaResult: "绩点: {}" -examArrange: - title: 考试安排 - check: 查询 - date: 日期 - time: 时间 - retake: 重修 - location: 地点 - noExamsTip: 没有考试安排 - seatNumber: 座位号 - addCalendarEvent: 添加事项 -expenseRecords: - title: 消费记录 - check: 查询 - statistics: 统计 - balanceInCard: "卡内余额 {} 元" - lastTransaction: "¥{amount} {place}" - income: "收入: {} 元" - outcome: "支出: {} 元" - noTransactionsTip: 没有交易记录 - refreshSuccessTip: 消费记录刷新成功 - refreshFailedTip: 消费记录刷新失败 - statsMode: - week: 周 - month: 月 - year: 年 - view: - balance: 卡内余额 - rmb: 元 - stats: - title: 消费统计 - categories: 消费名目 - total: 总计 - type: - consume: 消费 - coffee: 咖啡 - food: 食堂 - store: 商店 - water: 热水 - library: 图书馆 - shower: 洗浴 - topUp: 充值 - subsidy: 补贴 - other: 杂项 -homepage: - campusNetworkConnected: 已连接校园网 - campusNetworkUnconnected: 未连接校园网 -networkChecker: - testConnection: - title: 测试连接 - desc: 测试与学校服务器的连接 - button: - connected: 继续 - connecting: 正在检查中…… - disconnected: 重试 - none: 检查我的网络连接 - status: - connected: 好耶!你能够访问学校服务器! - connecting: 这可能需要几秒钟,请确保你在设备上已经打开了网络连接。 - disconnected: 糟糕!你无法访问学校服务器,请检查你的网络设置,然后再试一次。 - none: 你需要访问学校服务器才能继续,现在就检查一下吧。 -network: - error: 网络错误 - ipAddress: IP 地址 - connectionTimeoutError: 网络连接超时 - connectionTimeoutErrorDesc: "此功能需要访问校园网。注意: 在学校服务器崩溃或停机维护时, 也可能会出现这样的情况。" - openToolBtn: 打开网络工具 -networkTool: - title: 网络工具 - subtitle: 测试与学校服务器的连接 - openWlanSettingsBtn: 打开 WLAN 设置 - noAccessTip: 无法连接到网络,请检查你设备的网络设置后再试。 - connectionFailedError: | - 无法连接到校园网。疑难解答: - 1. 尝试连接学校的Wi-Fi: "i-SIT","i-SIT-1x" 或 "eduroam"。 - 2. 尝试使用"EasyConnect" VPN 连接校园网。 - 3. 尝试通过自行搭建的代理服务器使用 HTTP 代理连接校园网。 - 如果以上都不起作用, 可能因为是学校服务器停机维护或崩溃,请一段时间后重试。 - connectionFailedButCampusNetworkConnected: | - 已经连接至校园网,但仍然无法访问学校服务器。 - 可能因为是学校服务器停机维护或崩溃,请一段时间后重试。 - connectedByProxy: 已通过 HTTP 代理连接到校园网 - connectedByVpn: 已通过 VPN 连接到校园网 - connectedByWlan: 已通过 WLAN 连接到校园网 - connectedByEthernet: 已通过以太网连接到校园网 -login: - welcomeHeader: 欢迎! - loginOa: 登录 OA - validateInputAccountPwdRequest: 请检查你输入的账号与密码 - credentialsValidatedTip: 账号与密码验证成功 - accountOrPwdErrorTip: 账号或密码不正确 - captchaErrorTip: 验证码不正确 - accountFrozenTip: 账号处于冻结状态 - schoolServerUnconnectedTip: 无法连接学校服务器 - unknownAuthErrorTip: 未知账号验证错误 - accountHint: 学号/工号 - loggedInTip: 登录成功 - notLoggedIn: 未登录 - invalidAccountFormat: 账号格式不正确 - offlineModeBtn: 离线模式 - oaPwdHint: 输入你的OA密码 - failedWarn: 登录失败 - formatError: 格式错误 - loginRequired: 需要登录 - neverLoggedInTip: 你必须先登录才能继续。 -library: - title: 图书馆 - hotPost: 热搜 - readerId: 读者证号 - noBooks: 没有图书 - collectionStatus: 馆藏状态 - action: - searchBooks: 搜索图书 - login: 登录 - borrowing: 我的借阅 - login: - title: 登录图书馆 - readerIdHint: 你的学号 - passwordHint: 初始密码为 888888 - failedWarn: - title: 登录失败 - desc: 请检查密码 - search: - searchHistory: 搜索历史 - trending: 近期流行 - mostPopular: 最热门 - borrowing: - title: 我的借阅 - history: 历史 - renew: 续借 - history: - title: 历史 - operation: - borrowing: 借书 - returning: 还书 - searchMethod: - any: 任意 - title: 标题 - author: 作者 - isbn: ISBN - publisher: 出版社 - callNumber: 索书号 - primaryTitle: 正题 - subject: 主题 - $class: 分类 - bookId: 图书号 - orderNumber: 订购号 - info: - title: 标题 - author: 作者 - isbn: ISBN - publisher: 出版社 - publishDate: 出版日期 - callNumber: 索书号 - bookId: 图书号 - barcode: 条形码 - availableCollection: "在馆({available}) / 馆藏({collection})" -settings: - title: 设置 - version: 版本 - themeColor: 主题色 - fromSystem: 根据系统 - credentials: - testLoginOa: - title: 测试登录 OA - desc: 尝试使用当前的登录凭据登录 OA - dev: - title: 开发者选项 - devMode: - title: 开发者模式 - localStorage: - title: 本地存储 - desc: 管理在此设备上存储的数据 - selectBoxTip: 请先选择一个盒子。 - clearBoxDesc: 确定清空这个盒子里的数据吗? - deleteItemDesc: 确定删除这个键值对吗? - emptyValueDesc: 确定将这个键值设 null 为对吗? - reload: - title: 重新加载 - desc: 重新加载所有模块 - timetable: - title: 课程表 - autoUseImported: - title: 自动使用新课程表 - desc: 自动使用新导入的课程表 - palette: - title: 配色方案 - desc: 自定义课程表的颜色 - cellStyle: - title: 编辑单元格风格 - desc: 课程单元格如何显示 - background: - title: 壁纸 - desc: 课程表的背景 - school: - title: 学校 - class2nd: - autoRefresh: - title: 第二课堂分自动刷新 - desc: 启动时刷新第二课堂分 - life: - title: 生活 - electricity: - autoRefresh: - title: 电费余额自动刷新 - desc: 启动时刷新电费余额 - expenseRecords: - autoRefresh: - title: 消费记录自动刷新 - desc: 启动时刷新消费记录 - clearCache: - title: 清空缓存 - desc: 清空使用过程中产生的离线缓存 - request: 确定清空缓存?你下次需要连接校园网才能重新加载它们。 - themeMode: - title: 亮度 - proxy: - title: 代理 - desc: 代理校园网连接 - authentication: 身份验证 - protocol: 协议 - hostname: 主机名 - port: 端口 - username: 用户名 - password: 密码 - invalidProxyFormatTip: 代理的格式无效 - proxyChangedTip: 代理已更改 - shareQrCode: - title: 分享二维码 - desc: 将代理通过二维码分享 - enableProxy: - title: 启用代理 - desc: 代理你的请求 - proxyMode: - title: 代理模式 - global: - name: 全局 - tip: 通过代理访问所有网络 - schoolOnly: - name: 仅学校服务器 - tip: 仅通过代理访问学校服务器 - proxyType: - http: HTTP - https: HTTPS - all: 全部 - language: 语言 - wipeData: - title: 擦除数据并登出 - desc: 擦除所有数据并登出当前 OA 账号 - request: 擦除所有数据 - requestDesc: 此操作将永久擦除你的登录凭据,缓存和其他信息,如已导入的课程表。 -oaAnnounce: - title: OA 公告 - tab: - info: 信息 - content: 内容 - noOaAnnouncesTip: 无公告 - downloadCompleted: 下载完成 - downloadFailed: 下载失败 - downloading: 下载中 - info: - title: 标题 - publishTime: 发布时间 - department: 发布部门 - author: 作者 - tags: 标签 - attachmentHeader: - one: 附件 - other: 附件 - oaAnnounceCat: - studentAffairs: 学生事务 - learning: 学习课堂 - collegeNotification: 二级学院通知 - culture: 校园文化 - announcement: 公告信息 - life: 生活服务 - download: 文件下载专区 - training: 培養資訊 - academicReport: 學術報告 -yellowPages: - title: 学校黄页 -scanner: - barcodeNotRecognized: 未识别到条形码或二维码 -greeting: - headerA: 今天是你在上应大的 - headerB: - one: 第 1 天 - other: "第 {} 天" -qrCode: - hint: 请切换至 个人{me},然后点击右上角的 扫一扫{scan} -404: - title: 找不到页面 - subtitle: 未找到请求的页面 -weekday: - 0: 周一 - 1: 周二 - 2: 周三 - 3: 周四 - 4: 周五 - 5: 周六 - 6: 周日 -weekdayShort: - 0: 一 - 1: 二 - 2: 三 - 3: 四 - 4: 五 - 5: 六 - 6: 日 -campus: - xuhui: 徐汇 - fengxian: 奉贤 -unit: - rmb: "{} 元" - powerKwh: "{} 度" -time: - minute: 分钟 - hour: 小时 - hourMinuteFormat: "{hour} 小时 {minute} 分钟" - minuteFormat: "{} 分钟" - hourFormat: "{} 小时" -brightness: - light: 明亮 - dark: 黑暗 -themeMode: - dark: 黑暗 - light: 明亮 - system: 系统 -OaUserType: - undergraduate: 本科生 - postgraduate: 研究生 - other: 其他 -dormitoryRoom: "{building}号楼 #{room}" -open: 打开 -delete: 删除 -confirm: 确定 -notNow: 再等等 -error: 错误 -ok: 好的 -close: 关闭 -submit: 提交 -cancel: 取消 -save: 保存 -yes: 是的 -refresh: 刷新 -back: 返回 -clear: 清空 -continue: 继续 -unknown: 未知 -failed: 失败 -download: 下载 -fetching: 获取中 -warning: 警告 -untitled: 无标题 -congratulations: 恭喜 -search: 搜索 -seeAll: 查看全部 -select: 选择 -unselect: 取消选择 -share: 分享 -use: 使用 -used: 已使用 -edit: 编辑 -copy: 复制 -preview: 预览 -upload: 上传 -pick: 选择 -duplicate: 创建副本 -copyTip: "已复制 {} 到剪贴板了" -done: 完成 diff --git a/assets/l10n/zh-Hant.yaml b/assets/l10n/zh-Hant.yaml deleted file mode 100644 index 0de2a991b..000000000 --- a/assets/l10n/zh-Hant.yaml +++ /dev/null @@ -1,698 +0,0 @@ -timetable: - navigation: 課程表 - jump: 前往 - findToday: 尋找今天 - startWith: "起始於" - weekOrderedName: "第 {} 周" - focusTimetable: 對焦課程表 - signature: 簽名 - signaturePlaceholder: 你的名稱 - mine: - title: 我的課程表 - exportCalendar: 匯出行事曆 - deleteRequest: 確認刪除 - deleteRequestDesc: 你會失去這個課程表,永久地。 - emptyTip: 快去匯入一張新的課程表吧! - details: 詳細資料 - details: - classCode: 班级 - courseCode: 课程代码 - teacher: 教師 - edit: - name: 名稱 - import: - title: 匯入課程表 - import: 匯入 - fromFile: 從檔案 - tryImportBtn: 嘗試匯入 - fromFileBtn: 從檔案匯入 - connectivityCheckerDesc: 你需要訪問校園網路,才能匯入一個新的課程表。現在就開始檢查吧。 - selectSemesterTip: 請先選擇學年與學期 - endTip: 請補充課程表的資訊。 - failed: 匯入失敗 - failedDesc: 無法匯入課程表,請之後再試。 - failedTip: 匯入失敗,請之後再試。 - importing: 正在擷取課程表數據…… - timetableInfo: 課程表資訊 - defaultName: "{semester} {yearStart} 課程表" - export: - title: 匯出選項 - export: 匯出 - lessonMode: - title: 課程模式 - merged: 完整 - mergedTip: 同一節課的幾個部分會被合併為一個日程 - separate: 分離 - separateTip: 同一節課的幾個部分會被分離為多個日程 - enableAlarm: - title: 啟用提醒 - desc: 在課前增加提醒 - alarmMode: - title: 提醒模式 - desc: 如何通知你 - sound: 音效 - display: 顯示 - alarmDuration: 提醒時長 - alarmBeforeClassBegins: - title: 在上課前提醒 - desc: "提前 {duration}" - screenshot: - screenshot: 螢幕截圖 - take: 截圖 - title: 螢幕截圖 - enableBackground: - title: 顯示桌布 - desc: 在螢幕截圖中顯示課程表桌布 - displayMode: - daily: 日 - weekly: 周 - weekIndexType: - single: "第 {} 周" - all: "第 {start} 周到第 {end} 周" - odd: "第 {start} 周到第 {end} 週中的單週" - even: "第 {start} 周到第 {end} 週中的雙週" - freeTip: - dayTip: 好喔,這天沒課! - isTodayTip: "祝你今日過得愉快!🎉" - weekTip: 棒耶,這週都沒課! - isThisWeekTip: "祝你這週過得愉快!🎉" - termTip: "太酷啦,這學期都沒課耶! 🎉🎉🎉" - findNearestWeekWithClass: 尋找有課的一天 - findNearestDayWithClass: 尋找有課的一週 - p13n: - title: 調色盤 - builtinPalette: - classic: - name: 經典 - americano: - name: 美式復古 - candy: - name: 糖果幻境 - sprint: - name: 蘭時 - summary: - name: 時夏 - fall: - name: 蕭辰 - winter: - name: 北陸 - thicket: - name: 小森林 - creeksideBreeze: - name: 溪畔聽風 - palette: - title: 調色盤 - name: 名稱 - tab: - custom: 自訂 - builtin: 內建 - info: 資訊 - colors: 顏色 - namePlaceholder: 調色盤名稱 - author: 作者 - authorPlaceholder: 你的姓名,電郵,或 url - color: 顏色 - details: 詳細資料 - fab: 調色盤 - shareQrCode: 分享 QR 碼 - newPaletteName: 新的調色盤 - deleteRequest: 確認刪除? - deleteRequestDesc: 除非儲存了 QR 碼備份,否則調色板將被永久刪除 - addFromQrCode: 已從 QR 碼加載調色盤 - addColor: 增加一對顏色 - cellStyle: - title: 單元樣式 - showTeachers: - title: 教師 - desc: 在單元內顯示教師 - grayOut: - title: 已上的課程灰顯 - desc: 之前的所有課程 - harmonize: - title: 與主題顏色結合 - desc: 將單元格顏色與主題顏色混合 - alpha: 單元 alpha - background: - title: 桌布 - opacity: 背景不透明度 - pickTip: 選取你喜愛的影像 - repeat: - title: 重複影像 - desc: 重複影像作為圖案來填滿桌布 - antialias: - title: 反鋸齒 - desc: 如果影像像素較低,請關閉此功能 - livePreview: - 0: - name: 上過的 - place: 體育館 - teachers: 李老師 - 1: - name: 上過的 - place: 教室 - teachers: 王老師 - 2: - name: 今天的 - place: 活動中心 - teachers: 張老師 - 3: - name: 明天的 - place: 實驗室 - teachers: 劉老師 -school: - navigation: 學校 - schoolYear: 學年 - semester: - title: 學期 - all: 學年 - term1: 第一學期 - term2: 第二學期 -life: - navigation: 生活 -me: - navigation: 個人 -class2nd: - title: 第二課堂 - activity: 活動 - attended: - title: 已參與 - tab: - info: 資訊 - description: 描述 - refreshSuccessTip: 第二課堂分重新整理成功 - refreshFailedTip: 第二課堂分重新整理失敗 - noActivities: 無活動 - noAttendedActivities: 無參加過的活動 - viewDetails: 檢視詳細資料 - apply: - btn: 申請 - replyTip: 校方回信 - applyRequest: 申請 - applyRequestDesc: 確認要申請這個活動嗎? - info: - activityId: 活動 ID - applicationId: 申請 ID - activityOf: "活動 #{}" - applicationOf: "申請 #{}" - status: 狀態 - contactInfo: 联络人 - name: 活動名稱 - tags: 標籤 - category: 活動類別 - scoreType: 加分類型 - honestyPoints: 誠信分 - totalPoints: 總分 - applicationTime: 申請時間 - duration: 活動時長 - location: 活動地點 - organizer: 組織者 - principal: 主辦方 - signInTime: 簽到時間 - signOutTime: 簽退時間 - startTime: 開始時間 - undertaker: 承辦人 - activityCat: - all: 全部 - lecture: 講座報告 - thematicEdu: 主題教育 - creation: 創新創業創意 - schoolCultureActivity: 校園文化 - schoolCivilization: 校園文明 - practice: 社會實踐 - voluntary: 志願公益 - onlineSafetyEdu: 安全教育網路教學 - conference: 會議 - schoolCultureCompetition: 校園文化競賽 - paperAndPatent: 論文專利 - scoreType: - all: 全部 - thematicReport: - full: 講座報告 - short: 報告 - creation: - full: 創新創業創意 - short: 三創 - schoolCulture: - full: 校園文化 - short: 文化 - practice: - full: 社會實踐 - short: 實踐 - voluntary: - full: 志願公益 - short: 志願 - schoolSafetyCivilization: - full: 校園安全文明 - short: 安全 - noDetails: 暫無資訊 - details: 詳細資料 -ywb: - title: 應網辦 - info: | - 此功能基於 "上应一网通办"。 - 就大多數業務而言,即使你已在平台上完成申請,依舊需要到現場辦理。 - apply: 申請 - noServicesTip: 無可用申請 - mine: - title: 我的申請 - noApplicationsTip: 無申請 - details: - apply: 申請 - type: - todo: 待辦 - running: 在途 - complete: 辦結 -captcha: - title: 驗證碼 - enterHint: 請鍵入驗證碼 - emptyInputError: 未鍵入驗證碼 -credentials: - studentId: 學號 - account: 帳戶 - password: 密碼 - oaAccount: OA 帳戶 - oaPwd: OA 密碼 - savedOaPwd: 儲存的 OA 密碼 - savedOaPwdDesc: 用於自動登入 OA - pwd: 密碼 - error: 登入認證錯誤 - login: 登入 - forgotPwd: 忘記密碼? -easyconnect: - launchBtn: 啟用 EasyConnect - launchFailed: 啟動失敗 - launchFailedDesc: 無法為你啟動 EasyConnect,確保你安裝了它,或是現在開始下載? -electricity: - title: 電費餘額 - balance: 電費餘額 - remainingPower: 可用電量 - searchInvalidTip: 請先鍵入或選擇一個現有的房間號 - refreshSuccessTip: 電費餘額重新整理成功 - refreshFailedTip: 電費餘額重新整理失敗 - searchRoom: 搜尋房間 -eduEmail: - title: Edu 電郵 - action: - login: 登入 - inbox: 收件匣 - outbox: 寄件匣 - login: - title: 登入 Edu 電郵 - addressHint: 學號/工號 - passwordHint: 你的電郵密碼 - invalidEmailAddressFormatTip: 無效的電郵地址格式 - failedWarn: - title: 登入失敗 - desc: 請檢查密碼 - info: - emailAddress: 電郵地址 - inbox: - title: 收件匣 - outbox: - title: 寄件匣 - noContent: 無內容 - noSubject: 無主題 - pluralSenderTailing: 等 - text: 正文 -examResult: - title: 考試成績 - check: 查詢 - teacherEval: 評價教師 - noResultsTip: 無考試結果 - lessonNotEvaluated: 需評教 - teacherEvalTitle: 教師評價 - compulsory: 必修 - credit: 學分 - elective: 選修 - gpa: - lessonSelected: "已選 {} 門" - gpaResult: "績點: {}" -examArrange: - title: 考試安排 - check: 查詢 - date: 日期 - time: 時間 - retake: 重修 - location: 地點 - noExamsTip: 無考試安排 - seatNumber: 座位號 - addCalendarEvent: 增加事項 -expenseRecords: - title: 消費記錄 - check: 查詢 - statistics: 統計 - income: "入賬: ¥{}" - outcome: "消費: ¥{}" - balanceInCard: "卡內餘額 ¥{}" - lastTransaction: "¥{amount} {place}" - noTransactionsTip: 無交易記錄 - refreshSuccessTip: 消費記錄重新整理成功 - refreshFailedTip: 消費記錄重新整理失敗 - statsMode: - week: 周 - month: 月 - year: 年 - view: - balance: 卡內餘額 - rmb: 元 - stats: - title: 消費統計 - categories: 消費類別 - total: 總計 - type: - consume: 消費 - coffee: 咖啡 - food: 食堂 - store: 超商 - water: 熱水 - library: 圖書館 - shower: 洗浴 - topUp: 充值 - subsidy: 津貼 - other: 雜項 -homepage: - campusNetworkConnected: 已連線校園網路 - campusNetworkUnconnected: 未連線校園網路 -networkChecker: - testConnection: - title: 測試連線 - desc: 測試與校園網路的連線情況 - button: - connected: 繼續 - connecting: 檢查中…… - disconnected: 重試 - none: 檢查我的連線 - status: - connected: 好喔!你能夠訪問學校伺服器! - connecting: 這可能需要幾秒鐘的時間,請確認在裝置上啟用了網路。 - disconnected: 哎呀!你無法訪問學校伺服器誒,請檢查你的網路設定,然後再試一次。 - none: 你需要訪問學校伺服器才能繼續,現在就開始檢查吧! -network: - error: 網路錯誤 - ipAddress: IP 地址 - connectionTimeoutError: 網路逾時 - connectionTimeoutErrorDesc: "這個功能需要訪問學校網路。備註:這或許由於學校服務器當機或正在維護。" - openToolBtn: 打開網路工具 -networkTool: - title: 網路工具 - subtitle: 測試與學校伺服器的連線 - openWlanSettingsBtn: 打開 WLAN 設定 - noAccessTip: 無法連線到網路,請檢查你的裝置上的網路設定,然後再試一次。 - connectionFailedError: | - 無法連線到校園網路。疑難排解: - 1. 嘗試連線學校的Wi-Fi: "i-SIT","i-SIT-1x" 或 "eduroam"。 - 2. 嘗試啟用"EasyConnect" VPN 連線校園網路。 - 3. 嘗試通過自建的 HTTP Proxy 伺服器連線校園網路。 - 如果上述都無法工作, 或許因為學校伺服器停機維護或崩潰, 請稍後重試。 - connectionFailedButCampusNetworkConnected: | - 已經連線到校園網路,但依然無法訪問學校伺服器。 - 這或許因為學校伺服器停機維護或崩潰, 請稍後重試。 - connectedByProxy: 已通過 HTTP Proxy 連線校園網路。 - connectedByVpn: 已通過 VPN 連線校園網路。 - connectedByWlan: 已通過 WLAN 連線校園網路。 - connectedByEthernet: 已通過乙太網路連線校園網路。 -login: - welcomeHeader: 歡迎! - loginOa: 登入 OA - validateInputAccountPwdRequest: 請檢查你鍵入的賬戶和密碼 - credentialsValidatedTip: 帳戶與密碼正確 - accountOrPwdErrorTip: 賬戶或密碼錯誤 - captchaErrorTip: 驗證碼錯誤 - accountFrozenTip: 賬戶處於凍結狀態 - schoolServerUnconnectedTip: 無法連接學校伺服器 - unknownAuthErrorTip: 未知的帳戶身份驗證錯誤 - accountHint: 學號/工號 - loggedInTip: 已登入 - notLoggedIn: 未登入 - invalidAccountFormat: 無效的學號格式 - offlineModeBtn: 離線模式 - oaPwdHint: 鍵入你的 OA 密碼 - failedWarn: 登入失敗 - formatError: 格式錯誤 - loginRequired: 需要登入 - neverLoggedInTip: 你必須先登入才能繼續。 -library: - title: 圖書館 - hotPost: 熱門 - readerId: 讀者 ID - noBooks: 無圖書 - collectionStatus: 館藏狀態 - action: - searchBooks: 搜尋書 - login: 登入 - borrowing: 我的借閱 - login: - title: 登入圖書館 - readerIdHint: 你的學號 - passwordHint: 預設密碼為 888888 - failedWarn: - title: 登入失敗 - desc: 請檢查密碼 - search: - searchHistory: 搜尋歷史 - trending: 熱門 - mostPopular: 最受歡迎 - borrowing: - title: 我的借閱 - history: 歷史 - renew: 續借 - history: - title: 歷史 - operation: - borrowing: 借書 - returning: 還書 - searchMethod: - any: 任意 - title: 標題 - author: 作者 - isbn: ISBN - publisher: 發行商 - callNumber: 索書號 - primaryTitle: 主要標題 - subject: 主題 - $class: 類別 - bookId: 圖書 ID - orderNumber: 訂購號 - info: - title: 標題 - author: 作者 - isbn: ISBN - publisher: 發行商 - publishDate: 發行日期 - callNumber: 索書號 - bookId: 圖書 ID - barcode: 條碼 - availableCollection: "在館({available}) / 館藏({collection})" -settings: - title: 設定 - version: 版本 - themeColor: 主題顏色 - fromSystem: 從系統 - credentials: - testLoginOa: - title: 測試登入 OA - desc: 嘗試用當前的登入認證登入 OA - dev: - title: 開發者選項 - devMode: - title: 開發者模式 - localStorage: - title: 本機儲存 - desc: 管理在本裝置上的儲存 - selectBoxTip: 請先選擇一個盒子。 - clearBoxDesc: 確認清空這個盒子嗎? - deleteItemDesc: 確認刪除這個 key-value對 嗎? - emptyValueDesc: 確認將這個 key-value對 設定為 null 嗎? - reload: - title: 重新載入 - desc: 重新載入所有模塊 - timetable: - title: 課程表 - autoUseImported: - title: 自動使用匯入的 - desc: 使用新匯入的課程表 - palette: - title: 調色盤 - desc: 自訂課程表的調色盤 - cellStyle: - title: 編輯單元樣式 - desc: 課程單元看上去如何 - background: - title: 桌布 - desc: 課程表的背景 - school: - title: 學校 - class2nd: - autoRefresh: - title: 第二課堂分自動重新整理 - desc: 啟動時重新整理第二課堂分 - life: - title: 生活 - electricity: - autoRefresh: - title: 電費餘額自動重新整理 - desc: 啟動時重新整理電費餘額 - expenseRecords: - autoRefresh: - title: 消費記錄自動重新整理 - desc: 啟動時重新整理消費記錄 - clearCache: - title: 清空快取 - desc: 清空使用過程中產生的離線緩存 - request: 確認清空快取嗎?為加載它們,你必須重新連線校園網路。 - themeMode: - title: 亮度 - proxy: - title: 代理 - desc: 代理校園網路連線 - authentication: 驗證 - protocol: 通訊協定 - hostname: 主機名稱 - port: 連接埠 - username: 使用者名稱 - password: 密碼 - invalidProxyFormatTip: 無效的代理格式 - proxyChangedTip: 網路代理已變更 - shareQrCode: - title: 分享 QR 碼 - desc: 使用 QR 碼分享代理 - enableProxy: - title: 啟用代理 - desc: 代理你的請求 - proxyMode: - title: 代理模式 - global: - name: 全域 - tip: 通過代理訪問所有網路 - schoolOnly: - name: 僅校園網路 - tip: 繞過代理訪問非校園網路 - proxyType: - http: HTTP - https: HTTPS - all: 全部 - language: 語言 - wipeData: - title: 擦除資料 & 登出 - desc: 擦除帳戶並登出當前 OA 賬戶 - request: 擦除所有資料 - requestDesc: 這會擦除你本地除網頁快取外的登入資料與其他資料,例如匯入的課程表。 -oaAnnounce: - title: OA 公告 - tab: - info: 資訊 - content: 內容 - noOaAnnouncesTip: 無公告 - downloadCompleted: 下載完成 - downloadFailed: 下載失敗 - downloading: 下載中 - info: - title: 標題 - publishTime: 發佈日期 - department: 部門 - author: 作者 - tags: 標籤 - attachmentHeader: - one: 附件 - other: 附件 - oaAnnounceCat: - studentAffairs: 學生事務 - learning: 學習課堂 - collegeNotification: 學院通知 - culture: 校園文化 - announcement: 公告 - life: 生活服務 - download: 文件下載 - training: 培养信息 - academicReport: 学术报告 -yellowPages: - title: 學校黃頁 -scanner: - barcodeNotRecognized: 未識別到條碼或 QR 碼 -greeting: - headerA: 上應大與你一起度過了 - headerB: - one: 1 天 - other: "{} 天" -qrCode: - hint: 請導航至 個人{me},然後點擊右上角的 掃描{scan} -404: - title: 找不到頁面 - subtitle: 找不到所請求的頁面 -weekday: - 0: 週一 - 1: 週二 - 2: 週三 - 3: 週四 - 4: 週五 - 5: 週六 - 6: 週日 -weekdayShort: - 0: 一 - 1: 二 - 2: 三 - 3: 四 - 4: 五 - 5: 六 - 6: 日 -campus: - xuhui: 徐匯 - fengxian: 奉賢 -unit: - rmb: "{} 元" - powerKwh: "{} 千瓦時" -time: - minute: 分鐘 - hour: 小時 - hourMinuteFormat: "{hour} 小時 {minute} 分鐘" - minuteFormat: "{} 分鐘" - hourFormat: "{} 小時" -brightness: - light: 明亮 - dark: 黑暗 -themeMode: - dark: 深色 - light: 亮色 - system: 系統 -OaUserType: - undergraduate: 大學生 - postgraduate: 研究生 - other: 其他 -dormitoryRoom: "{building}號樓 #{room}" -open: 打開 -delete: 刪除 -confirm: 確認 -notNow: 現在不行 -error: 錯誤 -ok: 好的 -close: 關閉 -submit: 提交 -cancel: 取消 -save: 儲存 -yes: 好 -refresh: 重新整理 -back: 後退 -clear: 清除 -continue: 繼續 -unknown: 未知 -failed: 失敗 -download: 下載 -fetching: 取回中 -warning: 警告 -untitled: 無標題 -congratulations: 恭喜 -search: 搜尋 -seeAll: 查看全部 -select: 選擇 -unselect: 取消選擇 -share: 分享 -use: 使用 -used: 已使用 -edit: 編輯 -copy: 複製 -preview: 預覽 -upload: 上傳 -pick: 選取 -duplicate: 創建副本 -copyTip: "{} 被複製到剪貼板了" -done: 完成 diff --git a/assets/room_list.json b/assets/room_list.json deleted file mode 100644 index 510a169da..000000000 --- a/assets/room_list.json +++ /dev/null @@ -1 +0,0 @@ -[101106,101108,101110,101112,101114,101116,101118,101120,101122,101123,101124,101201,101202,101203,101204,101205,101206,101208,101210,101212,101214,101216,101218,101219,101220,101221,101222,101223,101224,101301,101302,101303,101304,101305,101306,101308,101310,101312,101314,101316,101318,101319,101320,101321,101322,101323,101324,101401,101402,101403,101404,101405,101406,101408,101410,101412,101414,101416,101418,101419,101420,101421,101422,101423,101424,101501,101502,101503,101504,101505,101506,101508,101510,101512,101514,101516,101518,101519,101520,101521,101522,101523,101524,101601,101602,101603,101604,101605,101606,101608,101610,101612,101614,101616,101618,101619,101620,101621,101622,101623,101624,102102,102106,102108,102110,102112,102114,102116,102118,102120,102122,102124,102125,102126,102127,102128,102130,102201,102202,102203,102204,102205,102206,102207,102208,102209,102210,102212,102214,102216,102218,102220,102222,102223,102224,102225,102226,102227,102228,102230,102301,102302,102303,102304,102305,102306,102307,102308,102309,102310,102312,102314,102316,102318,102320,102322,102323,102324,102325,102326,102327,102328,102330,102401,102402,102403,102404,102405,102406,102407,102408,102409,102410,102412,102414,102416,102418,102420,102422,102423,102424,102425,102426,102427,102428,102430,102501,102502,102503,102504,102505,102506,102507,102508,102509,102510,102512,102514,102516,102518,102520,102522,102523,102524,102525,102526,102527,102528,102530,102601,102602,102603,102604,102605,102606,102607,102608,102609,102610,102612,102614,102616,102618,102620,102622,102623,102624,102625,102626,102627,102628,102630,103102,103106,103108,103110,103112,103114,103116,103118,103120,103122,103124,103125,103126,103127,103128,103130,103201,103202,103203,103204,103205,103206,103207,103208,103209,103210,103212,103214,103216,103218,103220,103222,103223,103224,103225,103226,103227,103228,103230,103301,103302,103303,103304,103305,103306,103307,103308,103309,103310,103312,103314,103316,103318,103320,103322,103323,103324,103325,103326,103327,103328,103330,103401,103402,103403,103404,103405,103406,103407,103408,103409,103410,103412,103414,103416,103418,103420,103422,103423,103424,103425,103426,103427,103428,103430,103501,103502,103503,103504,103505,103506,103507,103508,103509,103510,103512,103514,103516,103518,103520,103522,103523,103524,103525,103526,103527,103528,103530,103601,103602,103603,103604,103605,103606,103607,103608,103609,103610,103612,103614,103616,103618,103620,103622,103623,103624,103625,103626,103627,103628,103630,104101,104102,104108,104110,104112,104114,104116,104118,104120,104122,104124,104125,104126,104127,104128,104130,104201,104202,104203,104204,104205,104206,104207,104208,104209,104210,104212,104214,104216,104218,104220,104222,104223,104224,104225,104226,104227,104228,104230,104301,104302,104303,104304,104305,104306,104307,104308,104309,104310,104312,104314,104316,104318,104320,104322,104323,104324,104325,104326,104327,104328,104330,104401,104402,104403,104404,104405,104406,104407,104408,104409,104410,104412,104414,104416,104418,104420,104422,104423,104424,104425,104426,104427,104428,104430,104501,104502,104503,104504,104505,104506,104507,104508,104509,104510,104512,104514,104516,104518,104520,104522,104523,104524,104525,104526,104527,104528,104530,104601,104602,104603,104604,104605,104606,104607,104608,104609,104610,104612,104614,104616,104618,104620,104622,104623,104624,104625,104626,104627,104628,104630,105102,105106,105108,105110,105112,105114,105116,105118,105120,105122,105124,105125,105126,105127,105128,105130,105201,105202,105203,105204,105205,105206,105207,105208,105209,105210,105212,105214,105216,105218,105220,105222,105223,105224,105225,105226,105227,105228,105230,105301,105302,105303,105304,105305,105306,105307,105308,105309,105310,105312,105314,105316,105318,105320,105322,105323,105324,105325,105326,105327,105328,105330,105401,105402,105403,105404,105405,105406,105407,105408,105409,105410,105412,105414,105416,105418,105420,105422,105423,105424,105425,105426,105427,105428,105430,105501,105502,105503,105504,105505,105506,105507,105508,105509,105510,105512,105514,105516,105518,105520,105522,105523,105524,105525,105526,105527,105528,105530,105601,105602,105603,105604,105605,105606,105607,105608,105609,105610,105612,105614,105616,105618,105620,105622,105623,105624,105625,105626,105627,105628,105630,106102,106104,106110,106112,106114,106116,106118,106122,106124,106201,106202,106203,106204,106205,106206,106208,106210,106212,106214,106216,106218,106219,106220,106221,106222,106223,106224,106301,106302,106303,106304,106305,106306,106308,106310,106312,106314,106316,106318,106319,106320,106321,106322,106323,106324,106401,106402,106403,106404,106405,106406,106408,106410,106412,106414,106416,106418,106419,106420,106421,106422,106423,106424,106501,106502,106503,106504,106505,106506,106508,106510,106512,106514,106516,106518,106519,106520,106521,106522,106523,106524,106601,106602,106603,106604,106605,106606,106608,106610,106612,106614,106616,106618,106619,106620,106621,106622,106623,106624,107102,107110,107112,107114,107116,107118,107120,107122,107124,107201,107202,107203,107204,107205,107206,107208,107210,107212,107214,107216,107218,107219,107220,107221,107222,107223,107224,107301,107302,107303,107304,107305,107306,107308,107310,107312,107314,107316,107318,107319,107320,107321,107322,107323,107324,107401,107402,107403,107404,107405,107406,107408,107410,107412,107414,107416,107418,107419,107420,107421,107422,107423,107424,107501,107502,107503,107504,107505,107506,107508,107510,107512,107514,107516,107518,107519,107520,107521,107522,107523,107524,107601,107602,107603,107604,107605,107606,107608,107610,107612,107614,107616,107618,107619,107620,107621,107622,107623,107624,108106,108112,108114,108116,108118,108119,108120,108121,108122,108124,108126,108128,108130,108201,108202,108203,108204,108205,108206,108207,108208,108209,108210,108212,108214,108216,108217,108218,108219,108220,108221,108222,108224,108226,108228,108301,108302,108303,108304,108305,108306,108307,108308,108309,108310,108312,108314,108316,108317,108318,108319,108320,108321,108322,108324,108326,108328,108330,108401,108402,108403,108404,108405,108406,108407,108408,108409,108410,108414,108416,108417,108418,108419,108420,108421,108422,108424,108426,108428,108501,108502,108503,108504,108505,108506,108507,108508,108509,108510,108512,108514,108516,108517,108518,108519,108520,108521,108522,108524,108526,108528,108530,108601,108602,108603,108605,108607,108608,108609,108610,108612,108614,108616,108617,108618,108619,108620,108621,108622,108624,108626,108628,109104,109110,109112,109114,109116,109118,109119,109120,109121,109122,109124,109126,109128,109130,109201,109202,109203,109204,109205,109206,109207,109208,109209,109210,109212,109214,109216,109217,109218,109219,109220,109221,109222,109224,109226,109228,109230,109301,109302,109303,109304,109305,109306,109307,109308,109309,109310,109312,109314,109316,109317,109318,109319,109320,109321,109322,109324,109326,109328,109330,109401,109402,109403,109404,109405,109406,109407,109408,109409,109410,109412,109414,109416,109417,109418,109419,109420,109421,109422,109424,109426,109428,109430,109501,109502,109503,109504,109505,109506,109507,109508,109509,109510,109512,109514,109516,109517,109518,109519,109520,109521,109522,109524,109526,109528,109530,109601,109602,109603,109604,109606,109607,109608,109609,109610,109612,109614,109616,109617,109618,109619,109620,109621,109622,109624,109626,109628,109630,1010106,1010108,1010110,1010112,1010114,1010116,1010118,1010120,1010122,1010124,1010201,1010202,1010203,1010204,1010205,1010206,1010208,1010210,1010212,1010214,1010215,1010216,1010217,1010218,1010219,1010220,1010222,1010224,1010301,1010302,1010303,1010304,1010305,1010306,1010308,1010310,1010312,1010314,1010315,1010316,1010317,1010318,1010319,1010320,1010322,1010324,1010401,1010402,1010403,1010404,1010405,1010406,1010408,1010410,1010412,1010414,1010415,1010416,1010417,1010418,1010419,1010420,1010422,1010424,1010501,1010502,1010503,1010504,1010505,1010506,1010508,1010510,1010512,1010514,1010515,1010516,1010517,1010518,1010519,1010520,1010522,1010524,1010601,1010602,1010603,1010604,1010605,1010606,1010608,1010610,1010612,1010614,1010615,1010616,1010617,1010618,1010619,1010620,1010622,1010624,1011104,1011105,1011106,1011116,1011118,1011120,1011122,1011124,1011126,1011201,1011202,1011203,1011204,1011205,1011206,1011208,1011210,1011212,1011214,1011216,1011218,1011220,1011222,1011223,1011224,1011225,1011226,1011227,1011228,1011229,1011230,1011301,1011302,1011303,1011304,1011305,1011306,1011308,1011310,1011312,1011314,1011316,1011318,1011320,1011322,1011323,1011324,1011325,1011326,1011327,1011329,1011330,1011401,1011402,1011403,1011404,1011405,1011406,1011408,1011410,1011412,1011414,1011416,1011418,1011420,1011422,1011423,1011424,1011425,1011426,1011427,1011428,1011429,1011430,1011501,1011502,1011503,1011504,1011505,1011506,1011508,1011510,1011512,1011514,1011516,1011518,1011520,1011522,1011523,1011524,1011525,1011526,1011527,1011528,1011529,1011530,1011601,1011602,1011603,1011604,1011605,1011606,1011608,1011610,1011612,1011614,1011616,1011618,1011620,1011622,1011623,1011624,1011625,1011626,1011627,1011628,1011629,1011630,1011701,1011702,1011703,1011704,1011705,1011706,1011708,1011710,1011712,1011714,1011716,1011718,1011720,1011722,1011723,1011724,1011725,1011726,1011727,1011728,1011729,1011730,1011801,1011802,1011803,1011804,1011805,1011806,1011808,1011810,1011812,1011814,1011816,1011818,1011820,1011822,1011823,1011824,1011825,1011826,1011827,1011828,1011829,1011830,1011901,1011902,1011903,1011904,1011905,1011906,1011908,1011910,1011912,1011914,1011916,1011918,1011920,1011922,1011923,1011924,1011925,1011926,1011927,1011928,1011929,1011930,10111001,10111002,10111003,10111004,10111005,10111006,10111008,10111010,10111012,10111014,10111016,10111018,10111020,10111022,10111023,10111024,10111025,10111026,10111027,10111028,10111029,10111030,10111101,10111102,10111103,10111104,10111105,10111106,10111108,10111110,10111112,10111114,10111116,10111118,10111120,10111122,10111123,10111124,10111125,10111126,10111127,10111128,10111129,10111130,10111201,10111202,10111203,10111204,10111205,10111206,10111208,10111210,10111212,10111214,10111216,10111218,10111220,10111222,10111223,10111224,10111225,10111226,10111227,10111228,10111229,10111230,10111301,10111302,10111303,10111304,10111305,10111306,10111308,10111310,10111312,10111314,10111316,10111318,10111320,10111322,10111323,10111324,10111325,10111326,10111327,10111328,10111329,10111330,10111401,10111402,10111403,10111404,10111405,10111406,10111408,10111410,10111412,10111414,10111416,10111418,10111420,10111422,10111423,10111424,10111425,10111426,10111427,10111428,10111429,10111430,10111501,10111502,10111503,10111504,10111505,10111506,10111508,10111510,10111512,10111514,10111516,10111518,10111520,10111522,10111523,10111524,10111525,10111526,10111527,10111528,10111529,10111530,10111601,10111602,10111603,10111604,10111605,10111606,10111608,10111610,10111612,10111614,10111616,10111618,10111620,10111622,10111623,10111624,10111625,10111626,10111627,10111628,10111629,10111630,1012104,1012105,1012106,1012107,1012108,1012114,1012116,1012118,1012120,1012201,1012202,1012203,1012204,1012205,1012206,1012207,1012208,1012210,1012212,1012214,1012216,1012218,1012219,1012220,1012221,1012222,1012223,1012224,1012301,1012302,1012303,1012304,1012305,1012306,1012307,1012308,1012310,1012312,1012314,1012316,1012318,1012319,1012320,1012321,1012322,1012323,1012324,1012401,1012402,1012403,1012404,1012405,1012406,1012407,1012408,1012410,1012412,1012414,1012416,1012418,1012419,1012420,1012421,1012422,1012423,1012424,1012501,1012502,1012503,1012504,1012505,1012506,1012507,1012508,1012510,1012512,1012514,1012516,1012518,1012519,1012520,1012521,1012522,1012523,1012524,1012601,1012602,1012603,1012604,1012605,1012606,1012607,1012608,1012610,1012612,1012614,1012616,1012618,1012619,1012620,1012621,1012622,1012624,1013101,1013108,1013116,1013118,1013120,1013122,1013124,1013126,1013201,1013202,1013203,1013204,1013205,1013206,1013208,1013210,1013212,1013214,1013216,1013218,1013220,1013222,1013223,1013224,1013225,1013226,1013227,1013228,1013229,1013230,1013301,1013302,1013303,1013304,1013305,1013306,1013308,1013310,1013312,1013314,1013316,1013318,1013320,1013322,1013323,1013324,1013325,1013326,1013327,1013328,1013329,1013330,1013401,1013402,1013403,1013404,1013405,1013406,1013408,1013410,1013412,1013414,1013416,1013418,1013420,1013422,1013423,1013424,1013425,1013426,1013427,1013428,1013429,1013430,1013501,1013502,1013503,1013504,1013505,1013506,1013508,1013510,1013512,1013514,1013516,1013518,1013520,1013522,1013523,1013524,1013525,1013526,1013527,1013528,1013529,1013530,1013601,1013602,1013603,1013604,1013605,1013606,1013608,1013610,1013612,1013614,1013616,1013618,1013620,1013622,1013623,1013624,1013625,1013626,1013627,1013628,1013629,1013630,1013701,1013702,1013703,1013704,1013705,1013706,1013708,1013710,1013712,1013714,1013716,1013718,1013720,1013722,1013723,1013724,1013725,1013726,1013727,1013728,1013729,1013730,1013801,1013802,1013803,1013804,1013805,1013806,1013808,1013810,1013812,1013814,1013816,1013818,1013820,1013822,1013823,1013824,1013825,1013826,1013827,1013828,1013829,1013830,1013901,1013902,1013903,1013904,1013905,1013906,1013908,1013910,1013912,1013914,1013916,1013918,1013920,1013922,1013923,1013924,1013925,1013926,1013927,1013928,1013929,1013930,10131001,10131002,10131003,10131004,10131005,10131006,10131008,10131010,10131012,10131014,10131016,10131018,10131020,10131022,10131023,10131024,10131025,10131026,10131027,10131028,10131029,10131030,10131101,10131102,10131103,10131104,10131105,10131106,10131108,10131110,10131112,10131114,10131116,10131118,10131120,10131122,10131123,10131124,10131125,10131126,10131127,10131128,10131129,10131130,10131201,10131202,10131203,10131204,10131205,10131206,10131208,10131210,10131212,10131214,10131216,10131218,10131220,10131222,10131223,10131224,10131225,10131226,10131227,10131228,10131229,10131230,10131301,10131302,10131303,10131304,10131305,10131306,10131308,10131310,10131312,10131314,10131316,10131318,10131320,10131322,10131323,10131324,10131325,10131326,10131327,10131328,10131329,10131330,10131401,10131402,10131403,10131404,10131405,10131406,10131408,10131410,10131412,10131414,10131416,10131418,10131420,10131422,10131423,10131424,10131425,10131426,10131427,10131428,10131429,10131430,10131501,10131502,10131503,10131504,10131505,10131506,10131508,10131510,10131512,10131514,10131516,10131518,10131520,10131522,10131523,10131524,10131525,10131526,10131527,10131528,10131529,10131530,10131601,10131602,10131603,10131604,10131605,10131606,10131608,10131610,10131612,10131614,10131616,10131618,10131620,10131622,10131623,10131624,10131625,10131626,10131627,10131628,10131629,10131630,1014104,1014108,1014110,1014112,1014116,1014118,1014120,1014122,1014124,1014201,1014202,1014203,1014204,1014205,1014206,1014207,1014208,1014210,1014212,1014214,1014216,1014218,1014219,1014220,1014221,1014222,1014223,1014224,1014301,1014302,1014303,1014304,1014305,1014306,1014307,1014308,1014310,1014312,1014314,1014316,1014318,1014319,1014320,1014321,1014322,1014323,1014324,1014401,1014402,1014403,1014404,1014405,1014406,1014407,1014408,1014410,1014412,1014414,1014416,1014418,1014419,1014420,1014421,1014422,1014423,1014424,1014501,1014502,1014503,1014504,1014505,1014506,1014507,1014508,1014510,1014512,1014514,1014516,1014518,1014519,1014520,1014521,1014522,1014523,1014524,1014601,1014602,1014603,1014604,1014605,1014606,1014607,1014608,1014610,1014612,1014614,1014616,1014618,1014619,1014620,1014621,1014622,1014623,1014624,1015106,1015116,1015118,1015120,1015122,1015124,1015126,1015201,1015202,1015203,1015204,1015205,1015206,1015208,1015210,1015212,1015214,1015216,1015218,1015220,1015223,1015224,1015225,1015226,1015227,1015228,1015229,1015230,1015301,1015302,1015303,1015304,1015305,1015306,1015308,1015310,1015312,1015314,1015316,1015318,1015320,1015322,1015323,1015324,1015325,1015326,1015327,1015328,1015329,1015330,1015401,1015402,1015403,1015404,1015405,1015406,1015408,1015410,1015412,1015414,1015416,1015418,1015420,1015422,1015423,1015424,1015425,1015426,1015427,1015428,1015429,1015430,1015501,1015502,1015503,1015504,1015505,1015506,1015508,1015510,1015512,1015514,1015516,1015518,1015520,1015522,1015523,1015524,1015525,1015526,1015527,1015528,1015529,1015530,1015601,1015602,1015603,1015604,1015605,1015606,1015608,1015610,1015612,1015614,1015616,1015618,1015620,1015622,1015623,1015624,1015625,1015626,1015627,1015628,1015629,1015630,1015701,1015702,1015703,1015704,1015705,1015706,1015708,1015710,1015712,1015714,1015716,1015718,1015720,1015722,1015723,1015724,1015725,1015726,1015727,1015728,1015729,1015730,1015801,1015802,1015803,1015804,1015805,1015806,1015808,1015810,1015812,1015814,1015816,1015818,1015820,1015822,1015823,1015824,1015825,1015826,1015827,1015828,1015829,1015830,1015901,1015902,1015903,1015904,1015905,1015906,1015908,1015910,1015912,1015914,1015916,1015918,1015920,1015922,1015923,1015924,1015925,1015926,1015927,1015928,1015929,1015930,10151001,10151002,10151003,10151004,10151005,10151006,10151008,10151010,10151012,10151014,10151016,10151018,10151020,10151022,10151023,10151024,10151025,10151026,10151027,10151028,10151029,10151030,10151101,10151102,10151103,10151104,10151105,10151106,10151108,10151110,10151112,10151114,10151116,10151118,10151120,10151122,10151123,10151124,10151125,10151126,10151127,10151128,10151129,10151130,10151201,10151202,10151203,10151204,10151205,10151206,10151208,10151210,10151212,10151214,10151216,10151218,10151220,10151222,10151223,10151224,10151226,10151227,10151228,10151229,10151230,10151302,10151303,10151304,10151305,10151306,10151308,10151310,10151312,10151314,10151316,10151318,10151320,10151322,10151323,10151324,10151325,10151326,10151327,10151328,10151329,10151330,10151401,10151402,10151403,10151404,10151405,10151406,10151408,10151410,10151412,10151414,10151416,10151418,10151420,10151422,10151423,10151424,10151425,10151426,10151427,10151428,10151429,10151430,10151501,10151502,10151503,10151504,10151505,10151506,10151508,10151510,10151512,10151514,10151516,10151518,10151520,10151522,10151523,10151524,10151525,10151526,10151527,10151528,10151530,10151601,10151602,10151603,10151604,10151605,10151606,10151608,10151610,10151612,10151614,10151616,10151618,10151620,10151622,10151623,10151624,10151625,10151626,10151627,10151628,10151629,10151630,1016101,1016103,1016105,1016106,1016108,1016116,1016118,1016120,1016122,1016124,1016126,1016201,1016202,1016203,1016204,1016205,1016206,1016208,1016210,1016212,1016214,1016216,1016218,1016220,1016222,1016223,1016224,1016225,1016226,1016227,1016228,1016229,1016230,1016301,1016302,1016303,1016304,1016305,1016306,1016308,1016310,1016312,1016314,1016316,1016318,1016320,1016322,1016323,1016324,1016325,1016326,1016327,1016328,1016329,1016330,1016401,1016402,1016403,1016404,1016405,1016406,1016408,1016410,1016412,1016414,1016416,1016418,1016420,1016422,1016423,1016424,1016425,1016426,1016427,1016428,1016429,1016430,1016501,1016502,1016503,1016504,1016505,1016506,1016508,1016510,1016512,1016514,1016516,1016518,1016520,1016522,1016523,1016524,1016525,1016526,1016527,1016528,1016529,1016530,1016601,1016602,1016603,1016604,1016605,1016606,1016608,1016610,1016612,1016614,1016616,1016618,1016620,1016622,1016623,1016624,1016625,1016626,1016627,1016628,1016629,1016630,1016701,1016702,1016703,1016704,1016705,1016706,1016708,1016710,1016712,1016714,1016716,1016718,1016720,1016722,1016723,1016724,1016725,1016726,1016727,1016728,1016729,1016730,1016801,1016802,1016803,1016804,1016805,1016806,1016808,1016810,1016812,1016814,1016816,1016818,1016820,1016822,1016823,1016824,1016825,1016826,1016827,1016828,1016829,1016830,1016901,1016902,1016903,1016904,1016905,1016906,1016908,1016910,1016912,1016914,1016916,1016918,1016920,1016922,1016923,1016924,1016925,1016926,1016927,1016928,1016929,1016930,10161001,10161002,10161003,10161004,10161005,10161006,10161008,10161010,10161012,10161014,10161016,10161018,10161020,10161022,10161023,10161024,10161025,10161026,10161027,10161028,10161029,10161030,10161101,10161102,10161103,10161104,10161105,10161106,10161108,10161110,10161112,10161114,10161116,10161118,10161120,10161122,10161123,10161124,10161125,10161126,10161127,10161128,10161129,10161201,10161202,10161203,10161204,10161205,10161206,10161208,10161210,10161212,10161214,10161216,10161218,10161220,10161222,10161223,10161224,10161225,10161226,10161227,10161228,10161229,10161230,10161301,10161302,10161303,10161304,10161305,10161306,10161308,10161310,10161312,10161314,10161316,10161318,10161320,10161322,10161323,10161324,10161325,10161326,10161327,10161328,10161329,10161330,10161401,10161402,10161403,10161404,10161405,10161406,10161408,10161410,10161412,10161414,10161416,10161418,10161420,10161422,10161423,10161424,10161425,10161426,10161427,10161428,10161429,10161430,10161501,10161502,10161503,10161504,10161505,10161506,10161508,10161510,10161512,10161514,10161516,10161518,10161520,10161522,10161523,10161524,10161525,10161526,10161527,10161528,10161529,10161530,10161601,10161602,10161603,10161604,10161605,10161606,10161608,10161610,10161612,10161614,10161616,10161618,10161620,10161622,10161623,10161624,10161625,10161626,10161627,10161628,10161629,10161630,1017101,1017102,1017103,1017104,1017106,1017108,1017110,1017112,1017114,1017116,1017118,1017119,1017120,1017122,1017124,1017126,1017128,1017201,1017202,1017203,1017204,1017206,1017208,1017210,1017211,1017212,1017213,1017214,1017215,1017216,1017217,1017218,1017219,1017220,1017222,1017224,1017226,1017227,1017228,1017229,1017230,1017301,1017302,1017303,1017304,1017306,1017308,1017310,1017311,1017312,1017313,1017314,1017315,1017316,1017317,1017318,1017319,1017320,1017322,1017324,1017327,1017328,1017329,1017330,1017401,1017402,1017403,1017404,1017406,1017408,1017410,1017411,1017412,1017413,1017414,1017415,1017416,1017417,1017418,1017419,1017420,1017422,1017424,1017426,1017427,1017428,1017429,1017430,1017501,1017502,1017503,1017504,1017506,1017508,1017510,1017511,1017512,1017513,1017514,1017515,1017516,1017517,1017518,1017519,1017520,1017522,1017524,1017526,1017527,1017528,1017529,1017530,1017601,1017602,1017603,1017604,1017606,1017608,1017610,1017611,1017612,1017613,1017614,1017615,1017616,1017617,1017618,1017619,1017620,1017622,1017624,1017626,1017627,1017629,1017630,1018201,1018203,1018204,1018206,1018208,1018210,1018211,1018212,1018213,1018214,1018215,1018216,1018218,1018220,1018222,1018224,1018225,1018226,1018227,1018228,1018229,1018230,1018301,1018302,1018303,1018304,1018306,1018308,1018310,1018311,1018312,1018313,1018314,1018315,1018316,1018317,1018318,1018320,1018322,1018324,1018325,1018326,1018327,1018328,1018329,1018330,1018401,1018402,1018403,1018404,1018406,1018408,1018410,1018411,1018412,1018413,1018414,1018415,1018416,1018417,1018418,1018420,1018422,1018424,1018425,1018426,1018427,1018428,1018429,1018430,1018501,1018502,1018503,1018506,1018508,1018510,1018511,1018512,1018513,1018514,1018515,1018516,1018517,1018518,1018520,1018522,1018524,1018525,1018526,1018527,1018528,1018529,1018530,1018601,1018602,1018603,1018604,1018608,1018610,1018611,1018612,1018613,1018614,1018615,1018616,1018617,1018618,1018620,1018622,1018624,1018625,1018626,1018627,1018628,1018629,1018630,1019103,1019110,1019112,1019114,1019116,1019118,1019119,1019120,1019121,1019122,1019123,1019124,1019125,1019126,1019201,1019202,1019203,1019204,1019206,1019207,1019208,1019209,1019210,1019211,1019212,1019213,1019214,1019215,1019216,1019217,1019218,1019219,1019220,1019222,1019223,1019224,1019226,1019228,1019301,1019302,1019303,1019304,1019306,1019307,1019308,1019309,1019310,1019311,1019312,1019313,1019314,1019315,1019316,1019317,1019318,1019319,1019320,1019321,1019322,1019323,1019324,1019325,1019328,1019401,1019402,1019403,1019404,1019406,1019407,1019408,1019409,1019410,1019411,1019412,1019413,1019414,1019415,1019416,1019417,1019418,1019419,1019420,1019421,1019422,1019423,1019424,1019425,1019426,1019427,1019428,1019501,1019502,1019503,1019504,1019506,1019507,1019508,1019509,1019510,1019511,1019512,1019513,1019514,1019515,1019516,1019517,1019518,1019519,1019520,1019521,1019522,1019523,1019524,1019525,1019526,1019527,1019528,1019601,1019602,1019603,1019604,1019606,1019607,1019608,1019609,1019610,1019611,1019612,1019613,1019614,1019615,1019616,1019617,1019618,1019619,1019620,1019621,1019622,1019623,1019624,1019625,1019626,1019627,1019628,1020201,1020202,1020203,1020204,1020205,1020206,1020208,1020210,1020212,1020214,1020215,1020216,1020217,1020218,1020219,1020220,1020222,1020224,1020226,1020227,1020229,1020301,1020302,1020303,1020304,1020305,1020306,1020308,1020310,1020312,1020313,1020314,1020315,1020316,1020317,1020318,1020319,1020320,1020322,1020324,1020326,1020327,1020328,1020329,1020330,1020401,1020402,1020403,1020404,1020405,1020406,1020408,1020410,1020412,1020413,1020414,1020415,1020416,1020417,1020418,1020419,1020420,1020422,1020424,1020426,1020427,1020428,1020429,1020430,1020501,1020502,1020503,1020504,1020505,1020506,1020508,1020510,1020512,1020513,1020514,1020515,1020516,1020517,1020518,1020519,1020520,1020522,1020524,1020526,1020527,1020528,1020529,1020530,1020601,1020602,1020603,1020604,1020605,1020606,1020608,1020610,1020612,1020613,1020614,1020615,1020616,1020617,1020618,1020619,1020620,1020622,1020624,1020626,1020627,1020628,1020629,1020630,1021201,1021202,1021203,1021204,1021205,1021206,1021207,1021208,1021210,1021212,1021214,1021216,1021218,1021219,1021220,1021221,1021222,1021223,1021224,1021301,1021302,1021303,1021304,1021305,1021306,1021307,1021308,1021310,1021312,1021314,1021316,1021318,1021319,1021320,1021321,1021322,1021323,1021324,1021401,1021402,1021403,1021404,1021405,1021406,1021407,1021408,1021410,1021412,1021414,1021416,1021418,1021419,1021420,1021421,1021422,1021423,1021424,1021501,1021502,1021503,1021504,1021505,1021506,1021507,1021508,1021510,1021512,1021514,1021516,1021518,1021519,1021520,1021521,1021522,1021523,1021524,1021601,1021602,1021603,1021604,1021605,1021606,1021607,1021608,1021610,1021612,1021614,1021616,1021618,1021619,1021620,1021621,1021622,1021623,1021624,1022201,1022202,1022203,1022204,1022205,1022206,1022207,1022208,1022210,1022212,1022214,1022216,1022218,1022219,1022220,1022221,1022222,1022223,1022224,1022301,1022302,1022303,1022304,1022305,1022306,1022307,1022308,1022310,1022312,1022314,1022316,1022318,1022319,1022320,1022321,1022322,1022323,1022324,1022401,1022402,1022403,1022404,1022405,1022406,1022407,1022408,1022410,1022412,1022414,1022416,1022418,1022419,1022420,1022421,1022422,1022423,1022424,1022501,1022502,1022503,1022504,1022505,1022506,1022507,1022508,1022510,1022512,1022514,1022516,1022518,1022519,1022520,1022521,1022522,1022523,1022524,1022601,1022602,1022603,1022604,1022605,1022606,1022607,1022608,1022610,1022612,1022614,1022616,1022618,1022619,1022620,1022621,1022622,1022623,1022624,1023101,1023105,1023106,1023108,1023116,1023118,1023120,1023122,1023124,1023126,1023201,1023202,1023203,1023204,1023205,1023206,1023208,1023210,1023212,1023214,1023216,1023218,1023220,1023222,1023223,1023224,1023225,1023226,1023227,1023228,1023229,1023230,1023301,1023302,1023303,1023304,1023305,1023306,1023308,1023310,1023312,1023314,1023316,1023318,1023320,1023322,1023323,1023324,1023325,1023326,1023327,1023328,1023329,1023330,1023401,1023402,1023403,1023404,1023405,1023406,1023408,1023410,1023412,1023414,1023416,1023418,1023420,1023422,1023423,1023424,1023425,1023426,1023427,1023428,1023429,1023430,1023501,1023502,1023503,1023504,1023505,1023506,1023508,1023510,1023512,1023514,1023516,1023518,1023520,1023522,1023523,1023524,1023525,1023526,1023527,1023528,1023529,1023530,1023601,1023602,1023603,1023604,1023605,1023606,1023608,1023610,1023612,1023614,1023616,1023618,1023620,1023622,1023623,1023624,1023625,1023626,1023627,1023628,1023629,1023630,1023701,1023702,1023703,1023704,1023705,1023706,1023708,1023710,1023712,1023714,1023716,1023718,1023720,1023722,1023723,1023724,1023725,1023726,1023727,1023728,1023729,1023730,1023801,1023802,1023803,1023804,1023805,1023806,1023808,1023810,1023814,1023816,1023818,1023820,1023822,1023823,1023824,1023826,1023827,1023828,1023829,1023830,1023901,1023902,1023903,1023904,1023905,1023906,1023908,1023910,1023912,1023914,1023916,1023918,1023920,1023922,1023923,1023924,1023925,1023926,1023927,1023928,1023929,1023930,10231001,10231002,10231003,10231004,10231005,10231006,10231008,10231010,10231012,10231014,10231016,10231018,10231020,10231022,10231023,10231024,10231025,10231026,10231027,10231028,10231029,10231030,10231101,10231102,10231103,10231104,10231105,10231106,10231108,10231110,10231112,10231114,10231116,10231118,10231120,10231122,10231123,10231124,10231125,10231126,10231127,10231128,10231129,10231130,10231201,10231202,10231204,10231205,10231206,10231208,10231210,10231212,10231214,10231216,10231218,10231220,10231222,10231223,10231224,10231225,10231226,10231227,10231228,10231229,10231230,1024104,1024105,1024106,1024116,1024118,1024120,1024124,1024126,1024201,1024202,1024203,1024204,1024205,1024206,1024208,1024210,1024212,1024214,1024216,1024218,1024220,1024222,1024223,1024224,1024225,1024226,1024227,1024228,1024229,1024230,1024301,1024302,1024303,1024304,1024305,1024306,1024308,1024310,1024312,1024314,1024316,1024318,1024320,1024322,1024323,1024324,1024325,1024326,1024327,1024328,1024329,1024330,1024401,1024402,1024403,1024404,1024405,1024406,1024408,1024410,1024412,1024414,1024416,1024418,1024420,1024422,1024423,1024424,1024425,1024426,1024427,1024428,1024429,1024430,1024501,1024502,1024503,1024504,1024505,1024506,1024508,1024510,1024512,1024516,1024518,1024520,1024522,1024523,1024524,1024525,1024526,1024527,1024528,1024529,1024530,1024601,1024602,1024603,1024604,1024605,1024606,1024608,1024610,1024612,1024614,1024616,1024618,1024620,1024622,1024623,1024624,1024625,1024626,1024627,1024628,1024629,1024630,1024701,1024702,1024703,1024704,1024705,1024706,1024708,1024710,1024712,1024714,1024716,1024718,1024720,1024722,1024723,1024724,1024725,1024726,1024727,1024728,1024729,1024730,1024801,1024802,1024803,1024804,1024805,1024806,1024808,1024810,1024812,1024814,1024816,1024818,1024820,1024822,1024823,1024824,1024825,1024826,1024827,1024828,1024829,1024830,1024901,1024902,1024903,1024904,1024905,1024906,1024908,1024910,1024912,1024914,1024916,1024918,1024920,1024922,1024923,1024924,1024925,1024926,1024927,1024928,1024929,1024930,10241001,10241002,10241003,10241004,10241005,10241006,10241008,10241010,10241012,10241014,10241016,10241018,10241020,10241022,10241023,10241024,10241025,10241026,10241027,10241028,10241029,10241030,10241101,10241102,10241103,10241104,10241105,10241106,10241108,10241110,10241112,10241114,10241116,10241118,10241120,10241122,10241123,10241124,10241125,10241126,10241127,10241128,10241129,10241130,10241201,10241202,10241203,10241204,10241205,10241206,10241208,10241210,10241212,10241214,10241216,10241218,10241220,10241222,10241223,10241224,10241225,10241226,10241227,10241228,10241229,10241230,1025102,1025109,1025111,1025114,1025118,1025120,1025122,1025124,1025126,1025128,1025201,1025202,1025203,1025204,1025205,1025206,1025207,1025208,1025209,1025210,1025211,1025212,1025214,1025216,1025218,1025220,1025222,1025224,1025226,1025228,1025301,1025302,1025303,1025304,1025305,1025306,1025307,1025308,1025309,1025310,1025311,1025312,1025314,1025316,1025318,1025320,1025322,1025324,1025326,1025328,1025401,1025402,1025403,1025404,1025405,1025406,1025407,1025408,1025409,1025410,1025411,1025412,1025414,1025416,1025418,1025420,1025422,1025424,1025426,1025428,1025501,1025502,1025503,1025504,1025505,1025506,1025507,1025508,1025509,1025510,1025511,1025512,1025514,1025516,1025518,1025520,1025522,1025524,1025528,1025601,1025602,1025603,1025604,1025605,1025606,1025607,1025608,1025609,1025610,1025611,1025612,1025614,1025616,1025618,1025620,1025622,1025624,1025626,1025628,1025701,1025702,1025703,1025704,1025705,1025706,1025707,1025708,1025709,1025710,1025711,1025712,1025714,1025716,1025718,1025720,1025722,1025724,1025726,1025728,1025801,1025802,1025803,1025804,1025805,1025806,1025807,1025808,1025809,1025810,1025811,1025812,1025814,1025816,1025818,1025820,1025822,1025824,1025826,1025828,1025901,1025902,1025903,1025904,1025906,1025908,1025909,1025910,1025911,1025912,1025914,1025916,1025918,1025920,1025922,1025924,1025926,1025928,10251001,10251002,10251003,10251004,10251006,10251008,10251009,10251010,10251011,10251012,10251014,10251016,10251018,10251020,10251022,10251024,10251026,10251028,10251101,10251102,10251103,10251104,10251106,10251108,10251109,10251110,10251111,10251112,10251114,10251116,10251118,10251120,10251122,10251124,10251126,10251128,10251201,10251202,10251203,10251204,10251206,10251208,10251209,10251210,10251211,10251212,10251214,10251216,10251218,10251220,10251222,10251224,10251226,10251228,10251301,10251302,10251303,10251304,10251306,10251308,10251309,10251310,10251311,10251312,10251314,10251316,10251318,10251320,10251322,10251324,10251326,10251328,10251401,10251402,10251403,10251404,10251406,10251408,10251409,10251410,10251411,10251412,10251414,10251416,10251418,10251420,10251422,10251424,10251426,10251428,10251506,10251508,10251510,10251512,10251514,10251516,10251518,10251520,10251522,10251524,1026101,1026102,1026104,1026106,1026107,1026109,1026112,1026114,1026116,1026118,1026201,1026202,1026204,1026206,1026207,1026208,1026209,1026210,1026212,1026214,1026216,1026218,1026220,1026222,1026224,1026226,1026301,1026302,1026304,1026306,1026307,1026308,1026309,1026310,1026312,1026314,1026316,1026318,1026320,1026322,1026324,1026326,1026401,1026402,1026404,1026406,1026407,1026408,1026409,1026410,1026412,1026414,1026416,1026418,1026420,1026422,1026424,1026426,1026501,1026502,1026504,1026506,1026507,1026508,1026509,1026510,1026512,1026514,1026516,1026518,1026520,1026522,1026524,1026526,1026601,1026602,1026604,1026606,1026607,1026608,1026609,1026610,1026612,1026614,1026616,1026618,1026620,1026622,1026624,1026626,1026701,1026702,1026704,1026706,1026707,1026708,1026709,1026710,1026712,1026714,1026716,1026718,1026720,1026722,1026724,1026726,1026801,1026802,1026804,1026806,1026807,1026808,1026809,1026810,1026812,1026814,1026816,1026818,1026820,1026822,1026824,1026826,1026901,1026902,1026904,1026906,1026907,1026908,1026909,1026910,1026912,1026914,1026916,1026918,1026920,1026922,1026924,1026926,10261001,10261002,10261004,10261006,10261007,10261008,10261009,10261010,10261012,10261014,10261016,10261018,10261020,10261022,10261024,10261026,10261101,10261102,10261104,10261106,10261107,10261108,10261109,10261110,10261112,10261114,10261116,10261118,10261120,10261122,10261124,10261126,10261201,10261202,10261204,10261206,10261207,10261208,10261209,10261210,10261212,10261214,10261216,10261218,10261220,10261222,10261224,10261226,10261301,10261302,10261304,10261306,10261307,10261308,10261309,10261310,10261312,10261314,10261316,10261318,10261320,10261322,10261324,10261326,10261401,10261402,10261404,10261406,10261407,10261408,10261409,10261410,10261412,10261414,10261416,10261418,10261420,10261422,10261424,10261426,10261501,10261502,10261504,10261506,10261507,10261508,10261509,10261510,10261512,10261514,10261516,10261518,10261520,10261522,10261524,10261526,10261606,10261608,10261610,10261612,10261614,10261616,10261618,10261620,10261622] \ No newline at end of file diff --git a/assets/user_agent.json b/assets/user_agent.json deleted file mode 100644 index bb660cde7..000000000 --- a/assets/user_agent.json +++ /dev/null @@ -1,99 +0,0 @@ -[ - "Mozilla/5.0 (iPhone; CPU iPhone OS 14_0_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148", - "Mozilla/5.0 (iPhone; CPU iPhone OS 14_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148", - "Mozilla/5.0 (iPhone; CPU iPhone OS 15_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148", - "Mozilla/5.0 (iPhone; CPU iPhone OS 15_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.30(0x18001e30) NetType/WIFI Language/zh_CN", - "Mozilla/5.0 (Linux; Android 10; ANG-AN00 Build/HUAWEIANG-AN00; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/88.0.4324.93 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 10; BMH-AN20 Build/HUAWEIBMH-AN20; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/88.0.4324.93 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 10; CDY-AN20 Build/HUAWEICDY-AN20; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/88.0.4324.93 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 10; CLT-AL00 Build/HUAWEICLT-AL00; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/83.0.4103.106 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 10; CLT-AL00 Build/HUAWEICLT-AL00; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/88.0.4324.93 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 10; ELE-AL00 Build/HUAWEIELE-AL00; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/81.0.4044.117 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 10; ELE-AL00 Build/HUAWEIELE-AL00; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/88.0.4324.93 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 10; GLK-AL00 Build/HUAWEIGLK-AL00; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/88.0.4324.93 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 10; GM1910 Build/QKQ1.190716.003; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/85.0.4183.101 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 10; HLK-AL00 Build/HONORHLK-AL00; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/86.0.4240.99 XWEB/4343 MMWEBSDK/20221012 Mobile Safari/537.36 MMWEBID/1941 MicroMessenger/8.0.30.2260(0x28001E55) WeChat/arm64 Weixin NetType/WIFI Language/zh_CN ABI/arm64 MiniProgramEnv/android", - "Mozilla/5.0 (Linux; Android 10; HMA-AL00 Build/HUAWEIHMA-AL00; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/83.0.4103.106 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 10; M2004J19C Build/QP1A.190711.020; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/87.0.4280.101 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 10; MAR-LX2 Build/HUAWEIMAR-L22B; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/80.0.3987.99 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 10; Mi9 Pro 5G Build/QKQ1.190825.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/80.0.3987.99 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 10; OXF-AN00 Build/HUAWEIOXF-AN00; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/88.0.4324.93 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 10; OXF-AN10 Build/HUAWEIOXF-AN10; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/88.0.4324.93 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 10; PBEM00 Build/QKQ1.190918.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/77.0.3865.92 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 10; POT-AL00a Build/HUAWEIPOT-AL00a; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/80.0.3987.99 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 10; PPA-AL20 Build/HUAWEIPPA-AL40; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/88.0.4324.93 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 10; Redmi Note 7 Pro Build/QKQ1.190915.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/87.0.4280.101 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 10; TNN-AN00 Build/HUAWEITNN-AN00; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/88.0.4324.93 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 10; V1838A Build/QP1A.190711.020; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/83.0.4103.106 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 10; V1938CT Build/QP1A.190711.020; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/86.0.4240.99 XWEB/4365 MMWEBSDK/20221012 Mobile Safari/537.36 MMWEBID/986 MicroMessenger/8.0.30.2260(0x28001E55) WeChat/arm64 Weixin NetType/5G Language/zh_CN ABI/arm64 MiniProgramEnv/android", - "Mozilla/5.0 (Linux; Android 10; V1963A Build/QP1A.190711.020; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/83.0.4103.106 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 10; V2031A Build/QP1A.190711.020; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/83.0.4103.106 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 10; VCE-AL00 Build/HUAWEIVCE-AL00; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/81.0.4044.138 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 10; WLZ-AL10 Build/HUAWEIWLZ-AL10; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/88.0.4324.93 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 10; YAL-AL00 Build/HUAWEIYAL-AL00; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/88.0.4324.93 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 10; YAL-AL50 Build/HUAWEIYAL-AL50; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/88.0.4324.93 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 11; GLA-AL00 Build/HUAWEIGLA-AL00; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/88.0.4324.93 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 11; IN2020 Build/RP1A.201005.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/87.0.4280.141 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 11; KB2000 Build/RP1A.201005.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/96.0.4664.104 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 11; Lenovo L79031 Build/RKQ1.201022.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/83.0.4103.106 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 11; M2007J22C Build/RP1A.200720.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/92.0.4515.131 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 11; M2011K2C Build/RKQ1.200928.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/87.0.4280.141 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 11; M2012K10C Build/RP1A.200720.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/86.0.4240.99 XWEB/4365 MMWEBSDK/20221012 Mobile Safari/537.36 MMWEBID/914 MicroMessenger/8.0.30.2260(0x28001E55) WeChat/arm64 Weixin NetType/5G Language/zh_CN ABI/arm64 MiniProgramEnv/android", - "Mozilla/5.0 (Linux; Android 11; M2012K10C Build/RP1A.200720.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/86.0.4240.99 XWEB/4365 MMWEBSDK/20221012 Mobile Safari/537.36 MMWEBID/914 MicroMessenger/8.0.30.2260(0x28001E55) WeChat/arm64 Weixin NetType/WIFI Language/zh_CN ABI/arm64 MiniProgramEnv/android", - "Mozilla/5.0 (Linux; Android 11; M2012K11AC Build/RKQ1.200826.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/104.0.5112.97 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 11; M2102J2SC Build/RKQ1.200826.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/87.0.4280.141 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 11; M2102J2SC Build/RKQ1.200826.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/90.0.4430.210 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 11; Mi 10 Build/RKQ1.200826.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/90.0.4430.210 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 11; PCAT00 Build/RKQ1.201217.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/83.0.4103.106 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 11; PCDM10 Build/RP1A.200720.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/83.0.4103.106 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 11; PDEM10 Build/RKQ1.200710.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/83.0.4103.106 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 11; PEAM00 Build/RP1A.200720.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/83.0.4103.106 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 11; RMX1991 Build/RKQ1.201112.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/83.0.4103.106 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 11; TFY-AN00 Build/HONORTFY-AN00; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/88.0.4324.93 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 11; V1936A Build/RP1A.200720.012; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/83.0.4103.106 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 11; V2048A Build/RP1A.200720.012; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/107.0.5304.141 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 11; V2059A Build/RP1A.200720.012; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/83.0.4103.106 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 11; V2068A Build/RP1A.200720.012; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/83.0.4103.106 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 11; V2069A Build/RP1A.200720.012; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/83.0.4103.106 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 11; V2080A Build/RP1A.200720.012; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/83.0.4103.106 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 11; V2134A Build/RP1A.200720.012; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/83.0.4103.106 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 11; V2157A Build/RP1A.200720.012; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/83.0.4103.106 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 11; V2162A Build/RP1A.200720.012; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/83.0.4103.106 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 11; V2163A Build/RP1A.200720.012; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/83.0.4103.106 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 11; VNE-AN00 Build/HONORVNE-AN00; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/83.0.4103.106 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 11; VNE-AN00 Build/HONORVNE-TN00; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/83.0.4103.106 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 12; 2106118C Build/SKQ1.211006.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/95.0.4638.74 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 12; 2107119DC Build/SKQ1.211006.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/96.0.4664.104 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 12; 2109119BC Build/SKQ1.211006.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/96.0.4664.104 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 12; 2109119BC Build/SKQ1.211006.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/99.0.4844.88 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 12; 2206123SC Build/SKQ1.220303.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/95.0.4638.74 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 12; DIO-AN00 Build/HONORDIO-AN00; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/92.0.4515.105 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 12; ELS-AN00 Build/HUAWEIELS-AN00; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/92.0.4515.105 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 12; ELS-AN10 Build/HUAWEIELS-AN10; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/92.0.4515.105 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 12; ELZ-AN10 Build/HONORELZ-AN10; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/92.0.4515.105 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 12; GM1910 Build/SKQ1.211113.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/97.0.4692.98 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 12; JLH-AN00 Build/HONORJLH-AN00; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/92.0.4515.105 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 12; LE2120 Build/RKQ1.211119.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/107.0.5304.105 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 12; M2007J17C Build/SKQ1.211006.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/96.0.4664.104 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 12; M2011K2C Build/SKQ1.211006.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/103.0.5060.129 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 12; M2011K2C Build/SKQ1.211006.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/105.0.5195.136 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 12; Mi 10 Build/SKQ1.211006.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/95.0.4638.74 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 12; Mi 10 Build/SKQ1.211006.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/96.0.4664.104 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 12; MT2110 Build/RKQ1.211119.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/105.0.5195.136 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 12; NOH-AN50 Build/HUAWEINOH-AN50; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/92.0.4515.105 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 12; NOP-AN00 Build/HUAWEINOP-AN00P; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/92.0.4515.105 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 12; PDEM10 Build/RKQ1.211103.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/97.0.4692.98 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 12; PDHM00 Build/RKQ1.211103.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/97.0.4692.98 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 12; PDRM00 Build/RKQ1.211103.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/97.0.4692.98 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 12; PEAM00 Build/SP1A.210812.016; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/97.0.4692.98 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 12; PEEM00 Build/RKQ1.211119.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/97.0.4692.98 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 12; PEHM00 Build/SKQ1.210216.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/97.0.4692.98 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 12; PEMM00 Build/SP1A.210812.016; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/97.0.4692.98 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 12; PEMM20 Build/SP1A.210812.016; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/97.0.4692.98 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 12; PEMT00 Build/SP1A.210812.016; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/97.0.4692.98 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 12; PENM00 Build/RKQ1.211103.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/97.0.4692.98 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 12; PERM00 Build/SP1A.210812.016; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/97.0.4692.98 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 12; PFEM10 Build/SKQ1.211019.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/97.0.4692.98 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 12; PFGM00 Build/SP1A.210812.016; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/106.0.5249.126 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 12; PFTM20 Build/SP1A.210812.016; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/97.0.4692.98 Mobile Safari/537.36" -] diff --git a/assets/webview/dark.js b/assets/webview/dark.js deleted file mode 100644 index 4fa14be96..000000000 --- a/assets/webview/dark.js +++ /dev/null @@ -1,4 +0,0 @@ -// https://cdn.jsdelivr.net/npm/darkmode-js@1/lib/darkmode-js.min.js -!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define("darkmode-js",[],t):"object"==typeof exports?exports["darkmode-js"]=t():e["darkmode-js"]=t()}("undefined"!=typeof self?self:this,(function(){return function(e){var t={};function n(o){if(t[o])return t[o].exports;var r=t[o]={i:o,l:!1,exports:{}};return e[o].call(r.exports,r,r.exports,n),r.l=!0,r.exports}return n.m=e,n.c=t,n.d=function(e,t,o){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:o})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(n.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var r in e)n.d(o,r,function(t){return e[t]}.bind(null,r));return o},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=0)}([function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var o=function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)if(Object.prototype.hasOwnProperty.call(e,n)){var o=Object.defineProperty&&Object.getOwnPropertyDescriptor?Object.getOwnPropertyDescriptor(e,n):{};o.get||o.set?Object.defineProperty(t,n,o):t[n]=e[n]}return t.default=e,t}(n(1));var r=o.default;t.default=r,o.IS_BROWSER&&function(e){e.Darkmode=o.default}(window),e.exports=t.default},function(e,t,n){"use strict";function o(e,t){for(var n=0;n + + + + + + 获取 小应生活 + + + + + + + +