├── .editorconfig
├── .gitattributes
├── .github
└── workflows
│ ├── build-android-release.yml
│ ├── build-ios.yml
│ ├── build.yml
│ └── windows.yml
├── .gitignore
├── .metadata
├── CODE_OF_CONDUCT.md
├── LICENSE
├── README.md
├── analysis_options.yaml
├── android
├── .gitignore
├── app
│ ├── build.gradle
│ ├── proguard-rules.pro
│ └── src
│ │ ├── debug
│ │ └── AndroidManifest.xml
│ │ ├── main
│ │ ├── AndroidManifest.xml
│ │ ├── kotlin
│ │ │ └── MainActivity.kt
│ │ └── res
│ │ │ ├── drawable-v21
│ │ │ └── launch_background.xml
│ │ │ ├── drawable
│ │ │ ├── launch_background.xml
│ │ │ └── splash.png
│ │ │ ├── mipmap
│ │ │ └── icon.png
│ │ │ ├── values-b+zh+CN
│ │ │ └── strings.xml
│ │ │ ├── values-b+zh+TW
│ │ │ └── strings.xml
│ │ │ ├── values-night
│ │ │ └── styles.xml
│ │ │ ├── values-v31
│ │ │ └── styles.xml
│ │ │ ├── values
│ │ │ ├── strings.xml
│ │ │ └── styles.xml
│ │ │ └── xml
│ │ │ └── network_security_config.xml
│ │ └── profile
│ │ └── AndroidManifest.xml
├── build.gradle
├── gradle.properties
├── gradle
│ └── wrapper
│ │ └── gradle-wrapper.properties
└── settings.gradle
├── assets
├── course
│ ├── art.png
│ ├── biological.png
│ ├── building.png
│ ├── business.png
│ ├── chemical.png
│ ├── circuit.png
│ ├── computer.png
│ ├── control.png
│ ├── curriculum.png
│ ├── design.png
│ ├── economic.png
│ ├── electricity.png
│ ├── engineering.png
│ ├── experiment.png
│ ├── generality.png
│ ├── geography.png
│ ├── history.png
│ ├── ideological.png
│ ├── internship.png
│ ├── language.png
│ ├── literature.png
│ ├── management.png
│ ├── mathematics.png
│ ├── mechanical.png
│ ├── music.png
│ ├── physical.png
│ ├── political .png
│ ├── practice.png
│ ├── principle.png
│ ├── reading.png
│ ├── running.png
│ ├── social.png
│ ├── sports.png
│ ├── statistical.png
│ ├── technology.png
│ └── training.png
├── fonts
│ └── ywb_iconfont.ttf
├── icon.png
├── icon.svg
└── l10n
│ └── zh-Hans.yaml
├── devtools_options.yaml
├── lib
├── agreements
│ ├── entity
│ │ └── agreements.dart
│ ├── i18n.dart
│ ├── page
│ │ └── privacy_policy.dart
│ ├── settings.dart
│ └── widget
│ │ └── agreements.dart
├── app.dart
├── backend
│ └── entity
│ │ └── user.dart
├── credentials
│ ├── entity
│ │ ├── credential.dart
│ │ ├── credential.g.dart
│ │ ├── login_status.dart
│ │ ├── login_status.g.dart
│ │ ├── user_type.dart
│ │ └── user_type.g.dart
│ ├── error.dart
│ ├── i18n.dart
│ ├── init.dart
│ ├── storage
│ │ └── credential.dart
│ └── utils.dart
├── design
│ ├── adaptive
│ │ ├── dialog.dart
│ │ ├── editor.dart
│ │ ├── foundation.dart
│ │ ├── menu.dart
│ │ ├── multiplatform.dart
│ │ └── swipe.dart
│ ├── animation
│ │ ├── animated.dart
│ │ ├── button.dart
│ │ ├── marquee.dart
│ │ ├── number.dart
│ │ └── progress.dart
│ ├── dash.dart
│ ├── entity
│ │ ├── dual_color.dart
│ │ └── dual_color.g.dart
│ └── widget
│ │ ├── app.dart
│ │ ├── card.dart
│ │ ├── common.dart
│ │ ├── duration_picker.dart
│ │ ├── entry_card.dart
│ │ ├── expansion_tile.dart
│ │ ├── grouped.dart
│ │ ├── list_tile.dart
│ │ ├── multi_select.dart
│ │ ├── navigation.dart
│ │ ├── tags.dart
│ │ ├── task_builder.dart
│ │ └── tooltip.dart
├── entity
│ ├── campus.dart
│ ├── campus.g.dart
│ ├── meta.dart
│ └── uuid.dart
├── files.dart
├── index.dart
├── init.dart
├── l10n
│ ├── app.dart
│ ├── common.dart
│ ├── extension.dart
│ ├── lang.dart
│ ├── time.dart
│ ├── tr.dart
│ └── yaml_assets_loader.dart
├── lifecycle.dart
├── login
│ ├── i18n.dart
│ ├── init.dart
│ ├── page
│ │ ├── captcha.dart
│ │ ├── index.dart
│ │ └── sheet.dart
│ ├── service
│ │ └── authserver.dart
│ ├── utils.dart
│ ├── widget
│ │ └── forgot_pwd.dart
│ └── x.dart
├── main.dart
├── network
│ └── dio.dart
├── r.dart
├── route.dart
├── school
│ ├── electricity
│ │ ├── card.dart
│ │ ├── entity
│ │ │ ├── balance.dart
│ │ │ ├── balance.g.dart
│ │ │ └── room.dart
│ │ ├── i18n.dart
│ │ ├── init.dart
│ │ ├── service
│ │ │ └── electricity.dart
│ │ ├── storage
│ │ │ └── electricity.dart
│ │ ├── widget
│ │ │ ├── card.dart
│ │ │ └── search.dart
│ │ └── x.dart
│ ├── entity
│ │ ├── icon.dart
│ │ ├── school.dart
│ │ ├── school.g.dart
│ │ └── timetable.dart
│ ├── event.dart
│ ├── exam_arrange
│ │ ├── card.dart
│ │ ├── entity
│ │ │ ├── exam.dart
│ │ │ └── exam.g.dart
│ │ ├── i18n.dart
│ │ ├── init.dart
│ │ ├── page
│ │ │ └── list.dart
│ │ ├── service
│ │ │ └── exam.dart
│ │ ├── storage
│ │ │ └── exam.dart
│ │ └── widget
│ │ │ └── exam.dart
│ ├── exam_result
│ │ ├── card.pg.dart
│ │ ├── card.ug.dart
│ │ ├── entity
│ │ │ ├── gpa.dart
│ │ │ ├── result.pg.dart
│ │ │ ├── result.pg.g.dart
│ │ │ ├── result.ug.dart
│ │ │ └── result.ug.g.dart
│ │ ├── i18n.dart
│ │ ├── init.dart
│ │ ├── page
│ │ │ ├── details.gpa.dart
│ │ │ ├── details.ug.dart
│ │ │ ├── gpa.dart
│ │ │ ├── result.pg.dart
│ │ │ └── result.ug.dart
│ │ ├── service
│ │ │ ├── result.pg.dart
│ │ │ └── result.ug.dart
│ │ ├── storage
│ │ │ ├── result.pg.dart
│ │ │ └── result.ug.dart
│ │ ├── utils.dart
│ │ ├── widget
│ │ │ ├── pg.dart
│ │ │ └── ug.dart
│ │ └── x.dart
│ ├── expense_records
│ │ ├── card.dart
│ │ ├── entity
│ │ │ ├── local.dart
│ │ │ ├── local.g.dart
│ │ │ ├── remote.dart
│ │ │ ├── remote.g.dart
│ │ │ └── statistics.dart
│ │ ├── i18n.dart
│ │ ├── init.dart
│ │ ├── page
│ │ │ ├── records.dart
│ │ │ └── statistics.dart
│ │ ├── service
│ │ │ └── fetch.dart
│ │ ├── storage
│ │ │ └── local.dart
│ │ ├── utils.dart
│ │ ├── widget
│ │ │ ├── balance.dart
│ │ │ ├── chart
│ │ │ │ ├── bar.dart
│ │ │ │ ├── delegate.dart
│ │ │ │ ├── header.dart
│ │ │ │ └── pie.dart
│ │ │ ├── group.dart
│ │ │ ├── selector.dart
│ │ │ └── transaction.dart
│ │ └── x.dart
│ ├── i18n.dart
│ ├── index.dart
│ ├── init.dart
│ ├── oa_announce
│ │ ├── entity
│ │ │ ├── announce.dart
│ │ │ ├── announce.g.dart
│ │ │ └── page.dart
│ │ ├── i18n.dart
│ │ ├── index.dart
│ │ ├── init.dart
│ │ ├── page
│ │ │ ├── details.dart
│ │ │ └── list.dart
│ │ ├── service
│ │ │ └── announce.dart
│ │ ├── storage
│ │ │ └── announce.dart
│ │ └── widget
│ │ │ ├── attachment.dart
│ │ │ └── tile.dart
│ ├── page
│ │ └── settings.dart
│ ├── settings.dart
│ ├── utils.dart
│ ├── widget
│ │ ├── campus.dart
│ │ ├── course.dart
│ │ └── semester.dart
│ └── ywb
│ │ ├── entity
│ │ ├── application.dart
│ │ ├── application.g.dart
│ │ ├── service.dart
│ │ └── service.g.dart
│ │ ├── i18n.dart
│ │ ├── index.dart
│ │ ├── init.dart
│ │ ├── page
│ │ ├── application.dart
│ │ ├── details.dart
│ │ └── service.dart
│ │ ├── service
│ │ ├── application.dart
│ │ └── service.dart
│ │ ├── storage
│ │ ├── application.dart
│ │ └── service.dart
│ │ └── widget
│ │ ├── application.dart
│ │ ├── detail.dart
│ │ └── service.dart
├── session
│ ├── freshman.dart
│ ├── pg_registration.dart
│ ├── sso.dart
│ ├── ug_registration.dart
│ └── ywb.dart
├── settings
│ ├── i18n.dart
│ ├── meta.dart
│ ├── page
│ │ ├── about.dart
│ │ ├── index.dart
│ │ ├── oa.dart
│ │ └── theme_color.dart
│ ├── settings.dart
│ └── widget
│ │ ├── credentials.dart
│ │ └── device.dart
├── storage
│ ├── hive
│ │ ├── adapter.dart
│ │ ├── builtin.dart
│ │ ├── cookie.dart
│ │ ├── init.dart
│ │ ├── table.dart
│ │ └── type_id.dart
│ └── prefs.dart
├── timetable
│ ├── entity
│ │ ├── course.dart
│ │ ├── course.g.dart
│ │ ├── display.dart
│ │ ├── issue.dart
│ │ ├── loc.dart
│ │ ├── loc.g.dart
│ │ ├── pos.dart
│ │ ├── pos.g.dart
│ │ ├── timetable.dart
│ │ ├── timetable.g.dart
│ │ ├── timetable_entity.dart
│ │ └── timetable_entity.g.dart
│ ├── events.dart
│ ├── i18n.dart
│ ├── init.dart
│ ├── p13n
│ │ ├── builtin.dart
│ │ ├── entity
│ │ │ ├── cell_style.dart
│ │ │ ├── cell_style.g.dart
│ │ │ ├── palette.dart
│ │ │ └── palette.g.dart
│ │ ├── page
│ │ │ └── cell_style.dart
│ │ └── widget
│ │ │ ├── style.dart
│ │ │ └── style.g.dart
│ ├── page
│ │ ├── edit
│ │ │ ├── course_editor.dart
│ │ │ └── editor.dart
│ │ ├── import.dart
│ │ ├── index.dart
│ │ ├── mine.dart
│ │ ├── preview.dart
│ │ ├── settings.dart
│ │ └── timetable.dart
│ ├── service
│ │ └── school.dart
│ ├── settings.dart
│ ├── storage
│ │ └── timetable.dart
│ ├── utils.dart
│ ├── utils
│ │ ├── export.dart
│ │ ├── freshman.dart
│ │ ├── import.dart
│ │ ├── parse.pg.dart
│ │ └── parse.ug.dart
│ └── widget
│ │ ├── course.dart
│ │ ├── focus.dart
│ │ ├── free.dart
│ │ ├── issue.dart
│ │ └── timetable
│ │ ├── board.dart
│ │ ├── course_sheet.dart
│ │ ├── daily.dart
│ │ ├── header.dart
│ │ └── weekly.dart
├── utils
│ ├── async_event.dart
│ ├── collection.dart
│ ├── color.dart
│ ├── date.dart
│ ├── dio.dart
│ ├── error.dart
│ ├── format.dart
│ ├── guard_launch.dart
│ ├── hive.dart
│ ├── iconfont.dart
│ ├── json.dart
│ ├── permission.dart
│ ├── riverpod.dart
│ ├── save.dart
│ ├── scroll_detector.dart
│ ├── state_notifier.dart
│ ├── strings.dart
│ └── tel.dart
└── widget
│ ├── captcha_box.dart
│ ├── html.dart
│ ├── image.dart
│ ├── inapp_webview
│ └── page.dart
│ ├── markdown.dart
│ ├── modal_image_view.dart
│ ├── not_found.dart
│ └── search.dart
├── pubspec.lock
└── pubspec.yaml
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.svg]
12 | insert_final_newline = false
13 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | text=auto
2 |
3 | *.json eol=lf
4 |
--------------------------------------------------------------------------------
/.github/workflows/build-android-release.yml:
--------------------------------------------------------------------------------
1 | name: Flutter Build Android Release
2 |
3 | on: workflow_dispatch
4 |
5 | jobs:
6 | build_android:
7 | runs-on: ubuntu-latest
8 | if: github.ref == 'refs/heads/master'
9 |
10 | steps:
11 | - uses: actions/checkout@v4
12 | with:
13 | repository: plum-tech/xiaoying-x
14 | token: ${{ secrets.MIMIR_GITHUB_TOKEN }}
15 | fetch-depth: 0
16 |
17 | - name: Install pnpm
18 | uses: pnpm/action-setup@v4
19 | with:
20 | version: 9
21 |
22 | - uses: actions/setup-node@v4
23 | with:
24 | node-version: 22
25 | cache: 'pnpm'
26 | cache-dependency-path: tools/pnpm-lock.yaml
27 |
28 | - name: Pnpm install
29 | run: |
30 | cd tools && pnpm i && pnpm build && cd ..
31 |
32 | - name: Install JDK 17
33 | uses: actions/setup-java@v4
34 | with:
35 | distribution: 'temurin'
36 | java-version: '17'
37 |
38 | - name: Install Flutter
39 | uses: subosito/flutter-action@v2
40 | with:
41 | channel: stable
42 | cache: true
43 | flutter-version-file: pubspec.yaml
44 |
45 | - name: Build APK
46 | run: |
47 | flutter config --no-cli-animations
48 | flutter build apk --release --target-platform android-arm,android-arm64 --split-per-abi
49 |
50 | - name: Sign APK
51 | uses: r0adkll/sign-android-release@v1
52 | id: sign_apk
53 | with:
54 | releaseDirectory: build/app/outputs/flutter-apk
55 | signingKeyBase64: ${{ secrets.APK_SIGN_JKS_BASE64 }}
56 | keyStorePassword: ${{ secrets.APK_SIGN_JKS_PASSWORD }}
57 | keyPassword: ${{ secrets.APK_SIGN_ALIAS_PASS }}
58 | alias: ${{ secrets.APK_SIGN_ALIAS }}
59 | env:
60 | BUILD_TOOLS_VERSION: "34.0.0"
61 |
62 | - name: Publish Android Artifact
63 | uses: actions/upload-artifact@v4
64 | with:
65 | name: Android-release
66 | path: build/app/outputs/flutter-apk/*-signed.apk
67 |
--------------------------------------------------------------------------------
/.github/workflows/windows.yml:
--------------------------------------------------------------------------------
1 | name: Windows
2 | on: workflow_dispatch
3 |
4 | env:
5 | flutter_version: '3.24.4'
6 |
7 | jobs:
8 | build:
9 | runs-on: windows-latest
10 | steps:
11 | - uses: actions/checkout@v4
12 | with:
13 | repository: plum-tech/xiaoying-x
14 | token: ${{ secrets.MIMIR_GITHUB_TOKEN }}
15 | fetch-depth: 0
16 |
17 | - name: Install Flutter
18 | uses: subosito/flutter-action@v2
19 | with:
20 | flutter-version: ${{ env.flutter_version }}
21 | channel: stable
22 | cache: true
23 |
24 | - name: Setup Flutter
25 | run: |
26 | flutter config --no-cli-animations
27 |
28 | - name: Build Windows
29 | run: |
30 | flutter pub run build_runner build --delete-conflicting-outputs
31 | dart run msix:create
32 |
33 | - name: Upload building
34 | uses: actions/upload-artifact@v4
35 | with:
36 | name: SITLife-Windows-release
37 | path: build\windows\x64\runner\Release\SIT-Life.msix
38 |
--------------------------------------------------------------------------------
/.metadata:
--------------------------------------------------------------------------------
1 | # This file tracks properties of this Flutter project.
2 | # Used by Flutter tool to assess capabilities and perform upgrades etc.
3 | #
4 | # This file should be version controlled and should not be manually edited.
5 |
6 | version:
7 | revision: "5dcb86f68f239346676ceb1ed1ea385bd215fba1"
8 | channel: "stable"
9 |
10 | project_type: app
11 |
12 | # Tracks metadata for the flutter migrate command
13 | migration:
14 | platforms:
15 | - platform: root
16 | create_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1
17 | base_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1
18 | - platform: web
19 | create_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1
20 | base_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1
21 |
22 | # User provided section
23 |
24 | # List of Local paths (relative to this file) that should be
25 | # ignored by the migrate tool.
26 | #
27 | # Files that are not part of the templates will be ignored by default.
28 | unmanaged_files:
29 | - 'lib/main.dart'
30 | - 'ios/Runner.xcodeproj/project.pbxproj'
31 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Xiao Ying 小应生活
4 |
5 |

6 |
7 | ### 校园生活就用小应生活!
8 |
9 | ### Source License
10 |
11 | The source codes and configurations are licensed under [Plum Technology Ltd. Software License Agreement](LICENSE).
12 |
13 | ### Copyright
14 |
15 | Copyright©️2024-2025 [Plum Technology Ltd](https://www.liplum.net). All Rights Reserved.
16 |
17 |
18 |
--------------------------------------------------------------------------------
/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | # This file configures the analyzer, which statically analyzes Dart code to
2 | # check for errors, warnings, and lints.
3 | #
4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled
5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
6 | # invoked from the command line by running `flutter analyze`.
7 |
8 | # The following line activates a set of recommended lints for Flutter apps,
9 | # packages, and plugins designed to encourage good coding practices.
10 | include: package:flutter_lints/flutter.yaml
11 |
12 | analyzer:
13 | exclude:
14 | - lib/**/*.g.dart
15 |
16 |
17 | linter:
18 | # The lint rules applied to this project can be customized in the
19 | # section below to disable rules from the `package:flutter_lints/flutter.yaml`
20 | # included above or to enable additional rules. A list of all available lints
21 | # and their documentation is published at
22 | # https://dart-lang.github.io/linter/lints/index.html.
23 | #
24 | # Instead of disabling a lint rule for the entire project in the
25 | # section below, it can also be suppressed for a single line of code
26 | # or a specific dart file by using the `// ignore: name_of_lint` and
27 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file
28 | # producing the lint.
29 | rules:
30 | # avoid_print: false # Uncomment to disable the `avoid_print` rule
31 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
32 |
33 | # Additional information about this file can be found at
34 | # https://dart.dev/guides/language/analysis-options
35 |
--------------------------------------------------------------------------------
/android/.gitignore:
--------------------------------------------------------------------------------
1 | gradle-wrapper.jar
2 | /.gradle
3 | /captures/
4 | /gradlew
5 | /gradlew.bat
6 | /local.properties
7 | GeneratedPluginRegistrant.java
8 |
9 | # Remember to never publicly share your keystore.
10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
11 | key.properties
12 | **/*.keystore
13 | **/*.jks
14 |
--------------------------------------------------------------------------------
/android/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Flutter
2 | # fix by https://github.com/juliansteenbakker/mobile_scanner/issues/614#issuecomment-1665473831
3 | -keep public class androidx.camera.core.impl.CameraCaptureMetaData$** { *; }
4 |
--------------------------------------------------------------------------------
/android/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/android/app/src/main/kotlin/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package net.liplum.mimir
2 |
3 | import io.flutter.embedding.android.FlutterActivity
4 |
5 | class MainActivity: FlutterActivity() {
6 | }
7 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-v21/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | -
5 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | -
5 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plum-tech/xiaoying/0bede0a9e177195ea8c6d1a37daad8be5f426826/android/app/src/main/res/drawable/splash.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plum-tech/xiaoying/0bede0a9e177195ea8c6d1a37daad8be5f426826/android/app/src/main/res/mipmap/icon.png
--------------------------------------------------------------------------------
/android/app/src/main/res/values-b+zh+CN/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 小应生活
4 |
5 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values-b+zh+TW/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 小鷹生活
4 |
5 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values-night/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values-v31/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | SIT Life
4 |
5 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
17 |
21 |
22 |
--------------------------------------------------------------------------------
/android/app/src/main/res/xml/network_security_config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/android/app/src/profile/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | allprojects {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | }
6 | }
7 |
8 | allprojects {
9 | // fix https://github.com/jonataslaw/VideoCompress/issues/255
10 | afterEvaluate { project ->
11 | if (project.hasProperty("kotlin")) {
12 | project.tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
13 | kotlinOptions {
14 | jvmTarget = "1.8"
15 | }
16 | }
17 | }
18 | }
19 | }
20 | allprojects {
21 | // fix for verifyReleaseResources: https://github.com/isar/isar/issues/1654#issuecomment-2295028039
22 | afterEvaluate { project ->
23 | if (project.plugins.hasPlugin("com.android.application") ||
24 | project.plugins.hasPlugin("com.android.library")) {
25 | project.android {
26 | compileSdkVersion 35
27 | buildToolsVersion "35.0.0"
28 | }
29 | }
30 | }
31 | }
32 |
33 | allprojects {
34 | afterEvaluate { project ->
35 | // check only for "com.android.library" to not modify
36 | // your "app" subproject. All plugins will have "com.android.library" plugin, and only your app "com.android.application"
37 | // Change your application's namespace in main build.gradle and in main android block.
38 |
39 | if (project.hasProperty("android")) {
40 | project.android {
41 | if (namespace == null) {
42 | namespace project.group
43 | }
44 | }
45 | }
46 | }
47 | }
48 |
49 | rootProject.buildDir = '../build'
50 | subprojects {
51 | project.buildDir = "${rootProject.buildDir}/${project.name}"
52 | }
53 | subprojects {
54 | project.evaluationDependsOn(':app')
55 | }
56 |
57 | tasks.register("clean", Delete) {
58 | delete rootProject.buildDir
59 | }
60 |
61 | configurations.all {
62 | resolutionStrategy {
63 | force 'androidx.core:core:1.6.0'
64 | force 'androidx.core:core-ktx:1.6.0'
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/android/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx4096M --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
2 | android.useAndroidX=true
3 | android.enableJetifier=true
4 | android.jetifier.ignorelist=bcprov-jdk15on
5 |
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | zipStoreBase=GRADLE_USER_HOME
4 | zipStorePath=wrapper/dists
5 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
6 |
--------------------------------------------------------------------------------
/android/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | def flutterSdkPath = {
3 | def properties = new Properties()
4 | file("local.properties").withInputStream { properties.load(it) }
5 | def flutterSdkPath = properties.getProperty("flutter.sdk")
6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
7 | return flutterSdkPath
8 | }
9 | settings.ext.flutterSdkPath = flutterSdkPath()
10 |
11 | includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")
12 |
13 | repositories {
14 | google()
15 | mavenCentral()
16 | gradlePluginPortal()
17 | }
18 | }
19 |
20 | plugins {
21 | id "dev.flutter.flutter-plugin-loader" version "1.0.0"
22 | id "com.android.application" version "8.7.0" apply false
23 | id "org.jetbrains.kotlin.android" version "1.8.10" apply false
24 | }
25 |
26 | include ":app"
27 |
--------------------------------------------------------------------------------
/assets/course/art.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plum-tech/xiaoying/0bede0a9e177195ea8c6d1a37daad8be5f426826/assets/course/art.png
--------------------------------------------------------------------------------
/assets/course/biological.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plum-tech/xiaoying/0bede0a9e177195ea8c6d1a37daad8be5f426826/assets/course/biological.png
--------------------------------------------------------------------------------
/assets/course/building.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plum-tech/xiaoying/0bede0a9e177195ea8c6d1a37daad8be5f426826/assets/course/building.png
--------------------------------------------------------------------------------
/assets/course/business.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plum-tech/xiaoying/0bede0a9e177195ea8c6d1a37daad8be5f426826/assets/course/business.png
--------------------------------------------------------------------------------
/assets/course/chemical.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plum-tech/xiaoying/0bede0a9e177195ea8c6d1a37daad8be5f426826/assets/course/chemical.png
--------------------------------------------------------------------------------
/assets/course/circuit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plum-tech/xiaoying/0bede0a9e177195ea8c6d1a37daad8be5f426826/assets/course/circuit.png
--------------------------------------------------------------------------------
/assets/course/computer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plum-tech/xiaoying/0bede0a9e177195ea8c6d1a37daad8be5f426826/assets/course/computer.png
--------------------------------------------------------------------------------
/assets/course/control.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plum-tech/xiaoying/0bede0a9e177195ea8c6d1a37daad8be5f426826/assets/course/control.png
--------------------------------------------------------------------------------
/assets/course/curriculum.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plum-tech/xiaoying/0bede0a9e177195ea8c6d1a37daad8be5f426826/assets/course/curriculum.png
--------------------------------------------------------------------------------
/assets/course/design.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plum-tech/xiaoying/0bede0a9e177195ea8c6d1a37daad8be5f426826/assets/course/design.png
--------------------------------------------------------------------------------
/assets/course/economic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plum-tech/xiaoying/0bede0a9e177195ea8c6d1a37daad8be5f426826/assets/course/economic.png
--------------------------------------------------------------------------------
/assets/course/electricity.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plum-tech/xiaoying/0bede0a9e177195ea8c6d1a37daad8be5f426826/assets/course/electricity.png
--------------------------------------------------------------------------------
/assets/course/engineering.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plum-tech/xiaoying/0bede0a9e177195ea8c6d1a37daad8be5f426826/assets/course/engineering.png
--------------------------------------------------------------------------------
/assets/course/experiment.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plum-tech/xiaoying/0bede0a9e177195ea8c6d1a37daad8be5f426826/assets/course/experiment.png
--------------------------------------------------------------------------------
/assets/course/generality.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plum-tech/xiaoying/0bede0a9e177195ea8c6d1a37daad8be5f426826/assets/course/generality.png
--------------------------------------------------------------------------------
/assets/course/geography.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plum-tech/xiaoying/0bede0a9e177195ea8c6d1a37daad8be5f426826/assets/course/geography.png
--------------------------------------------------------------------------------
/assets/course/history.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plum-tech/xiaoying/0bede0a9e177195ea8c6d1a37daad8be5f426826/assets/course/history.png
--------------------------------------------------------------------------------
/assets/course/ideological.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plum-tech/xiaoying/0bede0a9e177195ea8c6d1a37daad8be5f426826/assets/course/ideological.png
--------------------------------------------------------------------------------
/assets/course/internship.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plum-tech/xiaoying/0bede0a9e177195ea8c6d1a37daad8be5f426826/assets/course/internship.png
--------------------------------------------------------------------------------
/assets/course/language.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plum-tech/xiaoying/0bede0a9e177195ea8c6d1a37daad8be5f426826/assets/course/language.png
--------------------------------------------------------------------------------
/assets/course/literature.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plum-tech/xiaoying/0bede0a9e177195ea8c6d1a37daad8be5f426826/assets/course/literature.png
--------------------------------------------------------------------------------
/assets/course/management.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plum-tech/xiaoying/0bede0a9e177195ea8c6d1a37daad8be5f426826/assets/course/management.png
--------------------------------------------------------------------------------
/assets/course/mathematics.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plum-tech/xiaoying/0bede0a9e177195ea8c6d1a37daad8be5f426826/assets/course/mathematics.png
--------------------------------------------------------------------------------
/assets/course/mechanical.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plum-tech/xiaoying/0bede0a9e177195ea8c6d1a37daad8be5f426826/assets/course/mechanical.png
--------------------------------------------------------------------------------
/assets/course/music.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plum-tech/xiaoying/0bede0a9e177195ea8c6d1a37daad8be5f426826/assets/course/music.png
--------------------------------------------------------------------------------
/assets/course/physical.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plum-tech/xiaoying/0bede0a9e177195ea8c6d1a37daad8be5f426826/assets/course/physical.png
--------------------------------------------------------------------------------
/assets/course/political .png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plum-tech/xiaoying/0bede0a9e177195ea8c6d1a37daad8be5f426826/assets/course/political .png
--------------------------------------------------------------------------------
/assets/course/practice.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plum-tech/xiaoying/0bede0a9e177195ea8c6d1a37daad8be5f426826/assets/course/practice.png
--------------------------------------------------------------------------------
/assets/course/principle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plum-tech/xiaoying/0bede0a9e177195ea8c6d1a37daad8be5f426826/assets/course/principle.png
--------------------------------------------------------------------------------
/assets/course/reading.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plum-tech/xiaoying/0bede0a9e177195ea8c6d1a37daad8be5f426826/assets/course/reading.png
--------------------------------------------------------------------------------
/assets/course/running.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plum-tech/xiaoying/0bede0a9e177195ea8c6d1a37daad8be5f426826/assets/course/running.png
--------------------------------------------------------------------------------
/assets/course/social.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plum-tech/xiaoying/0bede0a9e177195ea8c6d1a37daad8be5f426826/assets/course/social.png
--------------------------------------------------------------------------------
/assets/course/sports.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plum-tech/xiaoying/0bede0a9e177195ea8c6d1a37daad8be5f426826/assets/course/sports.png
--------------------------------------------------------------------------------
/assets/course/statistical.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plum-tech/xiaoying/0bede0a9e177195ea8c6d1a37daad8be5f426826/assets/course/statistical.png
--------------------------------------------------------------------------------
/assets/course/technology.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plum-tech/xiaoying/0bede0a9e177195ea8c6d1a37daad8be5f426826/assets/course/technology.png
--------------------------------------------------------------------------------
/assets/course/training.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plum-tech/xiaoying/0bede0a9e177195ea8c6d1a37daad8be5f426826/assets/course/training.png
--------------------------------------------------------------------------------
/assets/fonts/ywb_iconfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plum-tech/xiaoying/0bede0a9e177195ea8c6d1a37daad8be5f426826/assets/fonts/ywb_iconfont.ttf
--------------------------------------------------------------------------------
/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plum-tech/xiaoying/0bede0a9e177195ea8c6d1a37daad8be5f426826/assets/icon.png
--------------------------------------------------------------------------------
/assets/icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/devtools_options.yaml:
--------------------------------------------------------------------------------
1 | description: This file stores settings for Dart & Flutter DevTools.
2 | documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
3 | extensions:
4 |
--------------------------------------------------------------------------------
/lib/agreements/entity/agreements.dart:
--------------------------------------------------------------------------------
1 | import 'package:easy_localization/easy_localization.dart';
2 |
3 | enum AgreementType {
4 | basic,
5 | account;
6 |
7 | String l10n() => "agreements.basic".tr();
8 | }
9 |
10 | enum AgreementVersion {
11 | v20240915("20240915"),
12 | v20241118("20241118"),
13 | ;
14 |
15 | static const current = v20241118;
16 |
17 | final String number;
18 |
19 | const AgreementVersion(this.number);
20 | }
21 |
--------------------------------------------------------------------------------
/lib/agreements/i18n.dart:
--------------------------------------------------------------------------------
1 | import 'package:easy_localization/easy_localization.dart';
2 | import 'package:mimir/l10n/common.dart';
3 |
4 | const i18n = _I18n();
5 |
6 | class _I18n with CommonI18nMixin {
7 | static const ns = "agreements";
8 |
9 | const _I18n();
10 |
11 | String get acceptanceRequired => "$ns.acceptanceRequired.title".tr();
12 |
13 | String get acceptanceRequiredDesc => "$ns.acceptanceRequired.desc".tr();
14 |
15 | String get privacyPolicy => "$ns.privacyPolicy.title".tr();
16 |
17 | String get privacyPolicyContent => "$ns.privacyPolicy.content".tr();
18 |
19 | String get accept => "$ns.privacyPolicy.accept".tr();
20 |
21 | String get decline => "$ns.privacyPolicy.decline".tr();
22 |
23 | String get onDecline => "$ns.privacyPolicy.onDecline".tr();
24 | }
25 |
--------------------------------------------------------------------------------
/lib/agreements/settings.dart:
--------------------------------------------------------------------------------
1 | import 'package:hive/hive.dart';
2 | import 'package:mimir/utils/hive.dart';
3 |
4 | import 'entity/agreements.dart';
5 |
6 | class _AgreementsK {
7 | static const ns = "/agreements";
8 |
9 | static String acceptanceKeyOf(AgreementType type, AgreementVersion version) =>
10 | "$ns/acceptance/${type.name}/${version.number}";
11 | }
12 |
13 | class AgreementsSettings {
14 | final Box box;
15 |
16 | AgreementsSettings(this.box);
17 |
18 | bool? getBasicAcceptanceOf(AgreementVersion version) =>
19 | box.safeGet(_AgreementsK.acceptanceKeyOf(AgreementType.basic, version));
20 |
21 | Future setBasicAcceptanceOf(AgreementVersion version, bool? newV) async =>
22 | await box.safePut(_AgreementsK.acceptanceKeyOf(AgreementType.basic, version), newV);
23 |
24 | late final $basicAcceptanceOf = box.providerFamily(
25 | (version) => _AgreementsK.acceptanceKeyOf(AgreementType.basic, version),
26 | get: getBasicAcceptanceOf,
27 | set: setBasicAcceptanceOf,
28 | );
29 |
30 | bool? getAccountAcceptanceOf(AgreementVersion version) =>
31 | box.safeGet(_AgreementsK.acceptanceKeyOf(AgreementType.account, version));
32 |
33 | Future setAccountAcceptanceOf(AgreementVersion version, bool? newV) async =>
34 | await box.safePut(_AgreementsK.acceptanceKeyOf(AgreementType.account, version), newV);
35 |
36 | late final $accountAcceptanceOf = box.providerFamily(
37 | (version) => _AgreementsK.acceptanceKeyOf(AgreementType.account, version),
38 | get: getAccountAcceptanceOf,
39 | set: setAccountAcceptanceOf,
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/lib/agreements/widget/agreements.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_riverpod/flutter_riverpod.dart';
3 | import 'package:mimir/design/adaptive/dialog.dart';
4 | import 'package:mimir/settings/settings.dart';
5 | import 'package:mimir/widget/markdown.dart';
6 | import 'package:rettulf/rettulf.dart';
7 |
8 | import '../entity/agreements.dart';
9 | import '../i18n.dart';
10 |
11 | class AgreementsCheckBox extends ConsumerWidget {
12 | final AgreementType type;
13 |
14 | const AgreementsCheckBox({
15 | super.key,
16 | required this.type,
17 | });
18 |
19 | const AgreementsCheckBox.basic({
20 | super.key,
21 | }) : type = AgreementType.basic;
22 |
23 | const AgreementsCheckBox.account({
24 | super.key,
25 | }) : type = AgreementType.account;
26 |
27 | @override
28 | Widget build(BuildContext context, WidgetRef ref) {
29 | final agreements = Settings.agreements;
30 | final acceptance = switch (type) {
31 | AgreementType.basic => agreements.$basicAcceptanceOf,
32 | AgreementType.account => agreements.$accountAcceptanceOf,
33 | };
34 | final $accepted = acceptance(AgreementVersion.current);
35 | final accepted = ref.watch($accepted) ?? false;
36 | return [
37 | Checkbox.adaptive(
38 | value: accepted,
39 | onChanged: (newV) {
40 | ref.read($accepted.notifier).set(newV);
41 | },
42 | ),
43 | FeaturedMarkdownWidget(data: type.l10n()).expanded(flex: 9),
44 | ].row();
45 | }
46 | }
47 |
48 | Future showAgreementsRequired2Accept(BuildContext context) async {
49 | await context.showTip(
50 | title: i18n.acceptanceRequired,
51 | desc: i18n.acceptanceRequiredDesc,
52 | primary: i18n.ok,
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/lib/backend/entity/user.dart:
--------------------------------------------------------------------------------
1 | import 'package:easy_localization/easy_localization.dart';
2 | import 'package:json_annotation/json_annotation.dart';
3 |
4 | @JsonEnum()
5 | enum SchoolCode {
6 | sit("10259");
7 |
8 | final String code;
9 |
10 | const SchoolCode(this.code);
11 |
12 | String l10n() => "mimir.school.$code".tr();
13 |
14 | static final code2enum = Map.fromEntries(values.map((v) => MapEntry(v.code, v)));
15 | }
16 |
--------------------------------------------------------------------------------
/lib/credentials/entity/credential.dart:
--------------------------------------------------------------------------------
1 | import 'package:copy_with_extension/copy_with_extension.dart';
2 | import 'package:mimir/storage/hive/type_id.dart';
3 |
4 | part 'credential.g.dart';
5 |
6 | @HiveType(typeId: CoreHiveType.credentials)
7 | @CopyWith(skipFields: true)
8 | class Credential {
9 | @HiveField(0)
10 | final String account;
11 | @HiveField(1)
12 | final String password;
13 |
14 | const Credential({
15 | required this.account,
16 | required this.password,
17 | });
18 |
19 | @override
20 | String toString() => 'account:"$account", password:"$password"';
21 |
22 | @override
23 | bool operator ==(Object other) {
24 | return other is Credential &&
25 | runtimeType == other.runtimeType &&
26 | account == other.account &&
27 | password == other.password;
28 | }
29 |
30 | @override
31 | int get hashCode => Object.hash(account, password);
32 | }
33 |
--------------------------------------------------------------------------------
/lib/credentials/entity/login_status.dart:
--------------------------------------------------------------------------------
1 | import 'package:mimir/storage/hive/type_id.dart';
2 |
3 | part 'login_status.g.dart';
4 |
5 | @HiveType(typeId: CoreHiveType.oaLoginStatus)
6 | enum OaLoginStatus {
7 | @HiveField(0)
8 | never,
9 | @HiveField(2)
10 | offline,
11 | @HiveField(3)
12 | validated,
13 | @HiveField(4)
14 | everLogin,
15 | }
16 |
--------------------------------------------------------------------------------
/lib/credentials/entity/login_status.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'login_status.dart';
4 |
5 | // **************************************************************************
6 | // TypeAdapterGenerator
7 | // **************************************************************************
8 |
9 | class OaLoginStatusAdapter extends TypeAdapter {
10 | @override
11 | final int typeId = 5;
12 |
13 | @override
14 | OaLoginStatus read(BinaryReader reader) {
15 | switch (reader.readByte()) {
16 | case 0:
17 | return OaLoginStatus.never;
18 | case 2:
19 | return OaLoginStatus.offline;
20 | case 3:
21 | return OaLoginStatus.validated;
22 | case 4:
23 | return OaLoginStatus.everLogin;
24 | default:
25 | return OaLoginStatus.never;
26 | }
27 | }
28 |
29 | @override
30 | void write(BinaryWriter writer, OaLoginStatus obj) {
31 | switch (obj) {
32 | case OaLoginStatus.never:
33 | writer.writeByte(0);
34 | break;
35 | case OaLoginStatus.offline:
36 | writer.writeByte(2);
37 | break;
38 | case OaLoginStatus.validated:
39 | writer.writeByte(3);
40 | break;
41 | case OaLoginStatus.everLogin:
42 | writer.writeByte(4);
43 | break;
44 | }
45 | }
46 |
47 | @override
48 | int get hashCode => typeId.hashCode;
49 |
50 | @override
51 | bool operator ==(Object other) =>
52 | identical(this, other) ||
53 | other is OaLoginStatusAdapter && runtimeType == other.runtimeType && typeId == other.typeId;
54 | }
55 |
--------------------------------------------------------------------------------
/lib/credentials/entity/user_type.dart:
--------------------------------------------------------------------------------
1 | import 'package:easy_localization/easy_localization.dart';
2 | import 'package:mimir/storage/hive/type_id.dart';
3 |
4 | part 'user_type.g.dart';
5 |
6 | @HiveType(typeId: CoreHiveType.oaUserType)
7 | enum OaUserType {
8 | @HiveField(0)
9 | undergraduate(),
10 | @HiveField(1)
11 | postgraduate(),
12 | @HiveField(2)
13 | freshman(),
14 | @HiveField(3)
15 | worker(),
16 | @HiveField(4)
17 | none();
18 |
19 | const OaUserType();
20 |
21 | String l10n() => "OaUserType.$name".tr();
22 | }
23 |
--------------------------------------------------------------------------------
/lib/credentials/entity/user_type.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'user_type.dart';
4 |
5 | // **************************************************************************
6 | // TypeAdapterGenerator
7 | // **************************************************************************
8 |
9 | class OaUserTypeAdapter extends TypeAdapter {
10 | @override
11 | final int typeId = 6;
12 |
13 | @override
14 | OaUserType read(BinaryReader reader) {
15 | switch (reader.readByte()) {
16 | case 0:
17 | return OaUserType.undergraduate;
18 | case 1:
19 | return OaUserType.postgraduate;
20 | case 2:
21 | return OaUserType.freshman;
22 | case 3:
23 | return OaUserType.worker;
24 | case 4:
25 | return OaUserType.none;
26 | default:
27 | return OaUserType.undergraduate;
28 | }
29 | }
30 |
31 | @override
32 | void write(BinaryWriter writer, OaUserType obj) {
33 | switch (obj) {
34 | case OaUserType.undergraduate:
35 | writer.writeByte(0);
36 | break;
37 | case OaUserType.postgraduate:
38 | writer.writeByte(1);
39 | break;
40 | case OaUserType.freshman:
41 | writer.writeByte(2);
42 | break;
43 | case OaUserType.worker:
44 | writer.writeByte(3);
45 | break;
46 | case OaUserType.none:
47 | writer.writeByte(4);
48 | break;
49 | }
50 | }
51 |
52 | @override
53 | int get hashCode => typeId.hashCode;
54 |
55 | @override
56 | bool operator ==(Object other) =>
57 | identical(this, other) ||
58 | other is OaUserTypeAdapter && runtimeType == other.runtimeType && typeId == other.typeId;
59 | }
60 |
--------------------------------------------------------------------------------
/lib/credentials/error.dart:
--------------------------------------------------------------------------------
1 | import 'package:easy_localization/easy_localization.dart';
2 |
3 | class CredentialErrorType {
4 | final String type;
5 |
6 | const CredentialErrorType._(this.type);
7 |
8 | static const //
9 | accountPassword = CredentialErrorType._("accountPassword"),
10 | captcha = CredentialErrorType._("captcha"),
11 | frozen = CredentialErrorType._("frozen"),
12 | locked = CredentialErrorType._("locked");
13 | static const //
14 | oaFrozen = CredentialErrorType._("oa-frozen"),
15 | oaLocked = CredentialErrorType._("oa-locked"),
16 | oaIncompleteUserInfo = CredentialErrorType._("oa-incompleteUserInfo");
17 |
18 | String l10n() => "credentials.error.$type".tr();
19 |
20 | @override
21 | String toString() {
22 | return type;
23 | }
24 | }
25 |
26 | class CredentialException implements Exception {
27 | final CredentialErrorType type;
28 | final String? message;
29 |
30 | const CredentialException({
31 | required this.type,
32 | this.message,
33 | });
34 |
35 | @override
36 | String toString() {
37 | final message = this.message;
38 | if (message == null) return "CredentialsException";
39 | return "CredentialsException: $type $message";
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/lib/credentials/i18n.dart:
--------------------------------------------------------------------------------
1 | import 'package:easy_localization/easy_localization.dart';
2 |
3 | class CredentialsI18n with CredentialsI18nMixin {
4 | const CredentialsI18n();
5 | }
6 |
7 | mixin class CredentialsI18nMixin {
8 | static const ns = "credentials";
9 |
10 | String get account => "$ns.account".tr();
11 |
12 | String get pwd => "$ns.pwd".tr();
13 |
14 | String get savedPwd => "$ns.savedPwd.title".tr();
15 |
16 | String get savedPwdDesc => "$ns.savedPwd.desc".tr();
17 |
18 | String get credentialsError => "$ns.credentialsError".tr();
19 | }
20 |
21 | class OaCredentialsI18n extends CredentialsI18n {
22 | const OaCredentialsI18n();
23 |
24 | static const ns = "${CredentialsI18nMixin.ns}.oa";
25 |
26 | String get studentId => "$ns.studentId".tr();
27 |
28 | String get oaAccount => "$ns.oaAccount".tr();
29 |
30 | String get oaPwd => "$ns.oaPwd".tr();
31 |
32 | String get oaPwdChangedOutsideRequest => "$ns.oaPwdChangedOutsideRequest".tr();
33 |
34 | String get goSettings => "$ns.goSettings".tr();
35 | }
36 |
37 | class EmailCredentialsI18n extends CredentialsI18n {
38 | const EmailCredentialsI18n();
39 |
40 | static const ns = "${CredentialsI18nMixin.ns}.email";
41 |
42 | String get eduEmail => "$ns.eduEmail".tr();
43 |
44 | String get emailAddress => "$ns.emailAddress".tr();
45 | }
46 |
--------------------------------------------------------------------------------
/lib/credentials/init.dart:
--------------------------------------------------------------------------------
1 | import 'storage/credential.dart';
2 |
3 | class CredentialsInit {
4 | static late CredentialStorage storage;
5 |
6 | static void init() {}
7 |
8 | static void initStorage() {
9 | storage = CredentialStorage();
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/lib/credentials/storage/credential.dart:
--------------------------------------------------------------------------------
1 | import 'package:hive_flutter/hive_flutter.dart';
2 | import 'package:mimir/credentials/entity/user_type.dart';
3 | import 'package:mimir/storage/hive/init.dart';
4 | import 'package:mimir/utils/hive.dart';
5 |
6 | import '../entity/credential.dart';
7 | import '../entity/login_status.dart';
8 |
9 | class CredentialStorage {
10 | Box get box => HiveInit.credentials;
11 |
12 | CredentialStorage();
13 |
14 | late final oa = _Oa(box);
15 | }
16 |
17 | class _OaK {
18 | static const ns = "/oa";
19 | static const credentials = "$ns/credentials";
20 | static const lastAuthTime = "$ns/lastAuthTime";
21 | static const loginStatus = "$ns/loginStatus";
22 | static const userType = "$ns/userType";
23 | }
24 |
25 | class _Oa {
26 | final Box box;
27 |
28 | _Oa(this.box);
29 |
30 | Credential? get credentials => box.safeGet(_OaK.credentials);
31 |
32 | set credentials(Credential? newV) => box.safePut(_OaK.credentials, newV);
33 |
34 | late final $credentials = box.provider(_OaK.credentials);
35 |
36 | DateTime? get lastAuthTime => box.safeGet(_OaK.lastAuthTime);
37 |
38 | set lastAuthTime(DateTime? newV) => box.safePut(_OaK.lastAuthTime, newV);
39 |
40 | late final $lastAuthTime = box.provider(_OaK.lastAuthTime);
41 |
42 | OaLoginStatus? get loginStatus => box.safeGet(_OaK.loginStatus) ?? OaLoginStatus.never;
43 |
44 | set loginStatus(OaLoginStatus? newV) => box.safePut(_OaK.loginStatus, newV);
45 |
46 | late final $loginStatus = box.providerWithDefault(_OaK.loginStatus, () => OaLoginStatus.never);
47 |
48 | OaUserType get userType => box.safeGet(_OaK.userType) ?? OaUserType.none;
49 |
50 | set userType(OaUserType newV) => box.safePut(_OaK.userType, newV);
51 |
52 | late final $userType = box.providerWithDefault(_OaK.userType, () => OaUserType.none);
53 | }
54 |
--------------------------------------------------------------------------------
/lib/credentials/utils.dart:
--------------------------------------------------------------------------------
1 | import 'package:mimir/credentials/entity/user_type.dart';
2 |
3 | /// 本、专科生(10位学号)
4 | final _undergraduateId = RegExp(r'^(\d{6}[YGHE\d]\d{3})$');
5 |
6 | /// 研究生(9位学号)
7 | final _postgraduateId = RegExp(r'^(\d{2}6\d{6})$');
8 |
9 | /// 教师(4位工号)
10 | final _teacherId = RegExp(r'^(\d{4})$');
11 |
12 | /// 新生(14位纯数字高考报名号)
13 | final _freshmanId = RegExp(r'^(\d{14})$');
14 |
15 | /// [schoolId] can be a student ID or a work number.
16 | OaUserType? estimateOaUserType(String schoolId) {
17 | if (schoolId.length == 10 && _undergraduateId.hasMatch(schoolId.toUpperCase())) {
18 | return OaUserType.undergraduate;
19 | } else if (schoolId.length == 9 && _postgraduateId.hasMatch(schoolId)) {
20 | return OaUserType.postgraduate;
21 | } else if (schoolId.length == 4 && _teacherId.hasMatch(schoolId)) {
22 | return OaUserType.worker;
23 | } else if (schoolId.length == 14 && _freshmanId.hasMatch(schoolId)) {
24 | return OaUserType.freshman;
25 | }
26 | return null;
27 | }
28 |
--------------------------------------------------------------------------------
/lib/design/adaptive/swipe.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_swipe_action_cell/flutter_swipe_action_cell.dart' as w;
3 | import 'package:mimir/design/adaptive/foundation.dart';
4 | import 'package:rettulf/rettulf.dart';
5 |
6 | class SwipeAction {
7 | final VoidCallback action;
8 | final IconData? icon;
9 | final String? label;
10 | final bool fullSwipeAction;
11 | final bool destructive;
12 | final Color? color;
13 |
14 | const SwipeAction({
15 | required this.action,
16 | this.icon,
17 | this.label,
18 | this.fullSwipeAction = false,
19 | this.destructive = false,
20 | this.color = Colors.green,
21 | });
22 |
23 | const SwipeAction.delete({
24 | required this.action,
25 | this.icon,
26 | this.label,
27 | }) : destructive = true,
28 | fullSwipeAction = true,
29 | color = null;
30 |
31 | w.SwipeAction build(BuildContext context) {
32 | return w.SwipeAction(
33 | title: label,
34 | icon: Icon(icon, color: Colors.white),
35 | color: color ?? context.$red$,
36 | style: context.textTheme.titleSmall ?? const TextStyle(),
37 | performsFirstActionWithFullSwipe: fullSwipeAction,
38 | onTap: (w.CompletionHandler handler) async {
39 | await handler(destructive);
40 | action();
41 | },
42 | );
43 | }
44 | }
45 |
46 | class WithSwipeAction extends StatelessWidget {
47 | final Widget child;
48 | final SwipeAction? left;
49 | final SwipeAction? right;
50 | final Key childKey;
51 |
52 | const WithSwipeAction({
53 | super.key,
54 | required this.childKey,
55 | required this.child,
56 | this.left,
57 | this.right,
58 | });
59 |
60 | @override
61 | Widget build(BuildContext context) {
62 | final left = this.left;
63 | final right = this.right;
64 | return w.SwipeActionCell(
65 | key: childKey,
66 | backgroundColor: Colors.transparent,
67 | leadingActions: left == null
68 | ? null
69 | : [
70 | left.build(context),
71 | ],
72 | trailingActions: right == null
73 | ? null
74 | : [
75 | right.build(context),
76 | ],
77 | child: child,
78 | );
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/lib/design/animation/animated.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | extension AnimatedEx on Widget {
4 | Widget animatedSwitched({
5 | Duration duration = Durations.medium2,
6 | Curve? switchInCurve,
7 | Curve? switchOutCurve,
8 | }) =>
9 | AnimatedSwitcher(
10 | switchInCurve: switchInCurve ?? Curves.linear,
11 | switchOutCurve: switchOutCurve ?? Curves.linear,
12 | duration: duration,
13 | child: this,
14 | );
15 |
16 | Widget animatedSized({
17 | Duration duration = Durations.medium2,
18 | Alignment align = Alignment.center,
19 | Curve curve = Curves.fastEaseInToSlowEaseOut,
20 | }) =>
21 | AnimatedSize(
22 | curve: curve,
23 | duration: duration,
24 | alignment: align,
25 | child: this,
26 | );
27 | }
28 |
29 | class AnimatedSwitched extends StatelessWidget {
30 | final Duration duration;
31 | final Widget? child;
32 | final Curve switchInCurve;
33 | final Curve switchOutCurve;
34 |
35 | const AnimatedSwitched({
36 | super.key,
37 | this.duration = Durations.medium2,
38 | this.child,
39 | this.switchInCurve = Curves.linear,
40 | this.switchOutCurve = Curves.linear,
41 | });
42 |
43 | @override
44 | Widget build(BuildContext context) {
45 | return AnimatedSwitcher(
46 | duration: duration,
47 | child: child ?? const SizedBox(),
48 | );
49 | }
50 | }
51 |
52 | class AnimatedShowUp extends StatelessWidget {
53 | final Duration duration;
54 | final Curve switchInCurve;
55 | final Curve switchOutCurve;
56 | final bool when;
57 | final WidgetBuilder builder;
58 |
59 | const AnimatedShowUp({
60 | super.key,
61 | this.duration = Durations.medium2,
62 | required this.when,
63 | required this.builder,
64 | this.switchInCurve = Curves.linear,
65 | this.switchOutCurve = Curves.linear,
66 | });
67 |
68 | @override
69 | Widget build(BuildContext context) {
70 | return AnimatedSwitcher(
71 | duration: duration,
72 | child: when ? builder(context) : const SizedBox(),
73 | );
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/lib/design/animation/number.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | class AnimatedNumber extends StatelessWidget {
4 | final Duration duration;
5 | final num value;
6 | final Widget Function(BuildContext context, double value) builder;
7 |
8 | const AnimatedNumber({
9 | super.key,
10 | required this.value,
11 | this.builder = _kBuild,
12 | this.duration = Durations.medium3,
13 | });
14 |
15 | static Widget _kBuild(BuildContext context, double value) {
16 | return Text("$value");
17 | }
18 |
19 | @override
20 | Widget build(BuildContext context) {
21 | return TweenAnimationBuilder(
22 | tween: Tween(begin: value, end: value),
23 | duration: duration,
24 | builder: (ctx, value, _) => builder(ctx, value.toDouble()),
25 | );
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/lib/design/widget/app.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:rettulf/rettulf.dart';
3 |
4 | class AppCard extends StatelessWidget {
5 | /// [SizedBox] by default.
6 | final Widget? view;
7 | final Widget? leading;
8 | final Widget? title;
9 | final Widget? subtitle;
10 | final Widget? trailing;
11 | final List? leftActions;
12 |
13 | /// 12 by default.
14 | final double? leftActionsSpacing;
15 | final List? rightActions;
16 |
17 | /// 0 by default
18 | final double? rightActionsSpacing;
19 |
20 | const AppCard({
21 | super.key,
22 | this.view,
23 | this.leading,
24 | this.title,
25 | this.subtitle,
26 | this.trailing,
27 | this.leftActions,
28 | this.rightActions,
29 | this.leftActionsSpacing,
30 | this.rightActionsSpacing,
31 | });
32 |
33 | @override
34 | Widget build(BuildContext context) {
35 | final leftActions = this.leftActions ?? const [];
36 | final rightActions = this.rightActions ?? const [];
37 | final textTheme = context.textTheme;
38 | return Card.outlined(
39 | clipBehavior: Clip.hardEdge,
40 | child: [
41 | AnimatedSize(
42 | duration: Durations.long2,
43 | alignment: Alignment.topCenter,
44 | curve: Curves.fastEaseInToSlowEaseOut,
45 | child: view ?? const SizedBox.shrink(),
46 | ).align(at: Alignment.center),
47 | ListTile(
48 | leading: leading,
49 | titleTextStyle: textTheme.titleLarge,
50 | title: title,
51 | subtitleTextStyle: textTheme.bodyLarge?.copyWith(color: context.colorScheme.onSurfaceVariant),
52 | subtitle: subtitle,
53 | trailing: trailing,
54 | ),
55 | OverflowBar(
56 | alignment: MainAxisAlignment.spaceBetween,
57 | children: [
58 | leftActions.wrap(spacing: leftActionsSpacing ?? 8),
59 | rightActions.wrap(spacing: rightActionsSpacing ?? 0),
60 | ],
61 | ).padOnly(l: 16, b: rightActions.isEmpty ? 12 : 8, r: 16),
62 | ].column(),
63 | );
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/lib/design/widget/card.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | enum CardVariant {
4 | elevated,
5 | filled,
6 | outlined;
7 | }
8 |
9 | extension WidgetCardX on Widget {
10 | Widget inAnyCard({
11 | Clip? clip,
12 | CardVariant type = CardVariant.elevated,
13 | EdgeInsetsGeometry? margin,
14 | Color? color,
15 | }) {
16 | return switch (type) {
17 | CardVariant.elevated => Card(
18 | clipBehavior: clip,
19 | color: color,
20 | margin: margin,
21 | child: this,
22 | ),
23 | CardVariant.filled => Card.filled(
24 | clipBehavior: clip,
25 | color: color,
26 | margin: margin,
27 | child: this,
28 | ),
29 | CardVariant.outlined => Card.outlined(
30 | clipBehavior: clip,
31 | color: color,
32 | margin: margin,
33 | child: this,
34 | ),
35 | };
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/lib/design/widget/common.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_svg/svg.dart';
3 | import 'package:rettulf/rettulf.dart';
4 |
5 | class LeavingBlank extends StatelessWidget {
6 | final WidgetBuilder iconBuilder;
7 | final String? desc;
8 | final Widget? subtitle;
9 | final Widget? action;
10 |
11 | const LeavingBlank.builder({
12 | super.key,
13 | required this.iconBuilder,
14 | required this.desc,
15 | this.subtitle,
16 | this.action,
17 | });
18 |
19 | factory LeavingBlank({
20 | Key? key,
21 | required IconData icon,
22 | String? desc,
23 | double size = 120,
24 | Widget? subtitle,
25 | Widget? action,
26 | }) {
27 | return LeavingBlank.builder(
28 | iconBuilder: (ctx) => icon.make(size: size, color: ctx.colorScheme.primary),
29 | desc: desc,
30 | subtitle: subtitle,
31 | action: action,
32 | );
33 | }
34 |
35 | factory LeavingBlank.svgAssets({
36 | Key? key,
37 | required String assetName,
38 | String? desc,
39 | VoidCallback? onIconTap,
40 | double width = 120,
41 | double height = 120,
42 | Widget? subtitle,
43 | Widget? action,
44 | }) {
45 | return LeavingBlank.builder(
46 | iconBuilder: (ctx) => SvgPicture.asset(assetName, width: width, height: height),
47 | desc: desc,
48 | subtitle: subtitle,
49 | action: action,
50 | );
51 | }
52 |
53 | @override
54 | Widget build(BuildContext context) {
55 | final desc = this.desc;
56 | final action = this.action;
57 | final subtitle = this.subtitle;
58 | Widget icon = iconBuilder(context).padAll(20);
59 | return [
60 | icon,
61 | if (desc != null) buildDesc(context, desc),
62 | if (subtitle != null) subtitle,
63 | if (action != null) action,
64 | ].column(maa: MainAxisAlignment.spaceAround, mas: MainAxisSize.min).center();
65 | }
66 |
67 | Widget buildDesc(BuildContext ctx, String desc) {
68 | return desc
69 | .text(
70 | style: ctx.textTheme.titleLarge,
71 | textAlign: TextAlign.center,
72 | )
73 | .center()
74 | .padAll(10);
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/lib/design/widget/list_tile.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter/services.dart';
3 | import 'package:rettulf/rettulf.dart';
4 | import 'package:mimir/design/adaptive/dialog.dart';
5 | import 'package:mimir/l10n/common.dart';
6 |
7 | class DetailListTile extends StatelessWidget {
8 | final String? title;
9 | final String? subtitle;
10 | final Widget? leading;
11 | final Widget? trailing;
12 | final bool copyable;
13 | final bool enabled;
14 | final VoidCallback? onTap;
15 |
16 | const DetailListTile({
17 | super.key,
18 | this.title,
19 | this.subtitle,
20 | this.copyable = true,
21 | this.leading,
22 | this.trailing,
23 | this.enabled = true,
24 | this.onTap,
25 | });
26 |
27 | @override
28 | Widget build(BuildContext context) {
29 | final subtitle = this.subtitle;
30 | return ListTile(
31 | leading: leading,
32 | trailing: trailing,
33 | title: title?.text(),
34 | subtitle: subtitle?.text(),
35 | visualDensity: VisualDensity.compact,
36 | enabled: enabled,
37 | onTap: onTap,
38 | onLongPress: copyable && subtitle != null
39 | ? () async {
40 | final title = this.title;
41 | if (title != null) {
42 | context.showSnackBar(content: const CommonI18n().copyTipOf(title).text());
43 | }
44 | await Clipboard.setData(ClipboardData(text: subtitle));
45 | }
46 | : null,
47 | );
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/lib/design/widget/navigation.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:go_router/go_router.dart';
3 | import 'package:mimir/design/adaptive/multiplatform.dart';
4 |
5 | class PageNavigationTile extends StatelessWidget {
6 | final Widget? title;
7 | final Widget? subtitle;
8 | final Widget? leading;
9 | final String path;
10 |
11 | const PageNavigationTile({
12 | super.key,
13 | this.title,
14 | this.subtitle,
15 | this.leading,
16 | required this.path,
17 | });
18 |
19 | @override
20 | Widget build(BuildContext context) {
21 | return ListTile(
22 | title: title,
23 | subtitle: subtitle,
24 | leading: leading,
25 | trailing: Icon(context.icons.rightChevron),
26 | onTap: () {
27 | context.push(path);
28 | },
29 | );
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/lib/design/widget/tags.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:rettulf/rettulf.dart';
3 |
4 | class TagsGroup extends StatelessWidget {
5 | final List tags;
6 |
7 | const TagsGroup(
8 | this.tags, {
9 | super.key,
10 | });
11 |
12 | @override
13 | Widget build(BuildContext context) {
14 | final textTheme = context.textTheme;
15 | return tags
16 | .map(
17 | (tag) => Chip(
18 | label: tag.text(),
19 | padding: EdgeInsets.zero,
20 | labelStyle: textTheme.bodySmall,
21 | elevation: 4,
22 | ),
23 | )
24 | .toList()
25 | .wrap(spacing: 4);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/lib/design/widget/task_builder.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/widgets.dart';
2 |
3 | typedef Task = Future Function();
4 |
5 | class TaskBuilder extends StatefulWidget {
6 | final Task? task;
7 | final void Function(dynamic error, StackTrace stackTrace)? onError;
8 | final Widget Function(BuildContext context, Task? task, bool? running) builder;
9 |
10 | const TaskBuilder({
11 | super.key,
12 | this.task,
13 | required this.builder,
14 | this.onError,
15 | });
16 |
17 | @override
18 | State createState() => _TaskBuilderState();
19 | }
20 |
21 | class _TaskBuilderState extends State {
22 | var running = false;
23 |
24 | @override
25 | Widget build(BuildContext context) {
26 | final task = widget.task;
27 | return widget.builder(
28 | context,
29 | task == null || running
30 | ? null
31 | : () async {
32 | setState(() {
33 | running = true;
34 | });
35 | try {
36 | await task();
37 | } catch (error, stackTrace) {
38 | widget.onError?.call(error, stackTrace);
39 | } finally {
40 | if (context.mounted) {
41 | setState(() {
42 | running = false;
43 | });
44 | }
45 | }
46 | },
47 | running,
48 | );
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/lib/design/widget/tooltip.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | class TooltipScope extends StatefulWidget {
4 | final String? message;
5 | final InlineSpan? richMessage;
6 | final Duration? showDuration;
7 | final Widget Function(
8 | BuildContext context,
9 | Widget trigger,
10 | Future Function() showTooltip,
11 | ) builder;
12 | final Widget trigger;
13 |
14 | const TooltipScope({
15 | super.key,
16 | this.message,
17 | this.richMessage,
18 | this.showDuration,
19 | required this.trigger,
20 | required this.builder,
21 | });
22 |
23 | @override
24 | State createState() => _TooltipState();
25 | }
26 |
27 | class _TooltipState extends State {
28 | final $tooltip = GlobalKey(debugLabel: "Tooltip");
29 |
30 | @override
31 | Widget build(BuildContext context) {
32 | return widget.builder(
33 | context,
34 | buildTrigger(),
35 | showTooltip,
36 | );
37 | }
38 |
39 | Widget buildTrigger() {
40 | return Tooltip(
41 | key: $tooltip,
42 | message: widget.message,
43 | richMessage: widget.richMessage,
44 | showDuration: widget.showDuration,
45 | triggerMode: TooltipTriggerMode.tap,
46 | child: widget.trigger,
47 | );
48 | }
49 |
50 | Future showTooltip() async {
51 | $tooltip.currentState?.ensureTooltipVisible();
52 | await Future.delayed(widget.showDuration ?? const Duration(milliseconds: 1500));
53 | Tooltip.dismissAllToolTips();
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/lib/entity/campus.dart:
--------------------------------------------------------------------------------
1 | import 'package:easy_localization/easy_localization.dart';
2 | import 'package:mimir/storage/hive/type_id.dart';
3 |
4 | part 'campus.g.dart';
5 |
6 | @HiveType(typeId: CoreHiveType.campus)
7 | enum Campus {
8 | @HiveField(0)
9 | fengxian(),
10 | @HiveField(1)
11 | xuhui();
12 |
13 | const Campus();
14 |
15 | String l10n() => "campus.$name".tr();
16 | }
17 |
--------------------------------------------------------------------------------
/lib/entity/campus.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'campus.dart';
4 |
5 | // **************************************************************************
6 | // TypeAdapterGenerator
7 | // **************************************************************************
8 |
9 | class CampusAdapter extends TypeAdapter {
10 | @override
11 | final int typeId = 3;
12 |
13 | @override
14 | Campus read(BinaryReader reader) {
15 | switch (reader.readByte()) {
16 | case 0:
17 | return Campus.fengxian;
18 | case 1:
19 | return Campus.xuhui;
20 | default:
21 | return Campus.fengxian;
22 | }
23 | }
24 |
25 | @override
26 | void write(BinaryWriter writer, Campus obj) {
27 | switch (obj) {
28 | case Campus.fengxian:
29 | writer.writeByte(0);
30 | break;
31 | case Campus.xuhui:
32 | writer.writeByte(1);
33 | break;
34 | }
35 | }
36 |
37 | @override
38 | int get hashCode => typeId.hashCode;
39 |
40 | @override
41 | bool operator ==(Object other) =>
42 | identical(this, other) || other is CampusAdapter && runtimeType == other.runtimeType && typeId == other.typeId;
43 | }
44 |
--------------------------------------------------------------------------------
/lib/entity/uuid.dart:
--------------------------------------------------------------------------------
1 | abstract class WithUuid {
2 | String get uuid;
3 | }
4 |
--------------------------------------------------------------------------------
/lib/files.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 |
3 | import 'package:flutter/foundation.dart';
4 | import 'package:path/path.dart';
5 | import 'package:mimir/r.dart';
6 | import 'package:universal_platform/universal_platform.dart';
7 |
8 | class Files {
9 | const Files._();
10 |
11 | static Future init({
12 | required Directory temp,
13 | required Directory cache,
14 | required Directory internal,
15 | required Directory user,
16 | }) async {
17 | Files._temp = temp;
18 | Files.cache = cache;
19 | Files.internal = internal;
20 | Files.user = user;
21 | debugPrint("Cache dir: $cache");
22 | debugPrint("Temp dir: $temp");
23 | debugPrint("Internal dir: $internal");
24 | debugPrint("User dir: $user");
25 | Files.temp = TempDir._();
26 | }
27 |
28 | static late final Directory _temp;
29 | static late final TempDir temp;
30 | static late final Directory cache;
31 | static late final Directory internal;
32 | static late final Directory user;
33 |
34 | static const oaAnnounce = OaAnnounceFiles._();
35 | }
36 |
37 | extension DirectoryX on Directory {
38 | File subFile(String p1, [String? p2, String? p3, String? p4]) => File(join(path, p1, p2, p3, p4));
39 |
40 | Directory subDir(String p1, [String? p2, String? p3, String? p4]) => Directory(join(path, p1, p2, p3, p4));
41 | }
42 |
43 | class OaAnnounceFiles {
44 | const OaAnnounceFiles._();
45 |
46 | Directory attachmentDir(String uuid) => Files.internal.subDir("attachment", uuid);
47 |
48 | Future init() async {}
49 | }
50 |
51 | class TempDir {
52 | TempDir._();
53 |
54 | Directory? _realDir = UniversalPlatform.isWindows ? null : Files._temp;
55 |
56 | Directory dir() {
57 | // lazy create temp dir
58 | return _realDir ??= Files._temp.createTempSync(R.appId);
59 | }
60 |
61 | File subFile(String p1, [String? p2, String? p3, String? p4]) => dir().subFile(p1, p2, p3, p4);
62 |
63 | Directory subDir(String p1, [String? p2, String? p3, String? p4]) => dir().subDir(p1, p2, p3, p4);
64 | }
65 |
--------------------------------------------------------------------------------
/lib/l10n/app.dart:
--------------------------------------------------------------------------------
1 | import 'package:mimir/timetable/i18n.dart' as t;
2 | import 'package:mimir/school/i18n.dart' as s;
3 |
4 | class AppI18n {
5 | const AppI18n();
6 | final navigation = const _Navigation();
7 | }
8 |
9 | class _Navigation {
10 | const _Navigation();
11 |
12 | String get timetable => t.i18n.navigation;
13 | String get school => s.i18n.navigation;
14 | }
15 |
--------------------------------------------------------------------------------
/lib/l10n/extension.dart:
--------------------------------------------------------------------------------
1 | import 'package:easy_localization/easy_localization.dart';
2 | import 'package:flutter/material.dart';
3 | import 'package:mimir/l10n/time.dart';
4 |
5 | import 'lang.dart';
6 |
7 | export 'package:mimir/r.dart';
8 |
9 | export 'lang.dart';
10 |
11 | extension I18nBuildContext on BuildContext {
12 | ///e.g.: Wednesday, September 21, 2022
13 | String formatYmdWeekText(DateTime date) => Lang.formatOf(locale).ymdWeekText.format(date);
14 |
15 | ///e.g.: Wednesday, September 21
16 | String formatMdWeekText(DateTime date) => Lang.formatOf(locale).mdWeekText.format(date);
17 |
18 | ///e.g.: September 21, 2022
19 | String formatYmdText(DateTime date) => Lang.formatOf(locale).ymdText.format(date);
20 |
21 | ///e.g.: 9/21/2022
22 | String formatYmdNum(DateTime date) => Lang.formatOf(locale).ymdNum.format(date);
23 |
24 | ///e.g.: 9/21/2022 23:57:23
25 | String formatYmdhmsNum(DateTime date) => Lang.formatOf(locale).ymdhmsNum.format(date);
26 |
27 | ///e.g.: 9/21/2022 23:57
28 | String formatYmdhmNum(DateTime date) => Lang.formatOf(locale).ymdhmNum.format(date);
29 |
30 | String formatYmText(DateTime date) => Lang.formatOf(locale).ymText.format(date);
31 |
32 | /// e.g.: 8:32:59
33 | String formatHmsNum(DateTime date) => Lang.formatOf(locale).hms.format(date);
34 |
35 | /// e.g.: 8:32
36 | String formatHmNum(DateTime date) => Lang.formatOf(locale).hm.format(date);
37 |
38 | /// e.g.: 9/21
39 | String formatMdNum(DateTime date) => Lang.formatOf(locale).mdNum.format(date);
40 |
41 | /// e.g.: 9/21 7:32
42 | String formatMdhmNum(DateTime date) => Lang.formatOf(locale).mdHmNum.format(date);
43 |
44 | Weekday firstDayInWeek() => Lang.formatOf(locale).firstDayInWeek;
45 | }
46 |
47 | extension BrightnessL10nX on Brightness {
48 | String l10n() => "brightness.$name".tr();
49 | }
50 |
51 | extension ThemeModeL10nX on ThemeMode {
52 | String l10n() => "themeMode.$name".tr();
53 | }
54 |
--------------------------------------------------------------------------------
/lib/l10n/lang.dart:
--------------------------------------------------------------------------------
1 | import 'dart:ui';
2 |
3 | import 'package:intl/intl.dart';
4 | import 'package:mimir/l10n/time.dart';
5 | import 'package:mimir/r.dart';
6 |
7 | abstract class RegionalFormatter {
8 | DateFormat get hms;
9 |
10 | DateFormat get hm;
11 |
12 | DateFormat get ymdText;
13 |
14 | DateFormat get ymdWeekText;
15 |
16 | DateFormat get mdWeekText;
17 |
18 | DateFormat get ymText;
19 |
20 | DateFormat get ymdNum;
21 |
22 | DateFormat get ymdhmsNum;
23 |
24 | DateFormat get ymdhmNum;
25 |
26 | DateFormat get mdHmNum;
27 |
28 | DateFormat get mdNum;
29 |
30 | Weekday get firstDayInWeek;
31 | }
32 |
33 | class Lang {
34 | Lang._();
35 |
36 | static final zhHansFormatter = _ZhHansFormatter();
37 | static final locale2Format = {
38 | R.zhHansLocale: _ZhHansFormatter(),
39 | };
40 |
41 | static RegionalFormatter formatOf(Locale locale) => locale2Format[locale] ?? zhHansFormatter;
42 | }
43 |
44 | class _ZhHansFormatter implements RegionalFormatter {
45 | @override
46 | final hms = DateFormat("H:mm:ss");
47 | @override
48 | final hm = DateFormat("H:mm");
49 | @override
50 | final ymdText = DateFormat("yyyy年M月d日", "zh_Hans");
51 | @override
52 | final ymdWeekText = DateFormat("yyyy年M月d日 EEEE", "zh_Hans");
53 | @override
54 | final mdWeekText = DateFormat("M月d日 EEEE", "zh_Hans");
55 | @override
56 | final ymText = DateFormat("yyyy年M月", "zh_Hans");
57 | @override
58 | final ymdNum = DateFormat("yyyy/M/d", "zh_Hans");
59 | @override
60 | final ymdhmsNum = DateFormat("yyyy/M/d H:mm:ss", "zh_Hans");
61 | @override
62 | final ymdhmNum = DateFormat("yyyy/M/d H:mm:ss", "zh_Hans");
63 | @override
64 | final mdHmNum = DateFormat("M/d H:mm", "zh_Hans");
65 | @override
66 | final mdNum = DateFormat("M/d", "zh_Hans");
67 | @override
68 | final firstDayInWeek = Weekday.monday;
69 | }
70 |
--------------------------------------------------------------------------------
/lib/l10n/time.dart:
--------------------------------------------------------------------------------
1 | import 'package:easy_localization/easy_localization.dart';
2 | import 'package:json_annotation/json_annotation.dart';
3 |
4 | @JsonEnum()
5 | enum Weekday {
6 | monday,
7 | tuesday,
8 | wednesday,
9 | thursday,
10 | friday,
11 | saturday,
12 | sunday;
13 |
14 | int toJson() => index;
15 |
16 | static const calendarOrder = [
17 | sunday,
18 | monday,
19 | tuesday,
20 | wednesday,
21 | thursday,
22 | friday,
23 | saturday,
24 | ];
25 |
26 | int getIndex({required Weekday firstDay}) {
27 | return (this - firstDay.index).index;
28 | }
29 |
30 | const Weekday();
31 | static Weekday fromJson(int json) => Weekday.values.elementAtOrNull(json) ?? Weekday.monday;
32 |
33 | static Weekday fromIndex(int index) {
34 | assert(0 <= index && index < Weekday.values.length);
35 | return Weekday.values[index % Weekday.values.length];
36 | }
37 |
38 | String l10n() => "weekday.$index".tr();
39 |
40 | String l10nShort() => "weekdayShort.$index".tr();
41 |
42 | static List genSequence(Weekday firstDay) {
43 | return List.generate(7, (index) => firstDay + index);
44 | }
45 |
46 | Weekday operator +(int delta) {
47 | return Weekday.values[(index + delta) % Weekday.values.length];
48 | }
49 |
50 | Weekday operator -(int delta) {
51 | return Weekday.values[(index - delta) % Weekday.values.length];
52 | }
53 |
54 | List genSequenceStartWithThis() => genSequence(this);
55 | }
56 |
--------------------------------------------------------------------------------
/lib/l10n/tr.dart:
--------------------------------------------------------------------------------
1 | import 'package:easy_localization/easy_localization.dart';
2 | import 'package:flutter/widgets.dart';
3 |
4 | extension TrX on String {
5 | List trSpan({
6 | BuildContext? context,
7 | required Map args,
8 | }) {
9 | final translated = this.tr(
10 | namedArgs: args.map((k, v) => MapEntry(k, "{$k}")),
11 | );
12 | return replaceWidget(raw: translated, args: args);
13 | }
14 | }
15 |
16 | List replaceWidget({
17 | required String raw,
18 | required Map args,
19 | }) {
20 | List spans = [];
21 | RegExp regExp = RegExp(r'{(.*?)}');
22 | Iterable matches = regExp.allMatches(raw);
23 | int currentIndex = 0;
24 |
25 | for (Match match in matches) {
26 | spans.add(TextSpan(text: raw.substring(currentIndex, match.start)));
27 | final key = match.group(1);
28 | if (key == null) {
29 | spans.add(const TextSpan(text: "?"));
30 | } else {
31 | final replaced = args[key];
32 | if (replaced == null) {
33 | spans.add(TextSpan(text: key));
34 | } else {
35 | spans.add(replaced);
36 | }
37 | }
38 | currentIndex = match.end;
39 | }
40 |
41 | if (currentIndex < raw.length) {
42 | spans.add(TextSpan(text: raw.substring(currentIndex)));
43 | }
44 | return spans;
45 | }
46 |
--------------------------------------------------------------------------------
/lib/l10n/yaml_assets_loader.dart:
--------------------------------------------------------------------------------
1 | import 'dart:developer';
2 | import 'dart:ui';
3 |
4 | import 'package:easy_localization/easy_localization.dart';
5 | import 'package:flutter/services.dart';
6 | import 'package:yaml/yaml.dart';
7 |
8 | //Loader for multiple yaml files
9 | class YamlAssetLoader extends AssetLoader {
10 | String getLocalePath(String basePath, Locale locale) {
11 | return '$basePath/${locale.toStringWithSeparator(separator: "-")}.yaml';
12 | }
13 |
14 | @override
15 | Future