├── .clineignore ├── .editorconfig ├── .github └── workflows │ ├── flutter-build.yml │ └── flutter-pre-build.yml ├── .gitignore ├── .metadata ├── .roo └── mcp.json ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── README.zh_CN.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── ic_launcher-playstore.png │ │ ├── kotlin │ │ │ └── love │ │ │ │ └── moegirl │ │ │ │ └── moekey │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable-v24 │ │ │ └── ic_launcher_background.xml │ │ │ ├── drawable │ │ │ ├── ic_launcher_foreground.xml │ │ │ └── launch_background.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ └── values │ │ │ ├── colors.xml │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── assets ├── android_icon │ ├── ic_launcher_background.svg │ └── ic_launcher_foreground.svg ├── emoji_list.json ├── favicon.icns ├── favicon.ico ├── favicon.png ├── locales │ ├── en-US.2023.11.0.json │ ├── ja-JP.2023.11.0.json │ ├── zh-CN.2023.11.0.json │ └── zh-TW.2023.11.0.json ├── misskey.png └── misskey.svg ├── crowdin.yml ├── devtools_options.yaml ├── docs ├── Moekey.png ├── Screenshot.png └── banner.png ├── ios ├── .gitignore ├── ExportOptions.plist ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Podfile ├── Podfile.lock ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings ├── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-50x50@1x.png │ │ │ ├── Icon-App-50x50@2x.png │ │ │ ├── Icon-App-57x57@1x.png │ │ │ ├── Icon-App-57x57@2x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-72x72@1x.png │ │ │ ├── Icon-App-72x72@2x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Info.plist │ └── Runner-Bridging-Header.h └── RunnerTests │ └── RunnerTests.swift ├── lib ├── apis │ ├── dio.dart │ ├── index.dart │ ├── models │ │ ├── announcement.dart │ │ ├── app.dart │ │ ├── auth.dart │ │ ├── clips.dart │ │ ├── drive.dart │ │ ├── emojis.dart │ │ ├── following.dart │ │ ├── login_user.dart │ │ ├── me_detailed.dart │ │ ├── meta.dart │ │ ├── note.dart │ │ ├── notification.dart │ │ ├── translate.dart │ │ ├── user_full.dart │ │ └── user_lite.dart │ └── services │ │ ├── account_service.dart │ │ ├── app_service.dart │ │ ├── auth_service.dart │ │ ├── clips_service.dart │ │ ├── drive_service.dart │ │ ├── following_service.dart │ │ ├── hashtags_service.dart │ │ ├── meta_service.dart │ │ ├── notes_service.dart │ │ ├── services.dart │ │ └── user_service.dart ├── constants │ └── languages.dart ├── database │ ├── init_database.dart │ ├── instance.dart │ ├── link_preview.dart │ ├── notes.dart │ ├── timeline.dart │ └── users.dart ├── generated │ ├── intl │ │ ├── messages_af_ZA.dart │ │ ├── messages_all.dart │ │ ├── messages_ar_SA.dart │ │ ├── messages_ca_ES.dart │ │ ├── messages_cs_CZ.dart │ │ ├── messages_da_DK.dart │ │ ├── messages_de_DE.dart │ │ ├── messages_el_GR.dart │ │ ├── messages_en_US.dart │ │ ├── messages_es_ES.dart │ │ ├── messages_fi_FI.dart │ │ ├── messages_fr_FR.dart │ │ ├── messages_he_IL.dart │ │ ├── messages_hu_HU.dart │ │ ├── messages_it_IT.dart │ │ ├── messages_ja_JP.dart │ │ ├── messages_ko_KR.dart │ │ ├── messages_nl_NL.dart │ │ ├── messages_no_NO.dart │ │ ├── messages_pl_PL.dart │ │ ├── messages_pt_BR.dart │ │ ├── messages_pt_PT.dart │ │ ├── messages_ro_RO.dart │ │ ├── messages_ru_RU.dart │ │ ├── messages_sr_SP.dart │ │ ├── messages_sv_SE.dart │ │ ├── messages_tr_TR.dart │ │ ├── messages_uk_UA.dart │ │ ├── messages_vi_VN.dart │ │ ├── messages_zh_CN.dart │ │ └── messages_zh_TW.dart │ └── l10n.dart ├── hook │ ├── useExtendedPageController.dart │ ├── useTimelineScrollController.dart │ └── use_mk_refresh_load_list_controller.dart ├── l10n │ ├── intl_af_ZA.arb │ ├── intl_ar_SA.arb │ ├── intl_ca_ES.arb │ ├── intl_cs_CZ.arb │ ├── intl_da_DK.arb │ ├── intl_de_DE.arb │ ├── intl_el_GR.arb │ ├── intl_en_US.arb │ ├── intl_es_ES.arb │ ├── intl_fi_FI.arb │ ├── intl_fr_FR.arb │ ├── intl_he_IL.arb │ ├── intl_hu_HU.arb │ ├── intl_it_IT.arb │ ├── intl_ja_JP.arb │ ├── intl_ko_KR.arb │ ├── intl_nl_NL.arb │ ├── intl_no_NO.arb │ ├── intl_pl_PL.arb │ ├── intl_pt_BR.arb │ ├── intl_pt_PT.arb │ ├── intl_ro_RO.arb │ ├── intl_ru_RU.arb │ ├── intl_sr_SP.arb │ ├── intl_sv_SE.arb │ ├── intl_tr_TR.arb │ ├── intl_uk_UA.arb │ ├── intl_vi_VN.arb │ ├── intl_zh_CN.arb │ └── intl_zh_TW.arb ├── logger.dart ├── main.dart ├── pages │ ├── announcements │ │ └── announcements.dart │ ├── clips │ │ ├── clips.dart │ │ ├── clips_collection.dart │ │ ├── clips_my.dart │ │ ├── clips_notes.dart │ │ └── clips_page.dart │ ├── drive │ │ ├── drive_info.dart │ │ └── drive_page.dart │ ├── explore │ │ ├── explore.dart │ │ ├── hot.dart │ │ └── users.dart │ ├── hashtag │ │ └── hashtag_page.dart │ ├── home │ │ ├── home_page.dart │ │ └── home_page_state.dart │ ├── image_preview │ │ └── image_preview.dart │ ├── login │ │ └── login_page.dart │ ├── notes │ │ └── note_page.dart │ ├── notifications │ │ ├── notifications_group_list.dart │ │ ├── notifications_mentions_list.dart │ │ └── notifications_page.dart │ ├── search │ │ ├── notes_search.dart │ │ ├── search_page.dart │ │ └── user_search.dart │ ├── settings │ │ ├── account_manager │ │ │ └── account_manager_page.dart │ │ ├── member_info_state.dart │ │ ├── profile │ │ │ └── profile.dart │ │ ├── router.dart │ │ ├── settings_page.dart │ │ ├── test │ │ │ └── test_page.dart │ │ └── two_panel_layout.dart │ ├── splash_page │ │ └── splash_page.dart │ ├── timeline │ │ ├── timeline_list.dart │ │ └── timeline_page.dart │ ├── user_widgets │ │ └── widgets_list │ │ │ ├── state.dart │ │ │ └── view.dart │ └── users │ │ ├── user_clip_list.dart │ │ ├── user_follow.dart │ │ ├── user_notes_list.dart │ │ ├── user_overview.dart │ │ ├── user_page.dart │ │ └── user_reactions_list.dart ├── router │ └── router.dart ├── status │ ├── apis.dart │ ├── dio.dart │ ├── global_snackbar.dart │ ├── me_detailed.dart │ ├── misskey_api.dart │ ├── mk_tabbar_refresh_scroll_state.dart │ ├── notes.dart │ ├── notes_listener.dart │ ├── notifications.dart │ ├── server.dart │ ├── themes.dart │ ├── timeline.dart │ ├── user.dart │ ├── user_login.dart │ └── websocket.dart ├── utils │ ├── custom_rect_tween.dart │ ├── format_duration.dart │ ├── get_padding_note.dart │ ├── get_random_string.dart │ ├── get_token.dart │ ├── image_compression.dart │ ├── parse_color.dart │ ├── save_image.dart │ ├── time_ago_since_date.dart │ ├── time_to_desired_format.dart │ └── update_themes.dart └── widgets │ ├── blur_widget.dart │ ├── clips │ ├── clips_create_dialog.dart │ ├── clips_create_dialog_state.dart │ └── clips_folder.dart │ ├── context_menu.dart │ ├── driver │ ├── create_from_url_dialog.dart │ ├── drive.dart │ ├── drive_thumbnail.dart │ ├── driver_list.dart │ ├── driver_select_dialog │ │ ├── driver_select_dialog.dart │ │ └── driver_select_dialog_state.dart │ ├── driver_upload_bar.dart │ └── upload_file_dialog.dart │ ├── emoji_list.dart │ ├── hashtag │ ├── hashtag_select_dialog.dart │ └── hashtag_select_dialog_state.dart │ ├── hover_builder.dart │ ├── keep_alive_wrapper.dart │ ├── loading_weight.dart │ ├── login │ ├── login_dialog.dart │ ├── servers_select.dart │ └── servers_select_state.dart │ ├── mfm_text │ ├── animate │ │ ├── jelly.dart │ │ └── spin.dart │ └── mfm_text.dart │ ├── mk_button.dart │ ├── mk_card.dart │ ├── mk_date_picker.dart │ ├── mk_dialog.dart │ ├── mk_header.dart │ ├── mk_image.dart │ ├── mk_info_dialog.dart │ ├── mk_input.dart │ ├── mk_load_more.dart │ ├── mk_modal.dart │ ├── mk_nav_button.dart │ ├── mk_overflow_show.dart │ ├── mk_parallax.dart │ ├── mk_refresh_indicator.dart │ ├── mk_refresh_load.dart │ ├── mk_refresh_loading_empty_wrapper.dart │ ├── mk_scaffold.dart │ ├── mk_select.dart │ ├── mk_sliver_pinned.dart │ ├── mk_switch.dart │ ├── mk_tabbar_list.dart │ ├── mk_text_input_dialog.dart │ ├── mk_user_card.dart │ ├── note_create_dialog │ ├── note_create_dialog.dart │ └── note_create_dialog_state.dart │ ├── notes │ ├── note_card.dart │ ├── note_children.dart │ ├── note_image.dart │ └── note_pagination_list.dart │ ├── notifications │ └── notifications_user_card.dart │ ├── reactions.dart │ ├── settings │ ├── editable_fields_list.dart │ ├── fields.dart │ └── folder.dart │ ├── sliver_load_more.dart │ ├── sliver_presistent_header.dart │ ├── timeline_listview.dart │ ├── user_select_dialog │ ├── user_select_dialog.dart │ └── user_select_dialog_state.dart │ └── video_player.dart ├── linux ├── .gitignore ├── CMakeLists.txt ├── flutter │ ├── CMakeLists.txt │ ├── generated_plugin_registrant.cc │ ├── generated_plugin_registrant.h │ └── generated_plugins.cmake ├── main.cc ├── my_application.cc └── my_application.h ├── macos ├── .gitignore ├── Flutter │ ├── Flutter-Debug.xcconfig │ ├── Flutter-Release.xcconfig │ └── GeneratedPluginRegistrant.swift ├── Podfile ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── app_icon_1024.png │ │ │ ├── app_icon_128.png │ │ │ ├── app_icon_16.png │ │ │ ├── app_icon_256.png │ │ │ ├── app_icon_32.png │ │ │ ├── app_icon_512.png │ │ │ └── app_icon_64.png │ ├── Base.lproj │ │ └── MainMenu.xib │ ├── Configs │ │ ├── AppInfo.xcconfig │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ └── Warnings.xcconfig │ ├── DebugProfile.entitlements │ ├── Info.plist │ ├── MainFlutterWindow.swift │ └── Release.entitlements └── RunnerTests │ └── RunnerTests.swift ├── pubspec.lock ├── pubspec.yaml ├── test └── widget_test.dart └── windows ├── .gitignore ├── CMakeLists.txt ├── flutter ├── CMakeLists.txt ├── generated_plugin_registrant.cc ├── generated_plugin_registrant.h └── generated_plugins.cmake └── runner ├── CMakeLists.txt ├── Runner.rc ├── flutter_window.cpp ├── flutter_window.h ├── main.cpp ├── resource.h ├── resources └── app_icon.ico ├── runner.exe.manifest ├── utils.cpp ├── utils.h ├── win32_window.cpp └── win32_window.h /.clineignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | *.g.dart 19 | *.freezed.dart 20 | # The .vscode folder contains launch configuration and tasks you configure in 21 | # VS Code which you may wish to be included in version control, so this line 22 | # is commented out by default. 23 | #.vscode/ 24 | 25 | # Flutter/Dart/Pub related 26 | **/doc/api/ 27 | **/ios/Flutter/.last_build_id 28 | .dart_tool/ 29 | .flutter-plugins 30 | .flutter-plugins-dependencies 31 | .packages 32 | .pub-cache/ 33 | .pub/ 34 | /build/ 35 | 36 | # Symbolication related 37 | app.*.symbols 38 | 39 | # Obfuscation related 40 | app.*.map.json 41 | 42 | # Android Studio will place build artifacts here 43 | /android/app/debug 44 | /android/app/profile 45 | /android/app/release 46 | /android/app/.cxx/ 47 | .git -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.dart] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | *.g.dart 19 | *.freezed.dart 20 | # The .vscode folder contains launch configuration and tasks you configure in 21 | # VS Code which you may wish to be included in version control, so this line 22 | # is commented out by default. 23 | #.vscode/ 24 | 25 | # Flutter/Dart/Pub related 26 | **/doc/api/ 27 | **/ios/Flutter/.last_build_id 28 | .dart_tool/ 29 | .flutter-plugins 30 | .flutter-plugins-dependencies 31 | .packages 32 | .pub-cache/ 33 | .pub/ 34 | /build/ 35 | 36 | # Symbolication related 37 | app.*.symbols 38 | 39 | # Obfuscation related 40 | app.*.map.json 41 | 42 | # Android Studio will place build artifacts here 43 | /android/app/debug 44 | /android/app/profile 45 | /android/app/release 46 | /android/app/.cxx/ 47 | -------------------------------------------------------------------------------- /.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: "9e1c857886f07d342cf106f2cd588bcd5e031bb2" 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: 9e1c857886f07d342cf106f2cd588bcd5e031bb2 17 | base_revision: 9e1c857886f07d342cf106f2cd588bcd5e031bb2 18 | - platform: android 19 | create_revision: 9e1c857886f07d342cf106f2cd588bcd5e031bb2 20 | base_revision: 9e1c857886f07d342cf106f2cd588bcd5e031bb2 21 | - platform: ios 22 | create_revision: 9e1c857886f07d342cf106f2cd588bcd5e031bb2 23 | base_revision: 9e1c857886f07d342cf106f2cd588bcd5e031bb2 24 | - platform: macos 25 | create_revision: 9e1c857886f07d342cf106f2cd588bcd5e031bb2 26 | base_revision: 9e1c857886f07d342cf106f2cd588bcd5e031bb2 27 | 28 | # User provided section 29 | 30 | # List of Local paths (relative to this file) that should be 31 | # ignored by the migrate tool. 32 | # 33 | # Files that are not part of the templates will be ignored by default. 34 | unmanaged_files: 35 | - 'lib/main.dart' 36 | - 'ios/Runner.xcodeproj/project.pbxproj' 37 | -------------------------------------------------------------------------------- /.roo/mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": {} 3 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "MoeKey", 9 | "request": "launch", 10 | "type": "dart" 11 | }, 12 | { 13 | "name": "MoeKey (profile mode)", 14 | "request": "launch", 15 | "type": "dart", 16 | "flutterMode": "profile" 17 | }, 18 | { 19 | "name": "MoeKey (release mode)", 20 | "request": "launch", 21 | "type": "dart", 22 | "flutterMode": "release" 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/*.freezed.dart": true, 4 | "**/*.g.dart": true 5 | }, 6 | "emeraldwalk.runonsave": { 7 | "commands": [ 8 | { 9 | "match": "\\.arb$", 10 | "isAsync": true, 11 | "cmd": "flutter pub global run intl_utils:generate" 12 | } 13 | ] 14 | } 15 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "Generate Intl", 6 | "type": "shell", 7 | "command": "flutter pub global run intl_utils:generate", 8 | "problemMatcher": [], 9 | "presentation": { 10 | "reveal": "always", 11 | "panel": "new" 12 | }, 13 | "group": "build" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | English | [简体中文](./README.zh_CN.md) 2 | 3 | ![](./docs/banner.png) 4 | 5 | [![Crowdin](https://badges.crowdin.net/moekey/localized.svg)](https://crowdin.com/project/moekey) 6 | 7 | # MoeKey 8 | 9 | MoeKey is a cross-platform misskey client made by Flutter. 10 | 11 | ## Features 12 | 13 | MoeKey wants to be a UI style consistent with the original Misskey. A fully functional Misskey client. 14 | 15 | > This project is currently under development and has many functional deficiencies 16 | 17 | Currently implemented features: 18 | 19 | - Multi-user login 20 | - Timeline view, search 21 | - Notes related functions 22 | - Notification view 23 | - Clip 24 | - Misskey Announcements 25 | - Explore 26 | - HashTag Page 27 | 28 | Temporarily unimplemented features 29 | 30 | - User Profile Edit 31 | - Misskey Settings 32 | - Antennas, Channels, Lists 33 | - User widgets 34 | - User Achievements 35 | 36 | ## Download 37 | 38 | - [GitHub Releases](https://github.com/MoeKeyDev/MoeKey/releases/latest) 39 | 40 | ## Screenshot 41 | 42 | ![](./docs/Screenshot.png) 43 | 44 | ## Developers 45 | 46 | ### Localize 47 | 48 | Help us translate MoeKey into your language on [Crowdin](https://crowdin.com/project/moekey) 49 | 50 | riverpod code gen 51 | 52 | ```shell 53 | dart run build_runner watch --use-polling-watcher 54 | ``` 55 | -------------------------------------------------------------------------------- /README.zh_CN.md: -------------------------------------------------------------------------------- 1 | [English](./README.md) | 简体中文 2 | 3 | ![](./docs/banner.png) 4 | 5 | [![Crowdin](https://badges.crowdin.net/moekey/localized.svg)](https://crowdin.com/project/moekey) 6 | 7 | # MoeKey 8 | 9 | MoeKey 是由 Flutter 制作的跨平台 misskey 客户端。 10 | 11 | ## 特性 12 | 13 | MoeKey希望成为一个UI风格与原版Misskey保持一致。功能完善的Misskey客户端。 14 | 15 | > 该项目目前正在开发中,存在许多功能缺陷 16 | 17 | 目前已经实现的功能: 18 | 19 | - 多用户登录 20 | - 时间线查看、搜索 21 | - 帖子的发布相关功能 22 | - 通知查看 23 | - 便签 24 | - 系统公告 25 | - 发现 26 | - HashTag 浏览 27 | 28 | 暂时未实现的功能 29 | 30 | - 个人资料编辑 31 | - Misskey设置 32 | - 天线,频道,列表 33 | - 用户小组件 34 | - 用户成就 35 | 36 | ## 下载 37 | 38 | - [GitHub Releases](https://github.com/MoeKeyDev/MoeKey/releases/latest) 39 | 40 | ## 截图 41 | 42 | ![](./docs/Screenshot.png) 43 | 44 | ## 开发人员 45 | 46 | ### 本地化 47 | 48 | 帮助我们将 MoeKey 翻译成您的语言,请登录 [Crowdin](https://crowdin.com/project/moekey) 49 | 50 | ### 代码生成 51 | 52 | Riverpod 代码生成 53 | 54 | ```shell 55 | dart run build_runner watch --use-polling-watcher 56 | ``` -------------------------------------------------------------------------------- /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 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at https://dart.dev/lints. 17 | # 18 | # Instead of disabling a lint rule for the entire project in the 19 | # section below, it can also be suppressed for a single line of code 20 | # or a specific dart file by using the `// ignore: name_of_lint` and 21 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 22 | # producing the lint. 23 | rules: 24 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 25 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 26 | 27 | # Additional information about this file can be found at 28 | # https://dart.dev/guides/language/analysis-options 29 | -------------------------------------------------------------------------------- /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/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | id "dev.flutter.flutter-gradle-plugin" 5 | } 6 | 7 | def localProperties = new Properties() 8 | def localPropertiesFile = rootProject.file('local.properties') 9 | if (localPropertiesFile.exists()) { 10 | localPropertiesFile.withReader('UTF-8') { reader -> 11 | localProperties.load(reader) 12 | } 13 | } 14 | 15 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 16 | if (flutterVersionCode == null) { 17 | flutterVersionCode = '1' 18 | } 19 | 20 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 21 | if (flutterVersionName == null) { 22 | flutterVersionName = '1.0' 23 | } 24 | def keystoreProperties = new Properties() 25 | def keystorePropertiesFile = rootProject.file('key.properties') 26 | if (keystorePropertiesFile.exists()) { 27 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 28 | } 29 | 30 | 31 | android { 32 | namespace "love.moegirl.moekey" 33 | compileSdkVersion flutter.compileSdkVersion 34 | ndkVersion "27.0.12077973" 35 | 36 | compileOptions { 37 | sourceCompatibility JavaVersion.VERSION_1_8 38 | targetCompatibility JavaVersion.VERSION_1_8 39 | } 40 | 41 | kotlinOptions { 42 | jvmTarget = '1.8' 43 | } 44 | 45 | sourceSets { 46 | main.java.srcDirs += 'src/main/kotlin' 47 | } 48 | 49 | defaultConfig { 50 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 51 | applicationId "love.moegirl.moekey" 52 | // You can update the following values to match your application needs. 53 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. 54 | minSdkVersion flutter.minSdkVersion 55 | targetSdkVersion flutter.targetSdkVersion 56 | versionCode flutterVersionCode.toInteger() 57 | versionName flutterVersionName 58 | archivesBaseName = "MoeKey-$versionName+$versionCode-Android" 59 | } 60 | 61 | signingConfigs { 62 | release { 63 | keyAlias keystoreProperties['keyAlias'] 64 | keyPassword keystoreProperties['keyPassword'] 65 | storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null 66 | storePassword keystoreProperties['storePassword'] 67 | } 68 | } 69 | buildTypes { 70 | release { 71 | signingConfig signingConfigs.release 72 | } 73 | } 74 | } 75 | 76 | flutter { 77 | source '../..' 78 | } 79 | 80 | dependencies {} 81 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 23 | 27 | 31 | 32 | 33 | 34 | 35 | 36 | 38 | 41 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /android/app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/android/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /android/app/src/main/kotlin/love/moegirl/moekey/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package love.moegirl.moekey 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 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v24/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 15 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #E9AFC6 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.9.22' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 10 | } 11 | } 12 | 13 | allprojects { 14 | repositories { 15 | google() 16 | mavenCentral() 17 | } 18 | } 19 | 20 | rootProject.buildDir = '../build' 21 | subprojects { 22 | project.buildDir = "${rootProject.buildDir}/${project.name}" 23 | } 24 | subprojects { 25 | project.evaluationDependsOn(':app') 26 | } 27 | 28 | tasks.register("clean", Delete) { 29 | delete rootProject.buildDir 30 | } 31 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4G 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /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.9-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 | plugins { 20 | id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false 21 | } 22 | } 23 | 24 | plugins { 25 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 26 | id "com.android.application" version "8.7.0" apply false 27 | } 28 | 29 | include ":app" 30 | -------------------------------------------------------------------------------- /assets/android_icon/ic_launcher_background.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 15 | 17 | 21 | 25 | 26 | 34 | 35 | 37 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /assets/favicon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/assets/favicon.icns -------------------------------------------------------------------------------- /assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/assets/favicon.ico -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/assets/favicon.png -------------------------------------------------------------------------------- /assets/misskey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/assets/misskey.png -------------------------------------------------------------------------------- /crowdin.yml: -------------------------------------------------------------------------------- 1 | project_id_env: CROWDIN_PROJECT_ID 2 | api_token_env: CROWDIN_KEY 3 | base_path: . 4 | base_url: 'https://api.crowdin.com' 5 | preserve_hierarchy: 1 6 | files: 7 | - source: /lib/l10n/intl_zh_CN.arb 8 | translation: /%original_path%/intl_%locale_with_underscore%.arb 9 | type: arb 10 | update_option: update_as_unapproved 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/Moekey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/docs/Moekey.png -------------------------------------------------------------------------------- /docs/Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/docs/Screenshot.png -------------------------------------------------------------------------------- /docs/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/docs/banner.png -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /ios/ExportOptions.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | method 6 | ad-hoc 7 | compileBitcode 8 | 9 | 10 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 11.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '11.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | target 'RunnerTests' do 36 | inherit! :search_paths 37 | end 38 | end 39 | 40 | post_install do |installer| 41 | installer.pods_project.targets.each do |target| 42 | flutter_additional_ios_build_settings(target) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | {"images":[{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}} -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | MoeKey 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | MoeKey 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | CADisableMinimumFrameDurationOnPhone 45 | 46 | UIApplicationSupportsIndirectInputEvents 47 | 48 | NSAppTransportSecurity 49 | 50 | NSAllowsArbitraryLoads 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /ios/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /lib/apis/dio.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:dio_smart_retry/dio_smart_retry.dart'; 3 | 4 | class MisskeyApisHttpClient { 5 | MisskeyApisHttpClient({ 6 | required this.host, 7 | required this.accessToken, 8 | required this.onUnauthorized, 9 | }) { 10 | client = Dio(BaseOptions( 11 | baseUrl: "$host/api", 12 | )); 13 | client.interceptors.add( 14 | RetryInterceptor( 15 | dio: client, 16 | logPrint: print, 17 | ), 18 | ); 19 | } 20 | 21 | String host; 22 | String accessToken; 23 | Function? onUnauthorized; 24 | late Dio client; 25 | 26 | Future post( 27 | String path, { 28 | Map? data, 29 | auth = true, 30 | Options? options, 31 | }) async { 32 | try { 33 | return (await client.post(path, 34 | data: { 35 | if (auth) "i": accessToken, 36 | ...?data, 37 | }, 38 | options: options)) 39 | .data; 40 | } on DioException catch (e) { 41 | // 401 42 | if (e.response?.statusCode == 401) { 43 | onUnauthorized?.call(); 44 | } 45 | rethrow; 46 | } 47 | } 48 | 49 | Future?> get( 50 | String path, { 51 | Map? data, 52 | auth = true, 53 | Options? options, 54 | }) async { 55 | try { 56 | return (await client.get(path, 57 | queryParameters: { 58 | if (auth) "i": accessToken, 59 | ...?data, 60 | }, 61 | options: options)) 62 | .data; 63 | } on DioException catch (e) { 64 | // 401 65 | if (e.response?.statusCode == 401) { 66 | onUnauthorized?.call(); 67 | } 68 | rethrow; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/apis/index.dart: -------------------------------------------------------------------------------- 1 | import 'package:moekey/apis/dio.dart'; 2 | import 'package:moekey/apis/services/account_service.dart'; 3 | import 'package:moekey/apis/services/app_service.dart'; 4 | import 'package:moekey/apis/services/auth_service.dart'; 5 | import 'package:moekey/apis/services/clips_service.dart'; 6 | import 'package:moekey/apis/services/drive_service.dart'; 7 | import 'package:moekey/apis/services/following_service.dart'; 8 | import 'package:moekey/apis/services/hashtags_service.dart'; 9 | import 'package:moekey/apis/services/meta_service.dart'; 10 | import 'package:moekey/apis/services/notes_service.dart'; 11 | import 'package:moekey/apis/services/user_service.dart'; 12 | 13 | class MisskeyApis { 14 | MisskeyApis({ 15 | required this.instance, 16 | required this.accessToken, 17 | required this.onUnauthorized, 18 | }) { 19 | client = MisskeyApisHttpClient( 20 | host: instance, 21 | accessToken: accessToken, 22 | onUnauthorized: onUnauthorized, 23 | ); 24 | account = AccountService(client: client); 25 | app = AppService(client: client); 26 | auth = AuthService(client: client); 27 | meta = MetaService(client: client); 28 | notes = NotesService(client: client); 29 | drive = DriveService(client: client); 30 | user = UserService(client: client); 31 | following = FollowingService(client: client); 32 | clips = ClipsService(client: client); 33 | hashtags = HashtagsService(client: client); 34 | } 35 | 36 | String instance; 37 | String accessToken; 38 | Function? onUnauthorized; 39 | late MisskeyApisHttpClient client; 40 | late AccountService account; 41 | late AppService app; 42 | late AuthService auth; 43 | late MetaService meta; 44 | late NotesService notes; 45 | late DriveService drive; 46 | late UserService user; 47 | late FollowingService following; 48 | late ClipsService clips; 49 | late HashtagsService hashtags; 50 | } 51 | -------------------------------------------------------------------------------- /lib/apis/models/announcement.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | 4 | part 'announcement.freezed.dart'; 5 | 6 | part 'announcement.g.dart'; 7 | 8 | @unfreezed 9 | abstract class Announcement with _$Announcement { 10 | factory Announcement({ 11 | double? closeDuration, 12 | required DateTime createdAt, 13 | required AnnouncementDisplay display, 14 | double? displayOrder, 15 | required bool forYou, 16 | required AnnouncementIcon icon, 17 | required String id, 18 | String? imageUrl, 19 | required bool isRead, 20 | required bool needConfirmationToRead, 21 | required bool silence, 22 | required String text, 23 | required String title, 24 | DateTime? updatedAt, 25 | }) = _Announcement; 26 | 27 | factory Announcement.fromJson(Map json) => 28 | _$AnnouncementFromJson(json); 29 | } 30 | 31 | enum AnnouncementDisplay { 32 | @JsonValue('banner') 33 | BANNER, 34 | @JsonValue('dialog') 35 | DIALOG, 36 | @JsonValue('normal') 37 | NORMAL, 38 | } 39 | 40 | enum AnnouncementIcon { 41 | @JsonValue('error') 42 | ERROR, 43 | @JsonValue('info') 44 | INFO, 45 | @JsonValue('success') 46 | SUCCESS, 47 | @JsonValue('warning') 48 | WARNING, 49 | } 50 | -------------------------------------------------------------------------------- /lib/apis/models/app.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | 4 | part 'app.freezed.dart'; 5 | 6 | part 'app.g.dart'; 7 | 8 | @freezed 9 | abstract class AppModel with _$AppModel { 10 | const factory AppModel({ 11 | String? callbackUrl, 12 | required String id, 13 | bool? isAuthorized, 14 | required String name, 15 | required List permission, 16 | String? secret, 17 | }) = _AppModel; 18 | 19 | factory AppModel.fromJson(Map json) => 20 | _$AppModelFromJson(json); 21 | } 22 | -------------------------------------------------------------------------------- /lib/apis/models/auth.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | 4 | part 'auth.freezed.dart'; 5 | 6 | part 'auth.g.dart'; 7 | 8 | @freezed 9 | abstract class SessionGenerateModel with _$SessionGenerateModel { 10 | const factory SessionGenerateModel({ 11 | required String token, 12 | required String url, 13 | }) = _SessionGenerateModel; 14 | 15 | factory SessionGenerateModel.fromJson(Map json) => 16 | _$SessionGenerateModelFromJson(json); 17 | } 18 | -------------------------------------------------------------------------------- /lib/apis/models/clips.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:moekey/apis/models/user_lite.dart'; 4 | 5 | part 'clips.freezed.dart'; 6 | 7 | part 'clips.g.dart'; 8 | 9 | @freezed 10 | abstract class ClipsModel with _$ClipsModel { 11 | const factory ClipsModel({ 12 | required DateTime createdAt, 13 | String? description, 14 | required int favoritedCount, 15 | required String id, 16 | required bool isFavorited, 17 | required bool isPublic, 18 | DateTime? lastClippedAt, 19 | required String name, 20 | required UserLiteModel user, 21 | required String userId, 22 | }) = _ClipsModel; 23 | 24 | factory ClipsModel.fromJson(Map json) => 25 | _$ClipsModelFromJson(json); 26 | } 27 | -------------------------------------------------------------------------------- /lib/apis/models/drive.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | 4 | part 'drive.freezed.dart'; 5 | 6 | part 'drive.g.dart'; 7 | 8 | class DriveModel { 9 | DriveModel( 10 | this.id, 11 | this.name, 12 | this.createdAt, 13 | ); 14 | 15 | final String id; 16 | final String name; 17 | final String createdAt; 18 | } 19 | 20 | @freezed 21 | abstract class DriveFileModel extends DriveModel with _$DriveFileModel { 22 | DriveFileModel._(super.id, super.name, super.createdAt); 23 | 24 | factory DriveFileModel( 25 | String id, 26 | String name, 27 | String createdAt, 28 | String? blurhash, 29 | String type, 30 | String url, 31 | int size, 32 | bool isSensitive, 33 | String? comment, 34 | Properties? properties, 35 | String? thumbnailUrl, 36 | ) = _DriveFileModel; 37 | 38 | factory DriveFileModel.fromJson(Map map) => 39 | _$DriveFileModelFromJson(map); 40 | } 41 | 42 | @freezed 43 | abstract class Properties with _$Properties { 44 | const factory Properties({ 45 | String? avgColor, 46 | double? height, 47 | double? orientation, 48 | double? width, 49 | }) = _Properties; 50 | 51 | factory Properties.fromJson(Map map) => 52 | _$PropertiesFromJson(map); 53 | } 54 | 55 | @freezed 56 | abstract class DriverFolderModel extends DriveModel with _$DriverFolderModel { 57 | DriverFolderModel._(super.id, super.name, super.createdAt); 58 | 59 | factory DriverFolderModel( 60 | String id, 61 | String? parentId, 62 | String name, 63 | String createdAt, 64 | ) = _DriverFolderModel; 65 | 66 | factory DriverFolderModel.fromJson(Map map) => 67 | _$DriverFolderModelFromJson(map); 68 | } 69 | -------------------------------------------------------------------------------- /lib/apis/models/emojis.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | 4 | part 'emojis.freezed.dart'; 5 | 6 | part 'emojis.g.dart'; 7 | 8 | @freezed 9 | abstract class EmojiSimple with _$EmojiSimple { 10 | const factory EmojiSimple({ 11 | required List aliases, 12 | String? category, 13 | bool? isSensitive, 14 | bool? localOnly, 15 | required String name, 16 | List? roleIdsThatCanBeUsedThisEmojiAsReaction, 17 | List? roleIdsThatCanNotBeUsedThisEmojiAsReaction, 18 | required String url, 19 | @Default(false) bool code, 20 | }) = _EmojiSimple; 21 | 22 | factory EmojiSimple.fromJson(Map json) => 23 | _$EmojiSimpleFromJson(json); 24 | } 25 | -------------------------------------------------------------------------------- /lib/apis/models/following.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:moekey/apis/models/user_full.dart'; 3 | import 'package:flutter/foundation.dart'; 4 | 5 | part 'following.freezed.dart'; 6 | 7 | part 'following.g.dart'; 8 | 9 | @freezed 10 | abstract class Following with _$Following { 11 | const factory Following({ 12 | required DateTime createdAt, 13 | UserFullModel? followee, 14 | required String followeeId, 15 | UserFullModel? follower, 16 | required String followerId, 17 | required String id, 18 | }) = _Following; 19 | 20 | factory Following.fromJson(Map json) => 21 | _$FollowingFromJson(json); 22 | } 23 | -------------------------------------------------------------------------------- /lib/apis/models/login_user.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:moekey/apis/models/user_full.dart'; 3 | import 'package:flutter/foundation.dart'; 4 | 5 | part 'login_user.freezed.dart'; 6 | 7 | part 'login_user.g.dart'; 8 | 9 | @freezed 10 | abstract class LoginUser with _$LoginUser { 11 | const factory LoginUser({ 12 | required String serverUrl, 13 | required String token, 14 | required UserFullModel userInfo, 15 | required String name, 16 | required String id, 17 | }) = _LoginUser; 18 | 19 | factory LoginUser.fromJson(Map json) => 20 | _$LoginUserFromJson(json); 21 | } 22 | -------------------------------------------------------------------------------- /lib/apis/models/translate.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | 4 | part 'translate.freezed.dart'; 5 | 6 | part 'translate.g.dart'; 7 | 8 | @unfreezed 9 | abstract class NoteTranslate with _$NoteTranslate { 10 | factory NoteTranslate({ 11 | required String sourceLang, 12 | required String text, 13 | @Default(true) bool loading, 14 | }) = _NoteTranslate; 15 | 16 | factory NoteTranslate.fromJson(Map json) => 17 | _$NoteTranslateFromJson(json); 18 | } 19 | -------------------------------------------------------------------------------- /lib/apis/models/user_full.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'note.dart'; 3 | import 'user_lite.dart'; 4 | 5 | part 'user_full.freezed.dart'; 6 | 7 | part 'user_full.g.dart'; 8 | 9 | @unfreezed 10 | abstract class UserFullModel with _$UserFullModel { 11 | factory UserFullModel({ 12 | String? avatarBlurhash, 13 | String? avatarUrl, 14 | String? bannerBlurhash, 15 | String? bannerUrl, 16 | String? birthday, 17 | required DateTime createdAt, 18 | String? description, 19 | String? email, 20 | @Default({}) Map emojis, 21 | required num followersCount, 22 | required num followingCount, 23 | String? host, 24 | required String id, 25 | @Default(false) bool isAdmin, 26 | required OnlineStatus onlineStatus, 27 | DateTime? updatedAt, 28 | required String username, 29 | String? name, 30 | @Default([]) List fields, 31 | List? avatarDecorations, 32 | String? ffVisibility, 33 | FollowVisibility? followersVisibility, 34 | FollowVisibility? followingVisibility, 35 | @Default([]) List pinnedNotes, 36 | @Default([]) List pinnedNotesIds, 37 | String? uri, 38 | String? url, 39 | required num notesCount, 40 | @Default(false) bool publicReactions, 41 | @Default(false) bool isFollowed, 42 | @Default(false) bool isFollowing, 43 | @Default(false) bool hasPendingFollowRequestFromYou, 44 | @Default(false) bool hasPendingFollowRequestToYou, 45 | @Default(false) bool isLocked, 46 | }) = _UserFullModel; 47 | 48 | factory UserFullModel.fromJson(Map json) => 49 | _$UserFullModelFromJson(json); 50 | } 51 | 52 | enum FollowVisibility { 53 | @JsonValue("followers") 54 | FOLLOWERS, 55 | @JsonValue("private") 56 | PRIVATE, 57 | @JsonValue("public") 58 | PUBLIC 59 | } 60 | -------------------------------------------------------------------------------- /lib/apis/models/user_lite.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | 4 | part 'user_lite.freezed.dart'; 5 | 6 | part 'user_lite.g.dart'; 7 | 8 | @freezed 9 | abstract class UserLiteModel with _$UserLiteModel { 10 | const factory UserLiteModel({ 11 | required String? avatarBlurhash, 12 | required List avatarDecorations, 13 | required String? avatarUrl, 14 | @Default([]) List badgeRoles, 15 | required Map emojis, 16 | required String? host, 17 | required String id, 18 | Instance? instance, 19 | @Default(false) bool isBot, 20 | @Default(false) bool isCat, 21 | required double? makeNotesFollowersOnlyBefore, 22 | required double? makeNotesHiddenBefore, 23 | required String? name, 24 | required OnlineStatus onlineStatus, 25 | @Default(false) bool requireSigninToViewContents, 26 | required String username, 27 | }) = _UserLiteModel; 28 | 29 | factory UserLiteModel.fromJson(Map json) => 30 | _$UserLiteModelFromJson(json); 31 | } 32 | 33 | extension UserLiteModelExtension on UserLiteModel { 34 | String getAtUserName() { 35 | return "@$username${host != null ? "@$host" : ""}"; 36 | } 37 | } 38 | 39 | @freezed 40 | abstract class AvatarDecoration with _$AvatarDecoration { 41 | const factory AvatarDecoration({ 42 | @Default(0) double angle, 43 | @Default(false) bool flipH, 44 | required String id, 45 | @Default(0) double offsetX, 46 | @Default(0) double offsetY, 47 | required String url, 48 | }) = _AvatarDecoration; 49 | 50 | factory AvatarDecoration.fromJson(Map json) => 51 | _$AvatarDecorationFromJson(json); 52 | } 53 | 54 | @freezed 55 | abstract class BadgeRole with _$BadgeRole { 56 | const factory BadgeRole({ 57 | required double displayOrder, 58 | required String? iconUrl, 59 | required String name, 60 | }) = _BadgeRole; 61 | 62 | factory BadgeRole.fromJson(Map json) => 63 | _$BadgeRoleFromJson(json); 64 | } 65 | 66 | @freezed 67 | abstract class Instance with _$Instance { 68 | const factory Instance({ 69 | required String? faviconUrl, 70 | required String? iconUrl, 71 | required String? name, 72 | required String? softwareName, 73 | required String? softwareVersion, 74 | required String? themeColor, 75 | }) = _Instance; 76 | 77 | factory Instance.fromJson(Map json) => 78 | _$InstanceFromJson(json); 79 | } 80 | 81 | enum OnlineStatus { 82 | @JsonValue("active") 83 | ACTIVE, 84 | @JsonValue("offline") 85 | OFFLINE, 86 | @JsonValue("online") 87 | ONLINE, 88 | @JsonValue("unknown") 89 | UNKNOWN 90 | } 91 | -------------------------------------------------------------------------------- /lib/apis/services/account_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:moekey/apis/models/me_detailed.dart'; 2 | import 'package:moekey/apis/models/notification.dart'; 3 | import 'package:moekey/apis/services/services.dart'; 4 | 5 | class AccountService extends MisskeyApiServices { 6 | AccountService({required super.client}); 7 | 8 | Future i() async { 9 | var data = await client.post("/i"); 10 | if (data != null) { 11 | return MeDetailed.fromJson(data); 12 | } 13 | return null; 14 | } 15 | 16 | Future> notificationsGrouped( 17 | {String? untilId}) async { 18 | var res = await client.post("/i/notifications-grouped", data: { 19 | "limit": 20, 20 | if (untilId != null) "untilId": untilId, 21 | }); 22 | if (res == null) { 23 | return []; 24 | } 25 | return List.from( 26 | res.map((e) => NotificationModel.fromJson(e))); 27 | } 28 | 29 | // i/update 30 | Future update({ 31 | Map? data, 32 | }) async { 33 | var res = await client.post("/i/update", data: data); 34 | return MeDetailed.fromJson(res); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/apis/services/app_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:moekey/apis/models/app.dart'; 2 | import 'package:moekey/apis/services/services.dart'; 3 | 4 | class AppService extends MisskeyApiServices { 5 | AppService({required super.client}); 6 | 7 | Future create() async { 8 | var data = await client.post( 9 | "/app/create", 10 | data: { 11 | "name": "MoeKey", 12 | "description": "app", 13 | "permission": [ 14 | "read:account", 15 | "write:account", 16 | "read:blocks", 17 | "write:blocks", 18 | "read:drive", 19 | "write:drive", 20 | "read:favorites", 21 | "write:favorites", 22 | "read:following", 23 | "write:following", 24 | "read:messaging", 25 | "write:messaging", 26 | "read:mutes", 27 | "write:mutes", 28 | "write:notes", 29 | "read:notifications", 30 | "write:notifications", 31 | "write:reactions", 32 | "write:votes", 33 | "read:pages", 34 | "write:pages", 35 | "write:page-likes", 36 | "read:page-likes", 37 | "write:gallery-likes", 38 | "read:gallery-likes", 39 | "write:clip-favorite", 40 | "read:clip-favorite", 41 | ] 42 | }, 43 | auth: false, 44 | ); 45 | if (data != null) { 46 | return AppModel.fromJson(data); 47 | } 48 | return null; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/apis/services/auth_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:moekey/apis/models/auth.dart'; 3 | import 'package:moekey/apis/services/services.dart'; 4 | 5 | class AuthService extends MisskeyApiServices { 6 | AuthService({required super.client}); 7 | 8 | Future sessionGenerate( 9 | {required String appSecret}) async { 10 | var data = await client.post( 11 | "/auth/session/generate", 12 | data: {"appSecret": appSecret}, 13 | auth: false, 14 | ); 15 | if (data != null) { 16 | return SessionGenerateModel.fromJson(data); 17 | } 18 | return null; 19 | } 20 | 21 | /// 22 | /// {"appSecret": appSecret, "token": token} 23 | Future?> sessionUserKey( 24 | {required String appSecret, required String token}) async { 25 | return await client.post>( 26 | "/auth/session/userkey", 27 | data: {"appSecret": appSecret, "token": token}, 28 | auth: false, 29 | options: Options( 30 | validateStatus: (status) { 31 | return status! < 500; 32 | }, 33 | ), 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/apis/services/following_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:moekey/apis/services/services.dart'; 2 | 3 | class FollowingService extends MisskeyApiServices { 4 | FollowingService({required super.client}); 5 | 6 | create({required String userId}) async { 7 | await client.post( 8 | "/following/create", 9 | data: { 10 | "userId": userId, 11 | }, 12 | ); 13 | } 14 | 15 | delete({required String userId}) async { 16 | await client.post( 17 | "/following/delete", 18 | data: { 19 | "userId": userId, 20 | }, 21 | ); 22 | } 23 | 24 | requestsCancel({required String userId}) async { 25 | await client.post( 26 | "/following/requests/cancel", 27 | data: { 28 | "userId": userId, 29 | }, 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/apis/services/hashtags_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:moekey/apis/services/services.dart'; 2 | 3 | class HashtagsService extends MisskeyApiServices { 4 | HashtagsService({required super.client}); 5 | 6 | search({String? query}) async { 7 | var list = await client 8 | .post("/hashtags/search", data: {"query": query, "limit": 30}); 9 | return List.from(list.map( 10 | (e) => e, 11 | )); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/apis/services/meta_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:moekey/apis/models/announcement.dart'; 2 | import 'package:moekey/apis/models/meta.dart'; 3 | import 'package:moekey/apis/services/services.dart'; 4 | 5 | import '../models/emojis.dart'; 6 | 7 | class MetaService extends MisskeyApiServices { 8 | MetaService({required super.client}); 9 | 10 | Future meta() async { 11 | var res = await client.post("/meta", data: {"detail": true}); 12 | if (res != null) { 13 | return MetaDetailedModel.fromJson(res); 14 | } 15 | return null; 16 | } 17 | 18 | Future> emojis() async { 19 | var data = await client.post("/emojis"); 20 | if (data == null) { 21 | return []; 22 | } 23 | return List.from( 24 | data["emojis"].map((x) => EmojiSimple.fromJson(x))); 25 | } 26 | 27 | Future> announcements( 28 | {int limit = 10, 29 | String? sinceId, 30 | String? untilId, 31 | bool isActive = true}) async { 32 | var data = await client.post("/announcements", data: { 33 | "limit": limit, 34 | if (sinceId != null) 'sinceId': sinceId, 35 | if (untilId != null) 'untilId': untilId, 36 | "isActive": isActive, 37 | }); 38 | if (data == null) { 39 | return []; 40 | } 41 | return List.from(data.map((x) => Announcement.fromJson(x))); 42 | } 43 | 44 | readAnnouncement({required String announcementId}) async { 45 | return client.post("/i/read-announcement", data: { 46 | "announcementId": announcementId, 47 | }); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/apis/services/services.dart: -------------------------------------------------------------------------------- 1 | import 'package:moekey/apis/dio.dart'; 2 | 3 | abstract class MisskeyApiServices { 4 | MisskeyApiServices({required this.client}); 5 | 6 | MisskeyApisHttpClient client; 7 | } 8 | -------------------------------------------------------------------------------- /lib/database/init_database.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:hive/hive.dart'; 4 | import 'package:path_provider/path_provider.dart'; 5 | 6 | Future initDatabase() async { 7 | // 初始化Hive 8 | // 文档 https://docs.hivedb.dev/ 9 | final appDocumentDirectory = await getApplicationSupportDirectory(); 10 | Hive.init(appDocumentDirectory.path); 11 | 12 | await Hive.openBox("preferences_string"); 13 | } 14 | 15 | class Preferences { 16 | static Box getPreferencesDatabase() { 17 | return Hive.box("preferences_string"); 18 | } 19 | 20 | static get(String key, {dynamic defaultValue}) { 21 | var value = getPreferencesDatabase().get(key); 22 | if (value == null) { 23 | return defaultValue; 24 | } 25 | return jsonDecode(value); 26 | } 27 | 28 | static set(String key, dynamic value) { 29 | getPreferencesDatabase().put(key, jsonEncode(value)); 30 | } 31 | } 32 | 33 | Preferences getPreferencesDatabase() { 34 | return Preferences(); 35 | } 36 | 37 | Future> openDatabase({ 38 | required String name, 39 | String? server, 40 | String? user, 41 | }) async { 42 | if (user != null) { 43 | name = "$user-$name"; 44 | } 45 | if (server != null) { 46 | var host = Uri.parse(server).host; 47 | name = "$host-$name"; 48 | } 49 | 50 | return Hive.openBox(name); 51 | } 52 | 53 | Future> openLazyDatabase({ 54 | required String name, 55 | String? server, 56 | String? user, 57 | }) async { 58 | if (user != null) { 59 | name = "$user-$name"; 60 | } 61 | if (server != null) { 62 | var host = Uri.parse(server).host; 63 | name = "$host-$name"; 64 | } 65 | return Hive.openLazyBox(name); 66 | } 67 | -------------------------------------------------------------------------------- /lib/database/instance.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:hive/hive.dart'; 4 | import 'package:moekey/apis/models/emojis.dart'; 5 | import 'package:moekey/apis/models/meta.dart'; 6 | import 'package:moekey/database/init_database.dart'; 7 | 8 | /// 实例信息 9 | class InstanceDatabase { 10 | /// 服务器地址 11 | String server; 12 | 13 | InstanceDatabase({required this.server}); 14 | 15 | Future> _getDatabase() { 16 | return openDatabase(name: "instance_data", server: server); 17 | } 18 | 19 | /// 设置元数据 20 | setMeta(MetaDetailedModel model) async { 21 | var db = await _getDatabase(); 22 | db.put("meta", jsonEncode(model.toJson())); 23 | } 24 | 25 | /// 获取元数据 26 | Future getMeta() async { 27 | var db = await _getDatabase(); 28 | String? res = db.get("meta"); 29 | if (res == null) { 30 | return null; 31 | } 32 | return MetaDetailedModel.fromJson(jsonDecode(res)); 33 | } 34 | 35 | /// 设置表情 36 | setEmojis(List models) async { 37 | var db = await _getDatabase(); 38 | db.put("emojis", jsonEncode(models)); 39 | } 40 | 41 | /// 获取表情 42 | Future?> getEmojis() async { 43 | var db = await _getDatabase(); 44 | var res = db.get("emojis"); 45 | if (res == null) { 46 | return null; 47 | } 48 | var emojis = jsonDecode(res); 49 | return List.from( 50 | emojis.map( 51 | (x) => EmojiSimple.fromJson(x), 52 | ), 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/database/link_preview.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:hive/hive.dart'; 4 | 5 | import 'init_database.dart'; 6 | import '../apis/models/note.dart' as note; 7 | 8 | class LinkPreviewDatabase { 9 | LazyBox? _box; 10 | 11 | Future> _getDatabase() async { 12 | _box ??= await openLazyDatabase(name: "link_preview"); 13 | return _box!; 14 | } 15 | 16 | put(String src, note.LinkPreview link) async { 17 | var db = await _getDatabase(); 18 | await db.put(src, jsonEncode(link.toJson())); 19 | } 20 | 21 | Future get(String src) async { 22 | var db = await _getDatabase(); 23 | var res = await db.get(src); 24 | if (res != null) { 25 | return note.LinkPreview.fromJson(jsonDecode(res)); 26 | } 27 | return null; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/database/notes.dart: -------------------------------------------------------------------------------- 1 | import 'package:hive/hive.dart'; 2 | import 'package:moekey/apis/models/note.dart'; 3 | 4 | import 'init_database.dart'; 5 | 6 | class NotesDatabase { 7 | /// 服务器地址 8 | String server; 9 | LazyBox? _box; 10 | 11 | NotesDatabase({required this.server}); 12 | 13 | Future _getDatabase() async { 14 | _box ??= 15 | await openLazyDatabase(name: "notes_cache", server: server); 16 | return _box!; 17 | } 18 | 19 | put(String noteId, NoteModel note) async { 20 | var db = await _getDatabase(); 21 | await db.put(noteId, note.toJson()); 22 | } 23 | 24 | Future get(String noteId) async { 25 | var db = await _getDatabase(); 26 | var res = await db.get(noteId); 27 | if (res != null) { 28 | return NoteModel.fromJson(res); 29 | } 30 | return null; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/database/timeline.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:hive/hive.dart'; 5 | import 'package:moekey/apis/models/note.dart'; 6 | import 'package:moekey/database/init_database.dart'; 7 | 8 | class TimelineDatabase { 9 | /// 服务器地址 10 | String server; 11 | 12 | /// 用户ID 13 | String userId; 14 | 15 | TimelineDatabase({required this.server, required this.userId}); 16 | 17 | Future> _getDatabase() async { 18 | return openDatabase(name: "timeline", server: server, user: userId); 19 | } 20 | 21 | setTimeline(String name, List list) async { 22 | var db = await _getDatabase(); 23 | db.put( 24 | "$name-timeline", 25 | await compute( 26 | (list) => jsonEncode(List.from(list.map((e) => e.toJson()))), 27 | list)); 28 | } 29 | 30 | cleanTimeline(String name) async { 31 | var db = await _getDatabase(); 32 | db.delete("$name-timeline"); 33 | } 34 | 35 | Future?> getTimeline(String name) async { 36 | var db = await _getDatabase(); 37 | var res = db.get("$name-timeline"); 38 | if (res == null) { 39 | return null; 40 | } 41 | List list = await compute((message) { 42 | return jsonDecode(message); 43 | }, res); 44 | return List.from(list.map((e) => NoteModel.fromJson(e))); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/database/users.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:hive/hive.dart'; 5 | import 'package:moekey/apis/models/user_full.dart'; 6 | import 'package:moekey/database/init_database.dart'; 7 | 8 | class UsersDatabase { 9 | /// 服务器地址 10 | String server; 11 | 12 | /// 用户ID 13 | String userId; 14 | 15 | UsersDatabase({required this.server, required this.userId}); 16 | 17 | Future> _getDatabase() async { 18 | return openLazyDatabase(name: "users_cache"); 19 | } 20 | 21 | put(UserFullModel userFullModel) async { 22 | var db = await _getDatabase(); 23 | compute((message) { 24 | var json = jsonEncode(message); 25 | db.put(message.id, json); 26 | }, userFullModel); 27 | } 28 | 29 | get(String id) async { 30 | var db = await _getDatabase(); 31 | return db.get(id); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/hook/useExtendedPageController.dart: -------------------------------------------------------------------------------- 1 | import 'package:extended_image/extended_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/widgets.dart'; 4 | import 'package:flutter_hooks/flutter_hooks.dart'; 5 | 6 | ExtendedPageController useExtendedPageController({ 7 | int initialPage = 0, 8 | bool keepPage = true, 9 | double pageSpacing = 0.0, 10 | bool shouldIgnorePointerWhenScrolling = false, 11 | double viewportFraction = 1.0, 12 | List? keys, 13 | }) { 14 | return use( 15 | _ScrollControllerHook( 16 | initialPage: initialPage, 17 | keepPage: keepPage, 18 | pageSpacing: pageSpacing, 19 | keys: keys, 20 | shouldIgnorePointerWhenScrolling: shouldIgnorePointerWhenScrolling, 21 | viewportFraction: viewportFraction), 22 | ); 23 | } 24 | 25 | class _ScrollControllerHook extends Hook { 26 | const _ScrollControllerHook({ 27 | required this.initialPage, 28 | required this.keepPage, 29 | required this.shouldIgnorePointerWhenScrolling, 30 | required this.pageSpacing, 31 | required this.viewportFraction, 32 | super.keys, 33 | }); 34 | 35 | final int initialPage; 36 | final bool keepPage; 37 | final bool shouldIgnorePointerWhenScrolling; 38 | final double pageSpacing; 39 | final double viewportFraction; 40 | 41 | @override 42 | HookState> 43 | createState() => _ScrollControllerHookState(); 44 | } 45 | 46 | class _ScrollControllerHookState 47 | extends HookState { 48 | late final controller = ExtendedPageController( 49 | initialPage: hook.initialPage, 50 | keepPage: hook.keepPage, 51 | pageSpacing: hook.pageSpacing, 52 | shouldIgnorePointerWhenScrolling: hook.shouldIgnorePointerWhenScrolling, 53 | viewportFraction: hook.viewportFraction); 54 | 55 | @override 56 | ExtendedPageController build(BuildContext context) => controller; 57 | 58 | @override 59 | void dispose() => controller.dispose(); 60 | 61 | @override 62 | String get debugLabel => 'useExtendedPageController'; 63 | } 64 | -------------------------------------------------------------------------------- /lib/hook/useTimelineScrollController.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | import 'package:flutter_hooks/flutter_hooks.dart'; 4 | 5 | import '../widgets/timeline_listview.dart'; 6 | 7 | TimelineScrollController useTimelineScrollController({ 8 | bool keepScrollOffset = true, 9 | double initialScrollOffset = 0.0, 10 | List? keys, 11 | }) { 12 | return use( 13 | _ScrollControllerHook( 14 | keepScrollOffset: keepScrollOffset, 15 | initialScrollOffset: initialScrollOffset, 16 | ), 17 | ); 18 | } 19 | 20 | class _ScrollControllerHook extends Hook { 21 | const _ScrollControllerHook({ 22 | required this.keepScrollOffset, 23 | required this.initialScrollOffset, 24 | }); 25 | 26 | final bool keepScrollOffset; 27 | final double initialScrollOffset; 28 | 29 | @override 30 | HookState> 31 | createState() => _ScrollControllerHookState(); 32 | } 33 | 34 | class _ScrollControllerHookState 35 | extends HookState { 36 | late final controller = TimelineScrollController( 37 | keepScrollOffset: hook.keepScrollOffset, 38 | initialScrollOffset: hook.initialScrollOffset); 39 | 40 | @override 41 | TimelineScrollController build(BuildContext context) => controller; 42 | 43 | @override 44 | void dispose() => controller.dispose(); 45 | 46 | @override 47 | String get debugLabel => 'useExtendedPageController'; 48 | } 49 | -------------------------------------------------------------------------------- /lib/hook/use_mk_refresh_load_list_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:flutter_hooks/flutter_hooks.dart'; 3 | 4 | import '../widgets/mk_refresh_load.dart'; 5 | 6 | MkRefreshLoadListController useMkRefreshLoadListController() { 7 | return use(_MkRefreshLoadListControllerHook()); 8 | } 9 | 10 | class _MkRefreshLoadListControllerHook 11 | extends Hook { 12 | @override 13 | HookState> 14 | createState() { 15 | return _MkRefreshLoadListControllerHookState(); 16 | } 17 | } 18 | 19 | class _MkRefreshLoadListControllerHookState extends HookState< 20 | MkRefreshLoadListController, _MkRefreshLoadListControllerHook> { 21 | late final controller = MkRefreshLoadListController(); 22 | 23 | @override 24 | MkRefreshLoadListController build(BuildContext context) { 25 | return controller; 26 | } 27 | 28 | @override 29 | void dispose() { 30 | controller.dispose(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/logger.dart: -------------------------------------------------------------------------------- 1 | import 'package:logger/logger.dart'; 2 | 3 | var logger = Logger(); 4 | -------------------------------------------------------------------------------- /lib/pages/clips/clips.dart: -------------------------------------------------------------------------------- 1 | import 'package:moekey/status/misskey_api.dart'; 2 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 3 | 4 | import '../../apis/models/clips.dart'; 5 | import '../../apis/models/note.dart'; 6 | import '../../logger.dart'; 7 | 8 | part 'clips.g.dart'; 9 | 10 | @Riverpod(keepAlive: true) 11 | class Clips extends _$Clips { 12 | @override 13 | FutureOr> build() async { 14 | return clipsList(); 15 | } 16 | 17 | Future> clipsList() async { 18 | var apis = ref.watch(misskeyApisProvider); 19 | 20 | var list = await apis.clips.list(); 21 | // 将list反序排列 22 | list = list.reversed.toList(); 23 | return list; 24 | } 25 | } 26 | 27 | class ClipsNoteListState { 28 | List list = []; 29 | var haveMore = true; 30 | } 31 | 32 | @riverpod 33 | class ClipsNotesList extends _$ClipsNotesList { 34 | @override 35 | FutureOr build(String clipId) async { 36 | var state = ClipsNoteListState(); 37 | state.list = await clipsNotesList(clipId: clipId); 38 | return state; 39 | } 40 | 41 | Future> clipsNotesList( 42 | {required String clipId, int limit = 10, String? untilId}) async { 43 | try { 44 | var apis = ref.read(misskeyApisProvider); 45 | return await apis.clips 46 | .notes(clipId: clipId, untilId: untilId, limit: limit); 47 | } finally {} 48 | } 49 | 50 | load() async { 51 | if (state.isLoading) return; 52 | state = const AsyncValue.loading(); 53 | var model = state.valueOrNull ?? ClipsNoteListState(); 54 | try { 55 | var untilId = model.list.lastOrNull?.id; 56 | var list = await clipsNotesList(clipId: clipId, untilId: untilId); 57 | 58 | if (list.isEmpty) { 59 | model.haveMore = false; 60 | } else { 61 | model.list += list; 62 | } 63 | } catch (e) { 64 | logger.e(e); 65 | } 66 | state = AsyncData(model); 67 | } 68 | } 69 | 70 | @riverpod 71 | class ClipsShow extends _$ClipsShow { 72 | @override 73 | FutureOr build(String clipId) async { 74 | try { 75 | var apis = ref.watch(misskeyApisProvider); 76 | 77 | return await apis.clips.show(clipId: clipId); 78 | } finally {} 79 | } 80 | } 81 | 82 | @riverpod 83 | class ClipsMyFavorites extends _$ClipsMyFavorites { 84 | @override 85 | FutureOr> build() async { 86 | return clipsMyFavorites(); 87 | } 88 | 89 | Future> clipsMyFavorites() async { 90 | var apis = ref.watch(misskeyApisProvider); 91 | return apis.clips.myFavorites(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /lib/pages/clips/clips_collection.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_hooks/flutter_hooks.dart'; 5 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 6 | 7 | import '../../apis/models/clips.dart'; 8 | import '../../utils/get_padding_note.dart'; 9 | import '../../widgets/clips/clips_folder.dart'; 10 | import '../../widgets/loading_weight.dart'; 11 | import 'clips.dart'; 12 | 13 | class ClipsCollection extends HookConsumerWidget { 14 | const ClipsCollection({super.key}); 15 | 16 | @override 17 | Widget build(BuildContext context, WidgetRef ref) { 18 | var queryPadding = MediaQuery.of(context).padding; 19 | var res = ref.watch(clipsMyFavoritesProvider); 20 | var scrollController = useScrollController(); 21 | 22 | return RefreshIndicator.adaptive( 23 | // 通知刷新指示器 24 | onRefresh: () => ref.refresh(clipsMyFavoritesProvider.future), 25 | edgeOffset: queryPadding.top, 26 | child: ScrollConfiguration( 27 | // 设置滑动配置,允许使用触摸和鼠标进行滑动 28 | behavior: ScrollConfiguration.of(context).copyWith(dragDevices: { 29 | PointerDeviceKind.touch, 30 | PointerDeviceKind.mouse, 31 | }), 32 | child: LoadingAndEmpty( 33 | loading: res.isLoading, 34 | empty: res.valueOrNull?.isEmpty ?? true, 35 | refresh: () => ref.refresh(clipsMyFavoritesProvider.future), 36 | child: LayoutBuilder( 37 | builder: (context, constraints) { 38 | var padding = getPaddingForNote(constraints); 39 | return ListView.builder( 40 | physics: const AlwaysScrollableScrollPhysics(), 41 | itemCount: res.valueOrNull!.length, 42 | padding: EdgeInsets.only( 43 | left: padding, 44 | right: padding, 45 | top: queryPadding.top, 46 | bottom: queryPadding.bottom), 47 | controller: scrollController, 48 | itemBuilder: (BuildContext context, int index) { 49 | return buildClipsListItem(res, index); 50 | }, 51 | ); 52 | }, 53 | )), 54 | )); 55 | } 56 | 57 | Widget buildClipsListItem(AsyncValue> clipsList, int index) { 58 | return Column( 59 | children: [ 60 | ClipsFolder( 61 | data: clipsList.valueOrNull![index], 62 | ), 63 | const SizedBox( 64 | height: 20, 65 | ) 66 | ], 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /lib/pages/drive/drive_info.dart: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lib/pages/drive/drive_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | 4 | import '../../widgets/driver/driver_list.dart'; 5 | 6 | class DrivePage extends HookConsumerWidget { 7 | const DrivePage({super.key}); 8 | 9 | @override 10 | Widget build(BuildContext context, WidgetRef ref) { 11 | return const Scaffold( 12 | body: DriverList(), 13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/pages/explore/explore.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_hooks/flutter_hooks.dart'; 3 | import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; 4 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 5 | import 'package:moekey/pages/explore/hot.dart'; 6 | import 'package:moekey/pages/explore/users.dart'; 7 | import 'package:moekey/status/themes.dart'; 8 | import 'package:moekey/widgets/mk_tabbar_list.dart'; 9 | 10 | import '../../generated/l10n.dart'; 11 | 12 | class ExplorePage extends HookConsumerWidget { 13 | const ExplorePage({super.key}); 14 | 15 | @override 16 | Widget build(BuildContext context, WidgetRef ref) { 17 | var themes = ref.watch(themeColorsProvider); 18 | var tabs = [ 19 | Tab( 20 | child: Row( 21 | children: [ 22 | const Icon( 23 | TablerIcons.bolt, 24 | size: 14, 25 | ), 26 | Text(S.current.exploreHot, style: const TextStyle(fontSize: 12)), 27 | ], 28 | ), 29 | ), 30 | Tab( 31 | child: Row( 32 | children: [ 33 | const Icon(TablerIcons.users, size: 14), 34 | Text(S.current.exploreUsers, style: const TextStyle(fontSize: 12)), 35 | ], 36 | ), 37 | ) 38 | ]; 39 | 40 | var tabController = useTabController(initialLength: tabs.length); 41 | 42 | var currentIndex = useState(0); 43 | tabController.addListener(() { 44 | currentIndex.value = tabController.index; 45 | }); 46 | return MkTabBarRefreshScroll( 47 | items: [ 48 | MkTabBarItem( 49 | label: Tab( 50 | child: Row( 51 | children: [ 52 | const Icon( 53 | TablerIcons.bolt, 54 | size: 14, 55 | ), 56 | Text(S.current.exploreHot, 57 | style: const TextStyle(fontSize: 12)), 58 | ], 59 | ), 60 | ), 61 | child: const ExploreHotPage(), 62 | ), 63 | MkTabBarItem( 64 | label: Tab( 65 | child: Row( 66 | children: [ 67 | const Icon(TablerIcons.users, size: 14), 68 | Text(S.current.exploreUsers, 69 | style: const TextStyle(fontSize: 12)), 70 | ], 71 | ), 72 | ), 73 | child: const ExploreUsersPage(), 74 | ) 75 | ], 76 | leading: Padding( 77 | padding: const EdgeInsets.symmetric(horizontal: 8), 78 | child: Row( 79 | mainAxisSize: MainAxisSize.min, 80 | children: [ 81 | Icon(TablerIcons.hash, color: themes.fgColor, size: 18), 82 | const SizedBox(width: 4), 83 | Text( 84 | S.current.explore, 85 | ) 86 | ], 87 | ), 88 | ), 89 | trailing: const SizedBox( 90 | width: 100, 91 | ), 92 | ); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /lib/pages/hashtag/hashtag_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | import 'package:moekey/apis/models/note.dart'; 4 | import 'package:moekey/hook/use_mk_refresh_load_list_controller.dart'; 5 | import 'package:moekey/widgets/mk_header.dart'; 6 | import 'package:moekey/widgets/mk_scaffold.dart'; 7 | import 'package:moekey/widgets/notes/note_pagination_list.dart'; 8 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 9 | 10 | import '../../status/misskey_api.dart'; 11 | 12 | part 'hashtag_page.g.dart'; 13 | 14 | class HashtagPage extends HookConsumerWidget { 15 | const HashtagPage({super.key, required this.name}); 16 | 17 | final String name; 18 | 19 | @override 20 | Widget build(BuildContext context, WidgetRef ref) { 21 | var model = hashTagPageProvider(name); 22 | var state = ref.watch(model); 23 | var data = state.valueOrNull; 24 | var controller = useMkRefreshLoadListController(); 25 | return MkScaffold( 26 | header: MkAppbar( 27 | showBack: true, 28 | content: GestureDetector( 29 | onTap: () { 30 | controller.refresh(); 31 | }, 32 | child: Text( 33 | "#$name", 34 | maxLines: 1, 35 | overflow: TextOverflow.ellipsis, 36 | ), 37 | ), 38 | ), 39 | body: Center( 40 | child: MkPaginationNoteList( 41 | onLoad: () => ref.read(model.notifier).load(), 42 | onRefresh: () => ref.refresh(model.future), 43 | hasMore: data?.hasMore ?? true, 44 | items: data?.list, 45 | controller: controller, 46 | ), 47 | ), 48 | ); 49 | } 50 | } 51 | 52 | @riverpod 53 | class HashTagPage extends _$HashTagPage { 54 | @override 55 | FutureOr build(String tag) async { 56 | var model = NoteListModel(); 57 | model.list = await notes(); 58 | return model; 59 | } 60 | 61 | Future> notes({String? untilId}) async { 62 | var apis = ref.read(misskeyApisProvider); 63 | var list = await apis.notes.searchByTag(tag: tag, untilId: untilId); 64 | return list; 65 | } 66 | 67 | load() async { 68 | if (state.isLoading) return; 69 | state = const AsyncValue.loading(); 70 | var model = state.valueOrNull ?? NoteListModel(); 71 | try { 72 | String? untilId; 73 | if (state.valueOrNull?.list.isNotEmpty ?? false) { 74 | untilId = state.valueOrNull?.list.last.id; 75 | } 76 | List notesList = await notes(untilId: untilId); 77 | 78 | model.list += notesList; 79 | if (notesList.isEmpty) { 80 | model.hasMore = false; 81 | } 82 | } finally { 83 | state = AsyncData(model); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /lib/pages/home/home_page_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; 3 | import 'package:moekey/status/mk_tabbar_refresh_scroll_state.dart'; 4 | import 'package:moekey/widgets/mk_tabbar_list.dart'; 5 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 6 | 7 | import '../../generated/l10n.dart'; 8 | import '../../router/router.dart'; 9 | 10 | part 'home_page_state.g.dart'; 11 | 12 | @Riverpod(keepAlive: true) 13 | class HomePageState extends _$HomePageState { 14 | @override 15 | HomeState build() { 16 | return HomeState(); 17 | } 18 | 19 | changePage(String id) { 20 | for (var element in state.navItemList) { 21 | if (element["id"] != null) { 22 | if (id == element["id"]) { 23 | var currPath = ref.read(routerProvider).state.name; 24 | if (element['id'] == currPath) { 25 | if (element.containsKey("onTop")) { 26 | element["onTop"]( 27 | key: ref.read(mkTabBarRefreshScrollStatusProvider(id))); 28 | } 29 | } else { 30 | var router = ref.watch(routerProvider); 31 | router.goNamed(element["id"]!); 32 | } 33 | } 34 | } 35 | } 36 | } 37 | } 38 | 39 | class HomeState { 40 | final List> navItemList = [ 41 | { 42 | "icon": TablerIcons.home, 43 | "label": S.current.timeline, 44 | "id": "timeline", 45 | "onTop": ({Key? key}) => { 46 | (key as GlobalKey) 47 | .currentState 48 | ?.refresh() 49 | } 50 | }, 51 | { 52 | "icon": TablerIcons.bell, 53 | "label": S.current.notifications, 54 | "id": "notifications", 55 | "onTop": ({Key? key}) => { 56 | (key as GlobalKey) 57 | .currentState 58 | ?.refresh() 59 | } 60 | }, 61 | { 62 | "icon": TablerIcons.paperclip, 63 | "label": S.current.clips, 64 | "id": "clips", 65 | }, 66 | { 67 | "icon": TablerIcons.cloud, 68 | "label": S.current.drive, 69 | "id": "drives", 70 | }, 71 | {"line": true}, 72 | { 73 | "icon": TablerIcons.hash, 74 | "label": S.current.explore, 75 | "id": "explore", 76 | }, 77 | { 78 | "icon": TablerIcons.speakerphone, 79 | "label": S.current.announcements, 80 | "id": "announcements", 81 | }, 82 | { 83 | "icon": TablerIcons.search, 84 | "label": S.current.search, 85 | "id": "search", 86 | }, 87 | {"line": true}, 88 | { 89 | "icon": TablerIcons.grid_dots, 90 | "label": S.current.more, 91 | "id": "more", 92 | }, 93 | ]; 94 | } 95 | -------------------------------------------------------------------------------- /lib/pages/notifications/notifications_mentions_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | import 'package:moekey/status/notifications.dart'; 4 | import '../../widgets/notes/note_pagination_list.dart'; 5 | 6 | class MentionsList extends HookConsumerWidget { 7 | const MentionsList({ 8 | super.key, 9 | this.specified = false, 10 | this.padding = EdgeInsets.zero, 11 | }); 12 | 13 | final bool specified; 14 | final EdgeInsets padding; 15 | 16 | @override 17 | Widget build(BuildContext context, WidgetRef ref) { 18 | var dataProvider = mentionsNotificationsProvider(specified: specified); 19 | var data = ref.watch(dataProvider); 20 | return MkPaginationNoteList( 21 | padding: padding, 22 | onLoad: () => ref.read(dataProvider.notifier).loadMore(), 23 | onRefresh: () => ref.refresh(dataProvider.future), 24 | items: data.valueOrNull?.list, 25 | hasMore: data.valueOrNull?.hasMore, 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/pages/search/search_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; 3 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 4 | import 'package:moekey/pages/search/notes_search.dart'; 5 | import 'package:moekey/pages/search/user_search.dart'; 6 | import 'package:moekey/status/themes.dart'; 7 | import 'package:moekey/widgets/mk_tabbar_list.dart'; 8 | 9 | import '../../generated/l10n.dart'; 10 | 11 | class SearchPage extends HookConsumerWidget { 12 | const SearchPage({super.key}); 13 | 14 | @override 15 | Widget build(BuildContext context, WidgetRef ref) { 16 | var themes = ref.watch(themeColorsProvider); 17 | return MkTabBarRefreshScroll( 18 | items: [ 19 | MkTabBarItem( 20 | label: Tab( 21 | child: Row( 22 | children: [ 23 | Icon( 24 | TablerIcons.pencil, 25 | size: 14, 26 | ), 27 | Text(S.current.notes, style: TextStyle(fontSize: 12)), 28 | ], 29 | ), 30 | ), 31 | child: NotesSearchPage()), 32 | MkTabBarItem( 33 | label: Tab( 34 | child: Row( 35 | children: [ 36 | Icon( 37 | TablerIcons.users, 38 | size: 14, 39 | ), 40 | Text(S.current.user, style: TextStyle(fontSize: 12)), 41 | ], 42 | ), 43 | ), 44 | child: UserSearchPage()), 45 | ], 46 | leading: Padding( 47 | padding: const EdgeInsets.symmetric(horizontal: 8), 48 | child: Row( 49 | mainAxisSize: MainAxisSize.min, 50 | children: [ 51 | Icon( 52 | TablerIcons.search, 53 | size: 18, 54 | color: themes.fgColor, 55 | ), 56 | const SizedBox( 57 | width: 8, 58 | ), 59 | Text(S.current.search) 60 | ], 61 | ), 62 | ), 63 | offset: 100, 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/pages/settings/member_info_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 2 | 3 | import '../../apis/models/me_detailed.dart'; 4 | import '../../status/me_detailed.dart'; 5 | import '../../status/misskey_api.dart'; 6 | 7 | part 'member_info_state.g.dart'; 8 | 9 | class MemberInfoStateModel { 10 | MeDetailed originalUser; 11 | MeDetailed user; 12 | 13 | MemberInfoStateModel(this.originalUser, this.user); 14 | } 15 | 16 | @riverpod 17 | class MemberInfoState extends _$MemberInfoState { 18 | @override 19 | FutureOr build() async { 20 | var meDetail = await ref.watch(currentMeDetailedProvider.future); 21 | return MemberInfoStateModel(meDetail!, meDetail); 22 | } 23 | 24 | void setBanner(String id) { 25 | updateUser(state.value!.user.copyWith(bannerId: id)); 26 | updateApi({ 27 | "bannerId": id, 28 | }); 29 | } 30 | 31 | void setAvatar(String id) { 32 | updateUser(state.value!.user.copyWith(avatarId: id)); 33 | updateApi({ 34 | "avatarId": id, 35 | }); 36 | } 37 | 38 | void updateUser(MeDetailed user) { 39 | // 加入一个延迟,避免在更新时直接使用state.value 40 | // 导致状态不一致的问题 41 | Future(() { 42 | state = AsyncData(MemberInfoStateModel(state.value!.originalUser, user)); 43 | }); 44 | } 45 | 46 | Future updateApi(Map data) async { 47 | var api = ref.watch(misskeyApisProvider); 48 | // 如果字段的value是空字符串,则设置为null 49 | data.forEach((key, value) { 50 | if (value == "") { 51 | data[key] = null; 52 | } 53 | }); 54 | var res = await api.account.update(data: data); 55 | state = AsyncData(MemberInfoStateModel(res, res)); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/pages/settings/router.dart: -------------------------------------------------------------------------------- 1 | import 'package:go_router/go_router.dart'; 2 | import 'package:moekey/pages/settings/account_manager/account_manager_page.dart'; 3 | import 'package:moekey/pages/settings/profile/profile.dart'; 4 | import 'package:moekey/pages/settings/settings_page.dart'; 5 | import 'package:moekey/pages/settings/two_panel_layout.dart'; 6 | 7 | var settingsRouter = ShellRoute( 8 | builder: (context, status, child) => SettingsTwoPanelLayout( 9 | leftPanel: SettingBodyWide(), 10 | rightPanel: child, 11 | ), 12 | routes: [ 13 | GoRoute( 14 | name: "settings", 15 | path: "/settings", 16 | builder: (_, __) => const SettingBodyNarrow(), 17 | routes: [ 18 | StatefulShellRoute.indexedStack( 19 | builder: (context, status, child) => child, 20 | branches: [ 21 | StatefulShellBranch( 22 | routes: [ 23 | GoRoute( 24 | path: 'account_manager', 25 | name: "settingsAccountManager", 26 | builder: (_, __) { 27 | return AccountManagerPage(); 28 | }, 29 | ), 30 | ], 31 | ), 32 | StatefulShellBranch( 33 | routes: [ 34 | GoRoute( 35 | path: 'profile', 36 | name: "settingProfile", 37 | builder: (_, __) { 38 | return SettingsProfile(); 39 | }, 40 | ), 41 | ], 42 | ), 43 | ], 44 | ) 45 | ], 46 | ) 47 | ], 48 | ); 49 | -------------------------------------------------------------------------------- /lib/pages/settings/test/test_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:go_router/go_router.dart'; 3 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 4 | import 'package:moekey/widgets/mk_scaffold.dart'; 5 | 6 | class SettingsTestPage extends HookConsumerWidget { 7 | const SettingsTestPage({super.key, required this.text}); 8 | 9 | final String text; 10 | 11 | @override 12 | Widget build(BuildContext context, WidgetRef ref) { 13 | return MkScaffold( 14 | body: ListView( 15 | children: [ 16 | ListTile( 17 | title: Text(text), 18 | ), 19 | FilledButton(onPressed: () => context.pop(), child: Text("back")) 20 | ], 21 | ), 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/pages/settings/two_panel_layout.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; 3 | import 'package:go_router/go_router.dart'; 4 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 5 | import 'package:moekey/main.dart'; 6 | import 'package:moekey/widgets/mk_header.dart'; 7 | import 'package:moekey/widgets/mk_scaffold.dart'; 8 | 9 | import '../../generated/l10n.dart'; 10 | import '../../status/themes.dart'; 11 | 12 | class SettingsTwoPanelLayout extends HookConsumerWidget { 13 | const SettingsTwoPanelLayout({ 14 | super.key, 15 | required this.leftPanel, 16 | required this.rightPanel, 17 | }); 18 | 19 | final Widget leftPanel; 20 | final Widget rightPanel; 21 | 22 | @override 23 | Widget build(BuildContext context, WidgetRef ref) { 24 | var isWide = WindowSize.of(context)!.isWide; 25 | var themes = ref.watch(themeColorsProvider); 26 | var currentId = GoRouter.of(context).state.name; 27 | return LayoutBuilder( 28 | builder: (context, constraints) { 29 | return MkScaffold( 30 | header: MkAppbar( 31 | isSmallLeadingCenter: constraints.maxWidth < 500, 32 | showBack: !isWide && currentId != 'settings', 33 | leading: Row( 34 | mainAxisSize: MainAxisSize.min, 35 | children: [ 36 | Icon( 37 | TablerIcons.settings, 38 | size: 18, 39 | color: themes.fgColor, 40 | ), 41 | const SizedBox( 42 | width: 8, 43 | ), 44 | Text(S.current.settings), 45 | ], 46 | ), 47 | ), 48 | body: Row( 49 | children: [ 50 | if (isWide) 51 | SizedBox( 52 | width: 300, 53 | child: leftPanel, 54 | ), 55 | Expanded( 56 | child: rightPanel, 57 | ), 58 | ], 59 | ), 60 | ); 61 | }, 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/pages/splash_page/splash_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_hooks/flutter_hooks.dart'; 5 | import 'package:go_router/go_router.dart'; 6 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 7 | import 'package:media_kit/media_kit.dart'; 8 | import 'package:moekey/status/apis.dart'; 9 | 10 | import '../../database/init_database.dart'; 11 | import '../../status/server.dart'; 12 | import '../../status/websocket.dart'; 13 | import '../../widgets/loading_weight.dart'; 14 | 15 | class HttpProxy extends HttpOverrides { 16 | String proxyServer = ""; 17 | 18 | @override 19 | String findProxyFromEnvironment(Uri url, Map? environment) { 20 | // if (kDebugMode) { 21 | // proxyServer = "127.0.0.1:7890"; 22 | // } 23 | if (proxyServer == "") { 24 | return super.findProxyFromEnvironment(url, environment); 25 | } 26 | environment ??= {}; 27 | environment['http_proxy'] = proxyServer; 28 | environment['https_proxy'] = proxyServer; 29 | return super.findProxyFromEnvironment(url, environment); 30 | } 31 | } 32 | 33 | Future initApp(BuildContext context, WidgetRef ref) async { 34 | WidgetsFlutterBinding.ensureInitialized(); 35 | 36 | // 视频初始化 37 | MediaKit.ensureInitialized(); 38 | 39 | // 代理配置 40 | HttpOverrides.global = HttpProxy(); 41 | 42 | // 初始化数据库 43 | await initDatabase(); 44 | 45 | var user = ref.read(currentLoginUserProvider); 46 | // 启动webSocket 47 | ref.read(moekeyGlobalEventProvider); 48 | ref.read(moekeyMainChannelProvider); 49 | if (user != null) { 50 | if (context.mounted) { 51 | await ref.read(instanceMetaProvider.future); 52 | if (!context.mounted) { 53 | return null; 54 | } 55 | context.replace("/timeline"); 56 | return null; 57 | } 58 | } 59 | if (context.mounted) { 60 | context.replace("/login"); 61 | } 62 | } 63 | 64 | class SplashPage extends HookConsumerWidget { 65 | const SplashPage({super.key}); 66 | 67 | @override 68 | Widget build(BuildContext context, WidgetRef ref) { 69 | var isLaunch = useState(false); 70 | useEffect(() { 71 | initApp(context, ref).then((value) { 72 | isLaunch.value = true; 73 | }); 74 | return null; 75 | }, const []); 76 | return const Scaffold( 77 | body: LoadingWidget(), 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/pages/timeline/timeline_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | import 'package:moekey/status/timeline.dart'; 4 | import 'package:moekey/widgets/notes/note_pagination_list.dart'; 5 | 6 | class TimeLineListPage extends HookConsumerWidget { 7 | const TimeLineListPage({super.key, required this.api, this.controller}); 8 | 9 | final ScrollController? controller; 10 | final String api; 11 | 12 | @override 13 | Widget build(BuildContext context, WidgetRef ref) { 14 | var dataProvider = timelineProvider(api: api); 15 | var data = ref.watch(dataProvider); 16 | return MkPaginationNoteList( 17 | onLoad: () => ref.read(dataProvider.notifier).load(), 18 | hasMore: data.valueOrNull?.hasMore, 19 | items: data.valueOrNull?.list, 20 | onRefresh: () async { 21 | await ref.read(dataProvider.notifier).cleanCache(); 22 | return await ref.refresh(dataProvider.future); 23 | }, 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/pages/user_widgets/widgets_list/state.dart: -------------------------------------------------------------------------------- 1 | class WidgetsListState { 2 | WidgetsListState() { 3 | ///Initialize variables 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /lib/pages/user_widgets/widgets_list/view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | import 'package:moekey/status/themes.dart'; 4 | 5 | import '../../../generated/l10n.dart'; 6 | 7 | class WidgetsListPage extends ConsumerWidget { 8 | const WidgetsListPage({super.key}); 9 | 10 | @override 11 | Widget build(BuildContext context, WidgetRef ref) { 12 | var theme = ref.watch(themeColorsProvider); 13 | return SizedBox( 14 | width: 350, 15 | height: double.infinity, 16 | child: DecoratedBox( 17 | decoration: BoxDecoration( 18 | border: Border( 19 | left: BorderSide(color: theme.dividerColor, width: 1), 20 | ), 21 | ), 22 | child: SingleChildScrollView( 23 | child: Column( 24 | children: [Text(S.current.userWidgetUnSupport)], 25 | ), 26 | ), 27 | ), 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/pages/users/user_reactions_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | import 'package:moekey/apis/models/note.dart'; 4 | import 'package:moekey/widgets/notes/note_pagination_list.dart'; 5 | 6 | import '../../status/user.dart'; 7 | 8 | class UserReactionsPage extends HookConsumerWidget { 9 | const UserReactionsPage({ 10 | super.key, 11 | required this.userId, 12 | }); 13 | 14 | final String userId; 15 | 16 | @override 17 | Widget build(BuildContext context, WidgetRef ref) { 18 | var dataProvider = userReactionsListProvider( 19 | userId: userId, 20 | ); 21 | var data = ref.watch(dataProvider); 22 | var items = data.valueOrNull?.list ?? (List.empty()); 23 | return MkPaginationNoteList( 24 | onLoad: () => ref.read(dataProvider.notifier).load(), 25 | onRefresh: () => ref.refresh(dataProvider.future), 26 | hasMore: data.valueOrNull?.hasMore ?? true, 27 | items: items, 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/status/dio.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:dio/dio.dart'; 4 | import 'package:dio/io.dart'; 5 | import 'package:moekey/status/server.dart'; 6 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 7 | 8 | part 'dio.g.dart'; 9 | 10 | @Riverpod(keepAlive: true) 11 | class Http extends _$Http { 12 | @override 13 | Future build() async { 14 | var user = ref.watch(currentLoginUserProvider); 15 | var host = user?.serverUrl; 16 | 17 | var dio = Dio(BaseOptions( 18 | baseUrl: host != null ? "$host/api" : "http://localhost", 19 | )); 20 | 21 | dio.httpClientAdapter = IOHttpClientAdapter( 22 | createHttpClient: () { 23 | final client = HttpClient(); 24 | return client; 25 | }, 26 | ); 27 | return dio; 28 | } 29 | } 30 | 31 | @Riverpod(keepAlive: true) 32 | class SelectHttp extends _$SelectHttp { 33 | @override 34 | Dio build() { 35 | var host = ref.watch(selectServerHostProvider); 36 | var dio = Dio(BaseOptions( 37 | baseUrl: host != "" ? "$host/api" : "", 38 | )); 39 | 40 | dio.httpClientAdapter = IOHttpClientAdapter( 41 | createHttpClient: () { 42 | final client = HttpClient(); 43 | return client; 44 | }, 45 | ); 46 | return dio; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/status/global_snackbar.dart: -------------------------------------------------------------------------------- 1 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 2 | 3 | part 'global_snackbar.g.dart'; 4 | 5 | @riverpod 6 | class GlobalSnackbar extends _$GlobalSnackbar { 7 | @override 8 | String? build() { 9 | return null; 10 | } 11 | 12 | show(String message) { 13 | state = message; 14 | // 触发监听器 15 | ref.notifyListeners(); 16 | } 17 | 18 | hide() { 19 | state = null; 20 | ref.notifyListeners(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/status/me_detailed.dart: -------------------------------------------------------------------------------- 1 | import 'package:moekey/status/misskey_api.dart'; 2 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 3 | 4 | import '../apis/models/me_detailed.dart'; 5 | 6 | part 'me_detailed.g.dart'; 7 | 8 | @Riverpod(keepAlive: true) 9 | class CurrentMeDetailed extends _$CurrentMeDetailed { 10 | @override 11 | FutureOr build() async { 12 | var api = ref.watch(misskeyApisProvider); 13 | var info = await api.account.i(); 14 | return info; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/status/misskey_api.dart: -------------------------------------------------------------------------------- 1 | import 'package:moekey/apis/index.dart'; 2 | import 'package:moekey/status/global_snackbar.dart'; 3 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 4 | 5 | import '../generated/l10n.dart'; 6 | import 'server.dart'; 7 | 8 | part 'misskey_api.g.dart'; 9 | 10 | @Riverpod(keepAlive: true) 11 | MisskeyApis misskeyApis(MisskeyApisRef ref) { 12 | var user = ref.watch(currentLoginUserProvider); 13 | var instance = user?.serverUrl; 14 | var accessToken = user?.token; 15 | return MisskeyApis( 16 | instance: instance ?? "http://localhost", 17 | accessToken: accessToken ?? "", 18 | onUnauthorized: () { 19 | ref.read(globalSnackbarProvider.notifier).show(S.current.loginExpired); 20 | }, 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /lib/status/mk_tabbar_refresh_scroll_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 4 | 5 | import '../widgets/mk_tabbar_list.dart'; 6 | 7 | part 'mk_tabbar_refresh_scroll_state.g.dart'; 8 | 9 | @Riverpod(keepAlive: true) 10 | GlobalKey mkTabBarRefreshScrollStatus( 11 | Ref ref, String label) { 12 | return GlobalKey(debugLabel: label); 13 | } 14 | -------------------------------------------------------------------------------- /lib/status/notifications.dart: -------------------------------------------------------------------------------- 1 | import 'package:moekey/apis/models/notification.dart'; 2 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 3 | 4 | import '../apis/models/note.dart'; 5 | import '../logger.dart'; 6 | import 'misskey_api.dart'; 7 | 8 | part 'notifications.g.dart'; 9 | 10 | @riverpod 11 | class Notifications extends _$Notifications { 12 | @override 13 | Future> build() async { 14 | var model = MkLoadMoreListModel(); 15 | var list = await notificationsGrouped(); 16 | model.list += list; 17 | return model; 18 | } 19 | 20 | ///i/notifications-grouped 21 | Future> notificationsGrouped( 22 | {String? untilId}) async { 23 | try { 24 | var apis = ref.watch(misskeyApisProvider); 25 | var res = await apis.account.notificationsGrouped(untilId: untilId); 26 | return res; 27 | } catch (e, s) { 28 | logger.e(e); 29 | logger.e(s); 30 | } 31 | return []; 32 | } 33 | 34 | loadMore() async { 35 | if (state.isLoading) return; 36 | state = const AsyncValue.loading(); 37 | var model = state.valueOrNull ?? MkLoadMoreListModel(); 38 | try { 39 | var res = await notificationsGrouped(untilId: model.list.lastOrNull?.id); 40 | 41 | if (res.isNotEmpty) { 42 | model.list += res; 43 | } else { 44 | model.hasMore = false; 45 | } 46 | } finally { 47 | state = AsyncData(model); 48 | } 49 | } 50 | } 51 | 52 | @riverpod 53 | class MentionsNotifications extends _$MentionsNotifications { 54 | @override 55 | FutureOr build({bool specified = false}) async { 56 | var model = NoteListModel(); 57 | model.list = await mentions(); 58 | return model; 59 | } 60 | 61 | ///i/notifications-grouped 62 | Future> mentions({String? untilId}) async { 63 | var apis = ref.watch(misskeyApisProvider); 64 | var notes = await apis.notes 65 | .mentions(untilId: untilId, limit: 20, specified: specified); 66 | return notes; 67 | } 68 | 69 | loadMore() async { 70 | if (state.isLoading) return; 71 | state = const AsyncValue.loading(); 72 | var model = state.valueOrNull ?? NoteListModel(); 73 | try { 74 | String? untilId; 75 | if (state.valueOrNull!.list.isNotEmpty) { 76 | untilId = state.valueOrNull?.list.last.id; 77 | } 78 | List notesList = await mentions(untilId: untilId); 79 | 80 | model.list += notesList; 81 | if (notesList.isEmpty) { 82 | model.hasMore = false; 83 | } 84 | } finally { 85 | state = AsyncData(model); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /lib/status/timeline.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:moekey/status/server.dart'; 4 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 5 | 6 | import '../apis/models/note.dart'; 7 | import '../database/timeline.dart'; 8 | import '../logger.dart'; 9 | import 'misskey_api.dart'; 10 | 11 | part 'timeline.g.dart'; 12 | 13 | @riverpod 14 | Future timelineDatabase(TimelineDatabaseRef ref) async { 15 | var user = ref.watch(currentLoginUserProvider); 16 | var instance = user?.serverUrl; 17 | return TimelineDatabase( 18 | server: instance ?? "default", userId: user?.id ?? "default"); 19 | } 20 | 21 | @riverpod 22 | class Timeline extends _$Timeline { 23 | @override 24 | FutureOr build({String api = "timeline"}) async { 25 | List? cache; 26 | TimelineDatabase? db; 27 | try { 28 | db = await ref.watch(timelineDatabaseProvider.future); 29 | cache = await db?.getTimeline(api); 30 | } catch (e) { 31 | logger.e(e); 32 | db?.cleanTimeline(api); 33 | } 34 | var model = NoteListModel(); 35 | if (cache != null && cache.isNotEmpty) { 36 | model.list = cache; 37 | } else { 38 | var list = await timeline(); 39 | model.list = list; 40 | db?.setTimeline(api, list); 41 | } 42 | 43 | return model; 44 | } 45 | 46 | Future> timeline({String? untilId, String? sinceId}) async { 47 | var apis = ref.watch(misskeyApisProvider); 48 | var list = await apis.notes.timeline( 49 | limit: 10, 50 | untilId: untilId, 51 | api: api, 52 | sinceId: sinceId, 53 | ); 54 | return list; 55 | } 56 | 57 | load() async { 58 | if (state.isLoading) return; 59 | 60 | var model = state.valueOrNull ?? NoteListModel(); 61 | state = const AsyncValue.loading(); 62 | try { 63 | String? untilId = model.list.lastOrNull?.id; 64 | 65 | List notesList = await timeline(untilId: untilId); 66 | 67 | model.list += notesList; 68 | if (notesList.isEmpty) { 69 | model.hasMore = false; 70 | } 71 | } catch (e) { 72 | logger.e(e); 73 | } 74 | state = AsyncData(model); 75 | } 76 | 77 | cleanCache() async { 78 | var db = await ref.read(timelineDatabaseProvider.future); 79 | await db.cleanTimeline(api); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /lib/status/user_login.dart: -------------------------------------------------------------------------------- 1 | import 'package:moekey/apis/index.dart'; 2 | import 'package:moekey/apis/models/user_full.dart'; 3 | import 'package:moekey/generated/l10n.dart'; 4 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 5 | import 'package:url_launcher/url_launcher_string.dart'; 6 | 7 | import '../apis/models/login_user.dart'; 8 | import '../utils/get_token.dart'; 9 | import 'server.dart'; 10 | 11 | part 'user_login.g.dart'; 12 | 13 | @riverpod 14 | class ApiUserLogin extends _$ApiUserLogin { 15 | var didDispose = false; 16 | 17 | @override 18 | FutureOr build() async { 19 | var userModel = await login(); 20 | if (userModel == null) { 21 | throw Exception(S.current.loginFailed); 22 | } 23 | // 保存 24 | await ref.watch(loginUserListProvider.notifier).addUser(userModel); 25 | await ref 26 | .watch(currentLoginUserProvider.notifier) 27 | .setLoginUser(userModel.id); 28 | return userModel; 29 | } 30 | 31 | Future login() async { 32 | var host = ref.read(selectServerHostProvider); 33 | 34 | var apis = 35 | MisskeyApis(instance: host, accessToken: "", onUnauthorized: null); 36 | 37 | var app = await apis.app.create(); 38 | if (app == null) { 39 | throw Exception(S.current.loginFailedWithAppCreate); 40 | } 41 | String secret = app.secret!; 42 | 43 | var authSession = await apis.auth.sessionGenerate(appSecret: secret); 44 | if (authSession == null) { 45 | throw Exception(S.current.loginFailedWithToken); 46 | } 47 | String token = authSession.token; 48 | String url = authSession.url; 49 | // 浏览器打开url完成认证 50 | launchUrlString(url); 51 | 52 | ref.onDispose(() => didDispose = true); 53 | while (true) { 54 | if (didDispose) break; 55 | await Future.delayed(const Duration(seconds: 1)); 56 | var key = await apis.auth.sessionUserKey(appSecret: secret, token: token); 57 | if (key?["error"] != null) { 58 | if (key?["error"]["code"] == "PENDING_SESSION") { 59 | // 等待中 60 | continue; 61 | } else { 62 | // 失败 63 | throw Exception(S.current.loginFailed); 64 | } 65 | } 66 | if (key == null) { 67 | throw Exception(S.current.loginFailed); 68 | } 69 | if (key["accessToken"] != null) { 70 | // 成功登陆 71 | return LoginUser( 72 | id: key["user"]["id"], 73 | name: key["user"]["name"] ?? key["user"]["username"], 74 | serverUrl: host, 75 | token: getToken(key["accessToken"], secret), 76 | userInfo: UserFullModel.fromJson(key["user"])); 77 | } 78 | } 79 | return null; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /lib/utils/custom_rect_tween.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/animation.dart'; 2 | 3 | class CustomRectTween extends RectTween { 4 | CustomRectTween({this.a, this.b}) : super(begin: a, end: b); 5 | final Rect? a; 6 | final Rect? b; 7 | 8 | @override 9 | Rect lerp(double t) { 10 | // Curves.elasticOut.transform(t); 11 | //any curve can be applied here e.g. Curve.elasticOut.transform(t); 12 | // final verticalDist = Cubic(0.72, 0.15, 0.5, 1).transform(t); 13 | 14 | final top = lerpDouble(a?.top, b?.top, t) * (1 - t); 15 | return Rect.fromLTRB( 16 | lerpDouble(a?.left, b?.left, t), 17 | top, 18 | lerpDouble(a?.right, b?.right, t), 19 | lerpDouble(a?.bottom, b?.bottom, t), 20 | ); 21 | } 22 | 23 | double lerpDouble(num? a, num? b, double t) { 24 | a ??= 0.0; 25 | b ??= 0.0; 26 | return a + (b - a) * t; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/utils/format_duration.dart: -------------------------------------------------------------------------------- 1 | import '../generated/l10n.dart'; 2 | 3 | String formatDuration(int milliseconds) { 4 | int seconds = (milliseconds / 1000).truncate(); 5 | int minutes = (seconds / 60).truncate(); 6 | int hours = (minutes / 60).truncate(); 7 | int days = (hours / 24).truncate(); 8 | 9 | String twoDigits(int n) => n.toString().padLeft(2, "0"); 10 | String twoDigitMinutes = twoDigits(minutes.remainder(60)); 11 | String twoDigitSeconds = twoDigits(seconds.remainder(60)); 12 | String twoDigitHours = twoDigits(hours.remainder(24)); 13 | 14 | if (days > 0) { 15 | return S.current 16 | .durationDay(days, twoDigitHours, twoDigitMinutes, twoDigitSeconds); 17 | } else if (hours > 0) { 18 | return S.current 19 | .durationHour(twoDigitHours, twoDigitMinutes, twoDigitSeconds); 20 | } else if (minutes > 0) { 21 | return S.current.durationMinute(twoDigitMinutes, twoDigitSeconds); 22 | } else { 23 | return S.current.durationSecond(twoDigitSeconds); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/utils/get_padding_note.dart: -------------------------------------------------------------------------------- 1 | double getPaddingForNote(constraints) { 2 | double padding = 0; 3 | if (constraints.maxWidth > 860) { 4 | padding = (constraints.maxWidth - 800) / 2; 5 | } else if (constraints.maxWidth > 500) { 6 | padding = 30; 7 | } else if (constraints.maxWidth > 400) { 8 | padding = 8; 9 | } else { 10 | padding = 0; 11 | } 12 | return padding; 13 | } 14 | -------------------------------------------------------------------------------- /lib/utils/get_random_string.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | String getRandomString(int length) { 4 | const characters = 5 | '+-*=?AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz'; 6 | Random random = Random(); 7 | return String.fromCharCodes(Iterable.generate( 8 | length, (_) => characters.codeUnitAt(random.nextInt(characters.length)))); 9 | } 10 | -------------------------------------------------------------------------------- /lib/utils/get_token.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:crypto/crypto.dart'; 4 | 5 | String getToken(String accessToken, String appSecret) { 6 | final bytes = utf8.encode(accessToken + appSecret); 7 | final hash = sha256.convert(bytes); 8 | return hash.toString(); 9 | } 10 | -------------------------------------------------------------------------------- /lib/utils/image_compression.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:image_compression/image_compression.dart'; 4 | 5 | Future imageCompression(String path) async { 6 | final file = File(path); 7 | 8 | final input = ImageFile( 9 | rawBytes: file.readAsBytesSync(), 10 | filePath: file.path, 11 | ); 12 | final output = await compressInQueue(ImageFileConfiguration(input: input)); 13 | return output; 14 | } 15 | -------------------------------------------------------------------------------- /lib/utils/save_image.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | import 'dart:typed_data'; 4 | import 'dart:ui' as ui; 5 | import 'dart:ui'; 6 | 7 | import 'package:crypto/crypto.dart'; 8 | import 'package:dio/dio.dart'; 9 | import 'package:extended_image/extended_image.dart'; 10 | import 'package:file_picker/file_picker.dart'; 11 | import 'package:gal/gal.dart'; 12 | import 'package:mime/mime.dart'; 13 | import 'package:path/path.dart'; 14 | import 'package:path_provider/path_provider.dart'; 15 | import 'package:shared_preferences/shared_preferences.dart'; 16 | 17 | const Map defaultExtensionMap = { 18 | 'image/png': 'png', 19 | 'image/jpeg': 'jpg', 20 | 'image/webp': 'webp', 21 | 'image/gif': 'gif', 22 | 'image/heif': 'heif', 23 | 'image/heic': 'heic', 24 | 'image/bmp': 'bmp', 25 | }; 26 | 27 | Future saveImage({ 28 | required Dio http, 29 | required String url, 30 | String? name, 31 | String album = "moekey", 32 | }) async { 33 | name = 34 | name != null ? basename(name) : md5.convert(utf8.encode(url)).toString(); 35 | 36 | var ext = extension(name).substring(1); 37 | var fileBasename = basenameWithoutExtension(name); 38 | 39 | var data = await getNetworkImageData(url, useCache: true); 40 | if (data == null) { 41 | return false; 42 | } 43 | var type = lookupMimeType(url, headerBytes: data); 44 | 45 | ext = defaultExtensionMap[type] ?? ext; 46 | 47 | name = "$fileBasename.$ext"; 48 | 49 | var codec = await ui.instantiateImageCodec(data); 50 | 51 | if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) { 52 | // webp 单帧图片转换成png 53 | if (ext.toLowerCase().contains("webp") && codec.frameCount == 1) { 54 | final frame = await codec.getNextFrame(); 55 | final image = frame.image; 56 | final byteData = await image.toByteData(format: ImageByteFormat.png); 57 | if (byteData == null) { 58 | return false; 59 | } 60 | data = Uint8List.view(byteData.buffer); 61 | name = "$fileBasename.png"; 62 | } 63 | 64 | var prefs = await SharedPreferences.getInstance(); 65 | var initialDirectory = prefs.getString("saveInitialDirectory"); 66 | String? outputFile = await FilePicker.platform.saveFile( 67 | dialogTitle: 'Please select an output file:', 68 | fileName: name, 69 | type: FileType.image, 70 | initialDirectory: initialDirectory); 71 | if (outputFile == null) { 72 | return false; 73 | } 74 | await prefs.setString("saveInitialDirectory", dirname(outputFile)); 75 | var file = File(outputFile); 76 | await file.writeAsBytes(data); 77 | } 78 | if (Platform.isAndroid || Platform.isIOS) { 79 | final imagePath = "${(await getTemporaryDirectory()).path}/$name"; 80 | var file = File(imagePath); 81 | await file.writeAsBytes(data); 82 | // print(await getTemporaryDirectory()); 83 | await Gal.putImage(imagePath, album: album); 84 | } 85 | return true; 86 | } 87 | -------------------------------------------------------------------------------- /lib/utils/time_ago_since_date.dart: -------------------------------------------------------------------------------- 1 | import '../generated/l10n.dart'; 2 | 3 | String timeAgoSinceDate(DateTime notificationDate) { 4 | final date2 = DateTime.now(); 5 | final difference = date2.difference(notificationDate); 6 | 7 | if (difference.inSeconds < 10) { 8 | return S.current.justNow; 9 | } else if (difference.inSeconds < 60) { 10 | return S.current.secondsAgo(difference.inSeconds); 11 | } else if (difference.inMinutes < 60) { 12 | return S.current.minutesAgo(difference.inMinutes); 13 | } else if (difference.inHours < 24) { 14 | return S.current.hoursAgo(difference.inHours); 15 | } else if (difference.inDays < 30) { 16 | return S.current.daysAgo(difference.inDays); 17 | } else if (difference.inDays < 365) { 18 | return S.current.monthsAgo((difference.inDays / 30).floor()); 19 | } else { 20 | return S.current.yearsAgo((difference.inDays / 365).floor()); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/utils/time_to_desired_format.dart: -------------------------------------------------------------------------------- 1 | import 'package:intl/intl.dart'; 2 | import 'package:moekey/utils/time_ago_since_date.dart'; 3 | 4 | String timeToDesiredFormat(DateTime dateTime) { 5 | String formattedDateTime = 6 | DateFormat('yyyy/MM/dd HH:mm:ss').format(dateTime.toLocal()); 7 | return "$formattedDateTime (${timeAgoSinceDate(dateTime)})"; 8 | } 9 | -------------------------------------------------------------------------------- /lib/utils/update_themes.dart: -------------------------------------------------------------------------------- 1 | Future getInstanceMetaAndUpdateThemes() async { 2 | // var response = await NetWork.meta(); 3 | // 4 | // if (response.data != null) { 5 | // var res = response.data; 6 | // 7 | // ServerMetaModel.metaData.value = res; 8 | // 9 | // if (res["defaultLightTheme"] != null) { 10 | // var themes = jsonDecode(res["defaultLightTheme"])["props"]; 11 | // ThemeColorModel.accentColor.value = parseColor(themes["accent"]); 12 | // ThemeColorModel.bgColor.value = parseColor(themes["bg"]); 13 | // ThemeColorModel.fgColor.value = parseColor(themes["fg"]); 14 | // ThemeColorModel.themeColor.value = ThemeColorModel.accentColor.value; 15 | // } else { 16 | // if (res["themeColor"] != null) { 17 | // ThemeColorModel.themeColor.value = 18 | // const Color.fromARGB(255, 152, 201, 52); 19 | // } 20 | // } 21 | // 22 | // var theme = ThemeData( 23 | // useMaterial3: true, 24 | // colorScheme: 25 | // ColorScheme.fromSeed(seedColor: ThemeColorModel.themeColor.value), 26 | // fontFamily: "微软雅黑", 27 | // textTheme: TextTheme( 28 | // bodyMedium: TextStyle( 29 | // color: ThemeColorModel.fgColor.value, 30 | // ), 31 | // ), 32 | // textSelectionTheme: TextSelectionThemeData( 33 | // selectionColor: ThemeColorModel.themeColor.value.withOpacity(0.3), 34 | // )); 35 | // Get.changeTheme(theme); 36 | // } 37 | // 38 | // return response.data; 39 | } 40 | -------------------------------------------------------------------------------- /lib/widgets/blur_widget.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 5 | import 'package:moekey/status/themes.dart'; 6 | 7 | class BlurWidget extends ConsumerWidget { 8 | final Widget? child; 9 | final Color? color; 10 | 11 | const BlurWidget({ 12 | super.key, 13 | this.child, 14 | this.color, 15 | }); 16 | 17 | @override 18 | Widget build(BuildContext context, WidgetRef ref) { 19 | var themes = ref.watch(themeColorsProvider); 20 | return ClipRect( 21 | child: BackdropFilter( 22 | filter: ImageFilter.blur(sigmaX: 15.0, sigmaY: 15.0), 23 | child: DecoratedBox( 24 | decoration: BoxDecoration(color: color ?? themes.headerColor), 25 | child: child, 26 | ), 27 | ), 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/widgets/clips/clips_create_dialog_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:moekey/generated/l10n.dart'; 4 | import 'package:moekey/pages/clips/clips.dart'; 5 | import 'package:moekey/status/misskey_api.dart'; 6 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 7 | 8 | import '../../logger.dart'; 9 | import '../mk_info_dialog.dart'; 10 | 11 | part 'clips_create_dialog_state.g.dart'; 12 | 13 | class ClipCreateDialogStateModel { 14 | String name = ""; 15 | String description = ""; 16 | bool isPublic = false; 17 | bool isPreview = false; 18 | 19 | @override 20 | String toString() { 21 | return 'ClipCreateDialogStateModel{name: $name, description: $description, isPublic: $isPublic, isPreview: $isPreview}'; 22 | } 23 | } 24 | 25 | @riverpod 26 | class ClipCreateDialogState extends _$ClipCreateDialogState { 27 | @override 28 | ClipCreateDialogStateModel build({String? clipId}) { 29 | return ClipCreateDialogStateModel(); 30 | } 31 | 32 | updateName(String name) { 33 | state.name = name; 34 | ref.notifyListeners(); 35 | } 36 | 37 | updateDescription(String description) { 38 | state.description = description; 39 | ref.notifyListeners(); 40 | } 41 | 42 | updateIsPublic(bool isPublic) { 43 | state.isPublic = isPublic; 44 | ref.notifyListeners(); 45 | } 46 | 47 | updateIsPreview(bool isPreview) { 48 | state.isPreview = isPreview; 49 | ref.notifyListeners(); 50 | } 51 | 52 | send(BuildContext context) async { 53 | var apis = ref.read(misskeyApisProvider); 54 | try { 55 | if (state.name.isEmpty) { 56 | throw Exception(S.current.nameCannotBeEmpty); 57 | } 58 | // 更新 59 | if (clipId != null) { 60 | var res = await apis.clips.update( 61 | name: state.name, 62 | description: state.description, 63 | isPublic: state.isPublic, 64 | clipId: clipId!); 65 | 66 | ref.invalidate(clipsProvider); 67 | ref.invalidate(clipsShowProvider(clipId!)); 68 | 69 | return res; 70 | } 71 | 72 | var res = await apis.clips.create( 73 | name: state.name, 74 | description: state.description, 75 | isPublic: state.isPublic, 76 | ); 77 | ref.invalidate(clipsProvider); 78 | return res; 79 | } on DioException catch (e) { 80 | logger.d(e.response); 81 | if (!context.mounted) return; 82 | MkInfoDialog.show( 83 | info: S.current 84 | .creationFailedDialog(e.response?.data.toString() ?? e.toString()), 85 | isError: true, 86 | context: context, 87 | ); 88 | } catch (e) { 89 | if (!context.mounted) return; 90 | MkInfoDialog.show( 91 | info: "$e", 92 | isError: true, 93 | context: context, 94 | ); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /lib/widgets/driver/driver_select_dialog/driver_select_dialog_state.dart: -------------------------------------------------------------------------------- 1 | import 'dart:collection'; 2 | 3 | import 'package:moekey/apis/models/drive.dart'; 4 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 5 | 6 | part 'driver_select_dialog_state.g.dart'; 7 | 8 | @Riverpod(keepAlive: true) 9 | class DriverSelectDialogState extends _$DriverSelectDialogState { 10 | @override 11 | LinkedHashMap build() { 12 | return LinkedHashMap(); 13 | } 14 | 15 | add(String id, DriveFileModel data) { 16 | state[id] = data; 17 | ref.notifyListeners(); 18 | } 19 | 20 | remove(String id) { 21 | if (state[id] != null) { 22 | state.remove(id); 23 | ref.notifyListeners(); 24 | } 25 | } 26 | 27 | clear() { 28 | state.clear(); 29 | ref.notifyListeners(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/widgets/hashtag/hashtag_select_dialog_state.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:moekey/status/misskey_api.dart'; 4 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 5 | 6 | part 'hashtag_select_dialog_state.g.dart'; 7 | 8 | @riverpod 9 | class HashtagSelectDialogState extends _$HashtagSelectDialogState { 10 | Timer? timer; 11 | 12 | @override 13 | FutureOr build() async { 14 | return []; 15 | } 16 | 17 | search({String? query}) { 18 | // 防抖 19 | timer?.cancel(); 20 | state = const AsyncLoading(); 21 | timer = Timer(const Duration(seconds: 1), () { 22 | _search(query: query); 23 | }); 24 | } 25 | 26 | _search({String? query}) async { 27 | if (query == null) { 28 | state = const AsyncData([]); 29 | return; 30 | } 31 | 32 | var http = ref.read(misskeyApisProvider); 33 | var data = await http.hashtags.search(query: query); 34 | state = AsyncData(data); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/widgets/hover_builder.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | class HoverBuilder extends StatefulWidget { 6 | const HoverBuilder({super.key, required this.builder, this.onHover}); 7 | 8 | final Widget Function(BuildContext context, bool isHover) builder; 9 | final void Function(bool isHover)? onHover; 10 | 11 | @override 12 | State createState() => _HoverBuilderState(); 13 | } 14 | 15 | class _HoverBuilderState extends State { 16 | var isHover = false; 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return MouseRegion( 21 | cursor: SystemMouseCursors.click, 22 | onEnter: (event) { 23 | setState(() { 24 | isHover = true; 25 | }); 26 | if (widget.onHover != null) { 27 | widget.onHover!(true); 28 | } 29 | }, 30 | onExit: (event) { 31 | setState(() { 32 | isHover = false; 33 | }); 34 | if (widget.onHover != null) { 35 | widget.onHover!(false); 36 | } 37 | }, 38 | child: Platform.isAndroid || Platform.isIOS 39 | ? Listener( 40 | onPointerDown: (event) { 41 | setState(() { 42 | isHover = true; 43 | }); 44 | }, 45 | onPointerUp: (event) { 46 | setState(() { 47 | isHover = false; 48 | }); 49 | }, 50 | // onPointerSignal: (event) { 51 | // setState(() { 52 | // isHover = false; 53 | // }); 54 | // }, 55 | onPointerCancel: (event) { 56 | setState(() { 57 | isHover = false; 58 | }); 59 | }, 60 | child: widget.builder(context, isHover), 61 | ) 62 | : widget.builder(context, isHover), 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/widgets/keep_alive_wrapper.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class KeepAliveWrapper extends StatefulWidget { 4 | const KeepAliveWrapper({ 5 | super.key, 6 | this.keepAlive = true, 7 | required this.child, 8 | }); 9 | 10 | final bool keepAlive; 11 | final Widget child; 12 | 13 | @override 14 | KeepAliveWrapperState createState() => KeepAliveWrapperState(); 15 | } 16 | 17 | class KeepAliveWrapperState extends State 18 | with AutomaticKeepAliveClientMixin { 19 | @override 20 | Widget build(BuildContext context) { 21 | super.build(context); 22 | return widget.child; 23 | } 24 | 25 | @override 26 | void didUpdateWidget(covariant KeepAliveWrapper oldWidget) { 27 | if (oldWidget.keepAlive != widget.keepAlive) { 28 | // keepAlive 状态需要更新,实现在 AutomaticKeepAliveClientMixin 中 29 | updateKeepAlive(); 30 | } 31 | super.didUpdateWidget(oldWidget); 32 | } 33 | 34 | @override 35 | bool get wantKeepAlive => widget.keepAlive; 36 | } 37 | -------------------------------------------------------------------------------- /lib/widgets/login/login_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:go_router/go_router.dart'; 5 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 6 | 7 | import '../../generated/l10n.dart'; 8 | import '../../status/server.dart'; 9 | import '../../status/user_login.dart'; 10 | import '../mk_dialog.dart'; 11 | 12 | class LoginDialog extends HookConsumerWidget { 13 | const LoginDialog({ 14 | super.key, 15 | }); 16 | 17 | @override 18 | Widget build(BuildContext context, WidgetRef ref) { 19 | var user = ref.watch(apiUserLoginProvider); 20 | var host = ref.watch(selectServerHostProvider); 21 | return MkDialog( 22 | padding: const EdgeInsets.all(16), 23 | child: Column( 24 | mainAxisSize: MainAxisSize.min, 25 | children: switch (user) { 26 | AsyncData() => [ 27 | // logger.d(valueOrNull) 28 | Builder( 29 | builder: (context) { 30 | Timer( 31 | const Duration(seconds: 1), 32 | () { 33 | context.replace("/"); 34 | }, 35 | ); 36 | return Text(S.current.loginSuccess); 37 | }, 38 | ), 39 | TextButton( 40 | child: Text(S.current.ok), 41 | onPressed: () { 42 | context.pop(); 43 | }, 44 | ) 45 | ], 46 | AsyncError(:final error, :final stackTrace) => [ 47 | Text(error.toString()), 48 | Text(stackTrace.toString()), 49 | TextButton( 50 | child: Text(S.current.ok), 51 | onPressed: () { 52 | context.pop(); 53 | }, 54 | ) 55 | ], 56 | _ => [ 57 | const SizedBox( 58 | height: 16, 59 | ), 60 | UnconstrainedBox( 61 | child: CircularProgressIndicator( 62 | strokeCap: StrokeCap.round, 63 | backgroundColor: Theme.of(context).primaryColor.withAlpha(32), 64 | color: Theme.of(context).primaryColor.withAlpha(200), 65 | strokeWidth: 6, 66 | ), 67 | ), 68 | const SizedBox( 69 | height: 16, 70 | ), 71 | Text( 72 | S.current.loginLoading(host), 73 | textAlign: TextAlign.center, 74 | ), 75 | const SizedBox( 76 | height: 16, 77 | ), 78 | TextButton( 79 | child: Text(S.current.cancel), 80 | onPressed: () { 81 | context.pop(); 82 | }, 83 | ) 84 | ] 85 | }, 86 | ), 87 | ); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /lib/widgets/login/servers_select_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:moekey/status/dio.dart'; 2 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 3 | 4 | part 'servers_select_state.g.dart'; 5 | 6 | @riverpod 7 | class InstanceListState extends _$InstanceListState { 8 | @override 9 | FutureOr> build() async { 10 | var http = await ref.watch(httpProvider.future); 11 | var response = 12 | await http.get("https://instanceapp.misskey.page/instances.json"); 13 | return response.data['instancesInfos']; 14 | } 15 | } 16 | 17 | List instanceListFilter(List list, String filter) { 18 | var res = []; 19 | for (var element in list) { 20 | if (element["url"].toString().contains(filter) || 21 | element["name"].toString().contains(filter)) { 22 | res.add(element); 23 | } 24 | } 25 | return res; 26 | } 27 | -------------------------------------------------------------------------------- /lib/widgets/mfm_text/animate/jelly.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class MfmJellyCode extends StatefulWidget { 4 | const MfmJellyCode({ 5 | super.key, 6 | required this.child, 7 | this.speed, 8 | }); 9 | 10 | final Widget child; 11 | final Duration? speed; 12 | 13 | @override 14 | State createState() => _SpinCodeState(); 15 | } 16 | 17 | class _SpinCodeState extends State 18 | with SingleTickerProviderStateMixin { 19 | late AnimationController _controller; 20 | late Animation _scaleXAnimation; 21 | final _scaleXSequence = TweenSequence([ 22 | TweenSequenceItem( 23 | tween: Tween(begin: 1.0, end: 1.25), weight: 30), // 0% -> 30% 24 | TweenSequenceItem( 25 | tween: Tween(begin: 1.25, end: 0.75), weight: 10), // 30% -> 40% 26 | TweenSequenceItem( 27 | tween: Tween(begin: 0.75, end: 1.15), weight: 10), // 40% -> 50% 28 | TweenSequenceItem( 29 | tween: Tween(begin: 1.15, end: 0.95), weight: 15), // 50% -> 65% 30 | TweenSequenceItem( 31 | tween: Tween(begin: 0.95, end: 1.05), weight: 10), // 65% -> 75% 32 | TweenSequenceItem( 33 | tween: Tween(begin: 1.05, end: 1.0), weight: 25), // 75% -> 100%> 35% 34 | ]); 35 | 36 | late Animation _scaleYAnimation; 37 | final _scaleYSequence = TweenSequence([ 38 | TweenSequenceItem( 39 | tween: Tween(begin: 1.0, end: 0.75), weight: 30), // 0% -> 30% 40 | TweenSequenceItem( 41 | tween: Tween(begin: 0.75, end: 1.25), weight: 10), // 30% -> 40% 42 | TweenSequenceItem( 43 | tween: Tween(begin: 1.25, end: 0.85), weight: 10), // 40% -> 50% 44 | TweenSequenceItem( 45 | tween: Tween(begin: 0.85, end: 1.05), weight: 15), // 50% -> 65% 46 | TweenSequenceItem( 47 | tween: Tween(begin: 1.05, end: 0.95), weight: 10), // 65% -> 75% 48 | TweenSequenceItem( 49 | tween: Tween(begin: 0.95, end: 1.0), weight: 25), // 75% -> 100%> 35% 50 | ]); 51 | 52 | @override 53 | void initState() { 54 | super.initState(); 55 | _controller = AnimationController(vsync: this, duration: widget.speed); 56 | 57 | _scaleXAnimation = _controller.drive(_scaleXSequence); 58 | _scaleYAnimation = _controller.drive(_scaleYSequence); 59 | 60 | Future(() async { 61 | _controller.repeat(); 62 | }); 63 | } 64 | 65 | @override 66 | void dispose() { 67 | _controller.dispose(); 68 | super.dispose(); 69 | } 70 | 71 | @override 72 | Widget build(BuildContext context) { 73 | return AnimatedBuilder( 74 | animation: _controller, 75 | child: widget.child, 76 | builder: (context, child) { 77 | return Transform( 78 | alignment: Alignment.center, 79 | transform: Matrix4.diagonal3Values( 80 | _scaleXAnimation.value, _scaleYAnimation.value, 1.0), 81 | child: widget.child, 82 | ); 83 | }); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /lib/widgets/mfm_text/animate/spin.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | class MfmSpinCode extends StatefulWidget { 6 | const MfmSpinCode( 7 | {super.key, 8 | required this.child, 9 | this.x = false, 10 | this.y = false, 11 | this.speed, 12 | this.left = false, 13 | this.alternate = false}); 14 | 15 | final Widget child; 16 | final bool x; 17 | final bool y; 18 | final Duration? speed; 19 | final bool left; 20 | final bool alternate; 21 | 22 | @override 23 | State createState() => _SpinCodeState(); 24 | } 25 | 26 | class _SpinCodeState extends State 27 | with SingleTickerProviderStateMixin { 28 | late AnimationController controller; 29 | late Animation animate; 30 | 31 | @override 32 | void initState() { 33 | super.initState(); 34 | var duration = widget.speed ?? const Duration(milliseconds: 1500); 35 | controller = AnimationController(duration: duration, vsync: this); 36 | animate = Tween( 37 | begin: widget.left ? 2 * pi : 0.0, end: widget.left ? 0.0 : 2 * pi) 38 | .animate(controller); 39 | controller.repeat(reverse: widget.alternate); 40 | controller.addListener(() { 41 | setState(() {}); 42 | }); 43 | } 44 | 45 | @override 46 | void dispose() { 47 | controller.dispose(); 48 | super.dispose(); 49 | } 50 | 51 | @override 52 | Widget build(BuildContext context) { 53 | Matrix4 transform = Matrix4.identity()..setEntry(3, 2, 0.01); 54 | if (!widget.x && !widget.y) { 55 | transform.rotateZ(animate.value); 56 | } 57 | if (widget.x) { 58 | transform.rotateX(animate.value); 59 | } 60 | if (widget.y) { 61 | transform.rotateY(-animate.value); 62 | } 63 | return Transform( 64 | alignment: Alignment.center, 65 | transform: transform, 66 | child: widget.child, 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /lib/widgets/mk_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | 4 | import '../status/themes.dart'; 5 | 6 | class MkPrimaryButton extends HookConsumerWidget { 7 | const MkPrimaryButton( 8 | {super.key, 9 | required this.child, 10 | required this.onPressed, 11 | this.onLongPress, 12 | this.clipBehavior}); 13 | 14 | final Widget child; 15 | final void Function() onPressed; 16 | final VoidCallback? onLongPress; 17 | final Clip? clipBehavior; 18 | 19 | @override 20 | Widget build(BuildContext context, WidgetRef ref) { 21 | var themes = ref.watch(themeColorsProvider); 22 | return ElevatedButton( 23 | onPressed: onPressed, 24 | onLongPress: onLongPress, 25 | clipBehavior: clipBehavior, 26 | style: ButtonStyle( 27 | backgroundColor: WidgetStateProperty.all(themes.accentColor), 28 | foregroundColor: WidgetStateProperty.all(themes.fgOnAccentColor), 29 | elevation: WidgetStateProperty.all(0)), 30 | child: child); 31 | } 32 | } 33 | 34 | class MkSecondaryButton extends HookConsumerWidget { 35 | const MkSecondaryButton( 36 | {super.key, 37 | required this.child, 38 | required this.onPressed, 39 | this.onLongPress, 40 | this.clipBehavior}); 41 | 42 | final Widget child; 43 | final void Function() onPressed; 44 | final VoidCallback? onLongPress; 45 | final Clip? clipBehavior; 46 | 47 | @override 48 | Widget build(BuildContext context, WidgetRef ref) { 49 | var themes = ref.watch(themeColorsProvider); 50 | return ElevatedButton( 51 | onPressed: onPressed, 52 | onLongPress: onLongPress, 53 | clipBehavior: clipBehavior, 54 | style: ButtonStyle( 55 | backgroundColor: WidgetStateProperty.all(themes.buttonBgColor), 56 | foregroundColor: WidgetStateProperty.all(themes.fgColor), 57 | elevation: WidgetStateProperty.all(0)), 58 | child: child); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/widgets/mk_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | import 'package:moekey/status/themes.dart'; 4 | 5 | class MkCard extends ConsumerWidget { 6 | final Widget child; 7 | final EdgeInsetsGeometry? padding; 8 | final bool shadow; 9 | final BorderRadius borderRadius; 10 | 11 | const MkCard( 12 | {super.key, 13 | required this.child, 14 | this.padding, 15 | this.shadow = true, 16 | this.borderRadius = const BorderRadius.all( 17 | Radius.circular(12), 18 | )}); 19 | 20 | @override 21 | Widget build(BuildContext context, WidgetRef ref) { 22 | var themes = ref.watch(themeColorsProvider); 23 | return ClipRRect( 24 | borderRadius: borderRadius, 25 | child: AnimatedContainer( 26 | decoration: BoxDecoration( 27 | borderRadius: borderRadius, 28 | color: themes.panelColor, 29 | boxShadow: [ 30 | if (shadow) 31 | BoxShadow( 32 | color: Colors.black.withAlpha(64), 33 | blurRadius: 20, 34 | offset: const Offset(0, 4), 35 | ) 36 | ], 37 | ), 38 | duration: const Duration(milliseconds: 300), 39 | child: Padding( 40 | padding: padding ?? const EdgeInsets.all(16), 41 | child: DefaultTextStyle( 42 | style: DefaultTextStyle.of(context).style, 43 | child: child, 44 | ), 45 | ), 46 | ), 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/widgets/mk_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:moekey/widgets/mk_modal.dart'; 3 | 4 | import 'mk_card.dart'; 5 | 6 | class MkDialog extends StatelessWidget { 7 | const MkDialog({super.key, required this.child, this.padding}); 8 | 9 | final EdgeInsetsGeometry? padding; 10 | final Widget child; 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | // 设置 15 | return ModalWrapper( 16 | child: MkCard( 17 | borderRadius: const BorderRadius.all(Radius.circular(24)), 18 | padding: padding, 19 | child: ConstrainedBox( 20 | constraints: const BoxConstraints(maxWidth: 600, minWidth: 300), 21 | child: child, 22 | ), 23 | )); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/widgets/mk_load_more.dart: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lib/widgets/mk_nav_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | 4 | import '../status/themes.dart'; 5 | 6 | class MkNavButton extends HookConsumerWidget { 7 | const MkNavButton({ 8 | super.key, 9 | required this.active, 10 | required this.text, 11 | required this.onTap, 12 | }); 13 | 14 | final bool active; 15 | final String text; 16 | final void Function() onTap; 17 | 18 | @override 19 | Widget build(BuildContext context, WidgetRef ref) { 20 | var themes = ref.watch(themeColorsProvider); 21 | return GestureDetector( 22 | onTap: onTap, 23 | behavior: HitTestBehavior.opaque, 24 | child: Container( 25 | decoration: BoxDecoration( 26 | color: active ? themes.accentedBgColor : Colors.transparent, 27 | borderRadius: const BorderRadius.all(Radius.circular(100))), 28 | padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), 29 | child: Text( 30 | text, 31 | style: TextStyle( 32 | color: active ? themes.accentColor : themes.fgColor, 33 | fontSize: 12), 34 | textAlign: TextAlign.center, 35 | ), 36 | ), 37 | ); 38 | } 39 | } 40 | 41 | class MkNavButtonBar extends StatelessWidget { 42 | const MkNavButtonBar({ 43 | super.key, 44 | required this.onSelect, 45 | required this.index, 46 | required this.navs, 47 | }); 48 | 49 | final void Function(int index) onSelect; 50 | final int index; 51 | final List navs; 52 | 53 | @override 54 | Widget build(BuildContext context) { 55 | return Row( 56 | children: [ 57 | for (var (i, nav) in navs.indexed) 58 | Expanded( 59 | child: MkNavButton( 60 | active: index == i, 61 | text: nav, 62 | onTap: () { 63 | onSelect(i); 64 | }, 65 | ), 66 | ), 67 | ], 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/widgets/mk_refresh_loading_empty_wrapper.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 5 | 6 | import 'loading_weight.dart'; 7 | 8 | class MkRefreshLoadingEmptyBuilder extends HookConsumerWidget { 9 | const MkRefreshLoadingEmptyBuilder({ 10 | super.key, 11 | required this.onRefresh, 12 | required this.loading, 13 | required this.empty, 14 | required this.builder, 15 | }); 16 | 17 | final Future Function() onRefresh; 18 | final bool loading; 19 | final bool empty; 20 | final Widget Function(BuildContext context, BoxConstraints constraints) 21 | builder; 22 | 23 | @override 24 | Widget build(BuildContext context, WidgetRef ref) { 25 | var queryPadding = MediaQuery.paddingOf(context); 26 | return RefreshIndicator.adaptive( 27 | // 通知刷新指示器 28 | onRefresh: onRefresh, 29 | edgeOffset: queryPadding.top - 8, 30 | child: ScrollConfiguration( 31 | // 设置滑动配置,允许使用触摸和鼠标进行滑动 32 | behavior: ScrollConfiguration.of(context).copyWith(dragDevices: { 33 | PointerDeviceKind.touch, 34 | PointerDeviceKind.mouse, 35 | }), 36 | child: LoadingAndEmpty( 37 | loading: loading, 38 | empty: empty, 39 | refresh: onRefresh, 40 | child: LayoutBuilder( 41 | builder: builder, 42 | ), 43 | ), 44 | ), 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/widgets/mk_scaffold.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | 4 | import '../status/themes.dart'; 5 | 6 | class MkScaffold extends ConsumerWidget { 7 | const MkScaffold({super.key, this.header, required this.body}); 8 | 9 | final PreferredSizeWidget? header; 10 | final Widget body; 11 | 12 | @override 13 | Widget build(BuildContext context, WidgetRef ref) { 14 | MediaQueryData mediaQueryData = MediaQuery.of(context); 15 | // logger.d(mediaQueryData); 16 | var topPaddingHeight = mediaQueryData.padding.top; 17 | // 嵌套的 Scaffold 来保证 18 | Widget body = Scaffold( 19 | body: this.body, 20 | ); 21 | var themes = ref.watch(themeColorsProvider); 22 | return LayoutBuilder( 23 | builder: (context, constraints) { 24 | if (header != null) { 25 | topPaddingHeight += header!.preferredSize.height; 26 | body = MediaQuery( 27 | data: mediaQueryData.copyWith( 28 | padding: mediaQueryData.padding.copyWith( 29 | top: topPaddingHeight + 8, 30 | ), 31 | size: Size(constraints.maxWidth, constraints.maxHeight), 32 | ), 33 | child: body, 34 | ); 35 | } 36 | // 颜色与 RGBA(255,255,255,255) 叠加 37 | // 叠加颜色 38 | return Material( 39 | color: themes.isDark ? Colors.black : Colors.white, 40 | child: Material( 41 | color: themes.bgColor, 42 | child: Stack( 43 | children: [ 44 | body, 45 | if (header != null) 46 | Positioned( 47 | top: 0, 48 | left: 0, 49 | right: 0, 50 | child: header!, 51 | ), 52 | ], 53 | ), 54 | ), 55 | ); 56 | }, 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/widgets/mk_switch.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | 4 | import '../status/themes.dart'; 5 | 6 | class MkSwitch extends ConsumerWidget { 7 | const MkSwitch({ 8 | super.key, 9 | required this.value, 10 | this.onChanged, 11 | }); 12 | 13 | final bool value; 14 | final void Function(bool value)? onChanged; 15 | 16 | @override 17 | Widget build(BuildContext context, WidgetRef ref) { 18 | var themes = ref.watch(themeColorsProvider); 19 | return Switch( 20 | value: value, 21 | activeColor: themes.switchOnFgColor, 22 | inactiveThumbColor: themes.switchOffFgColor, 23 | activeTrackColor: themes.switchOnBgColor, 24 | inactiveTrackColor: themes.switchOffBgColor, 25 | trackOutlineColor: WidgetStateProperty.all(Colors.transparent), 26 | onChanged: onChanged, 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/widgets/sliver_load_more.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | import 'loading_weight.dart'; 4 | 5 | /// Sliver 滑动加载更多 6 | class SliverLoadMore extends StatefulWidget { 7 | const SliverLoadMore({ 8 | super.key, 9 | required this.hasMore, 10 | required this.onLoad, 11 | }); 12 | 13 | /// 是否还存在更多内容 14 | /// 当此项为false时,会停止调用onload方法 15 | final bool? hasMore; 16 | 17 | /// 加载更多的回调函数 18 | final Future Function() onLoad; 19 | 20 | @override 21 | State createState() => _SliverLoadMoreState(); 22 | } 23 | 24 | enum _LoadMoreStatus { 25 | inactive, 26 | done, 27 | loading, 28 | } 29 | 30 | class _SliverLoadMoreState extends State { 31 | _LoadMoreStatus currentState = _LoadMoreStatus.inactive; 32 | 33 | void handleNextState(double offset) { 34 | switch (currentState) { 35 | case _LoadMoreStatus.inactive: 36 | // 当滑动不超过阈值 37 | if (offset < 10) break; 38 | // 超过就触发回调并且更新状态 39 | currentState = _LoadMoreStatus.loading; 40 | // isTriggered = true; 41 | widget.onLoad().whenComplete(() { 42 | // 当加载完成之后回退状态 43 | currentState = _LoadMoreStatus.done; 44 | // isTriggered = false; 45 | // 判断加载动画是否还在显示,如果还在显示,就继续加载 46 | if (context.mounted) { 47 | // 获取当前的滚动控制器 48 | } 49 | }); 50 | break; 51 | case _LoadMoreStatus.loading: 52 | break; 53 | case _LoadMoreStatus.done: 54 | // 执行完毕之后,检查offset是是否为1 55 | // 这里判断为1是因为在滑动的时候,会有一点点的误差,导致offset某些情况不会为0,而是比0大一点点 56 | if (offset <= 1) { 57 | currentState = _LoadMoreStatus.inactive; 58 | } 59 | } 60 | } 61 | 62 | @override 63 | Widget build(BuildContext context) { 64 | return SliverLayoutBuilder( 65 | builder: (context, constraints) { 66 | // 没有更多了 67 | if (!(widget.hasMore ?? true)) { 68 | return const SliverToBoxAdapter( 69 | child: SizedBox( 70 | height: 10, 71 | ), 72 | ); 73 | } 74 | 75 | // 更新状态 76 | Future(() { 77 | handleNextState(constraints.remainingPaintExtent); 78 | }); 79 | 80 | // 显示当前状态 81 | return SliverToBoxAdapter( 82 | child: LayoutBuilder( 83 | builder: (context, constraints) { 84 | return Padding( 85 | padding: EdgeInsets.all(16.0), 86 | child: Center( 87 | child: LoadingCircularProgress(), 88 | ), 89 | ); 90 | }, 91 | ), 92 | ); 93 | }, 94 | ); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /lib/widgets/sliver_presistent_header.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class HeaderDelegate extends SliverPersistentHeaderDelegate { 4 | const HeaderDelegate({required this.child, this.extent = 50}); 5 | 6 | final Widget child; 7 | final double extent; 8 | 9 | @override 10 | Widget build( 11 | BuildContext context, double shrinkOffset, bool overlapsContent) { 12 | return child; 13 | } 14 | 15 | @override 16 | double get maxExtent => extent; 17 | 18 | @override 19 | double get minExtent => extent; 20 | 21 | @override 22 | bool shouldRebuild(covariant HeaderDelegate oldDelegate) => 23 | child != oldDelegate.child; 24 | } 25 | -------------------------------------------------------------------------------- /lib/widgets/user_select_dialog/user_select_dialog_state.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:moekey/status/dio.dart'; 4 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 5 | 6 | import '../../apis/models/user_full.dart'; 7 | import '../../status/server.dart'; 8 | import '../../status/user.dart'; 9 | 10 | part 'user_select_dialog_state.g.dart'; 11 | 12 | @riverpod 13 | class UserSelectDialogState extends _$UserSelectDialogState { 14 | String name = ""; 15 | String host = ""; 16 | Timer? timer; 17 | 18 | @override 19 | FutureOr> build() async { 20 | return loadFollowing(); 21 | } 22 | 23 | FutureOr> loadFollowing() async { 24 | var user = ref.read(currentLoginUserProvider); 25 | var userList = await ref.read(userFollowingProvider(user?.id ?? "").future); 26 | var list = []; 27 | for (var item in userList) { 28 | if (item.followee != null) { 29 | list.add(item.followee!); 30 | } 31 | } 32 | return list; 33 | } 34 | 35 | search({String? name, String? host}) { 36 | // 防抖 37 | timer?.cancel(); 38 | state = const AsyncLoading(); 39 | timer = Timer(const Duration(seconds: 1), () { 40 | _search(name: name, host: host); 41 | }); 42 | } 43 | 44 | _search({String? name, String? host}) async { 45 | if (name != null) { 46 | this.name = name; 47 | } 48 | if (host != null) { 49 | this.host = host; 50 | } 51 | 52 | if (this.host != "" || this.name != "") { 53 | var http = await ref.read(httpProvider.future); 54 | var user = ref.read(currentLoginUserProvider); 55 | var data = 56 | await http.post("/users/search-by-username-and-host", data: { 57 | "username": this.name, 58 | "host": this.host, 59 | "limit": 30, 60 | "detail": true, 61 | "i": user!.token 62 | }); 63 | 64 | state = AsyncData(List.from( 65 | data.data!.map((e) => UserFullModel.fromJson(e)))); 66 | } else { 67 | var data = await loadFollowing(); 68 | state = AsyncData(data); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /linux/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral 2 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | void fl_register_plugins(FlPluginRegistry* registry) { 14 | g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar = 15 | fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin"); 16 | media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar); 17 | g_autoptr(FlPluginRegistrar) media_kit_video_registrar = 18 | fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitVideoPlugin"); 19 | media_kit_video_plugin_register_with_registrar(media_kit_video_registrar); 20 | g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = 21 | fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); 22 | url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); 23 | } 24 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void fl_register_plugins(FlPluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | media_kit_libs_linux 7 | media_kit_video 8 | url_launcher_linux 9 | ) 10 | 11 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 12 | ) 13 | 14 | set(PLUGIN_BUNDLED_LIBRARIES) 15 | 16 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 17 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) 18 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 19 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 20 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 21 | endforeach(plugin) 22 | 23 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 24 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) 25 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 26 | endforeach(ffi_plugin) 27 | -------------------------------------------------------------------------------- /linux/main.cc: -------------------------------------------------------------------------------- 1 | #include "my_application.h" 2 | 3 | int main(int argc, char** argv) { 4 | g_autoptr(MyApplication) app = my_application_new(); 5 | return g_application_run(G_APPLICATION(app), argc, argv); 6 | } 7 | -------------------------------------------------------------------------------- /linux/my_application.h: -------------------------------------------------------------------------------- 1 | #ifndef FLUTTER_MY_APPLICATION_H_ 2 | #define FLUTTER_MY_APPLICATION_H_ 3 | 4 | #include 5 | 6 | G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, 7 | GtkApplication) 8 | 9 | /** 10 | * my_application_new: 11 | * 12 | * Creates a new Flutter-based application. 13 | * 14 | * Returns: a new #MyApplication. 15 | */ 16 | MyApplication* my_application_new(); 17 | 18 | #endif // FLUTTER_MY_APPLICATION_H_ 19 | -------------------------------------------------------------------------------- /macos/.gitignore: -------------------------------------------------------------------------------- 1 | # Flutter-related 2 | **/Flutter/ephemeral/ 3 | **/Pods/ 4 | 5 | # Xcode-related 6 | **/dgph 7 | **/xcuserdata/ 8 | -------------------------------------------------------------------------------- /macos/Flutter/Flutter-Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Flutter/Flutter-Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Flutter/GeneratedPluginRegistrant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | import FlutterMacOS 6 | import Foundation 7 | 8 | import file_picker 9 | import gal 10 | import media_kit_libs_macos_video 11 | import media_kit_video 12 | import package_info_plus 13 | import path_provider_foundation 14 | import share_plus 15 | import shared_preferences_foundation 16 | import url_launcher_macos 17 | import volume_controller 18 | import wakelock_plus 19 | 20 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 21 | FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) 22 | GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin")) 23 | MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin")) 24 | MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin")) 25 | FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) 26 | PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) 27 | SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) 28 | SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) 29 | UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) 30 | VolumeControllerPlugin.register(with: registry.registrar(forPlugin: "VolumeControllerPlugin")) 31 | WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) 32 | } 33 | -------------------------------------------------------------------------------- /macos/Podfile: -------------------------------------------------------------------------------- 1 | platform :osx, '11.0' 2 | 3 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 4 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 5 | 6 | project 'Runner', { 7 | 'Debug' => :debug, 8 | 'Profile' => :release, 9 | 'Release' => :release, 10 | } 11 | 12 | def flutter_root 13 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) 14 | unless File.exist?(generated_xcode_build_settings_path) 15 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" 16 | end 17 | 18 | File.foreach(generated_xcode_build_settings_path) do |line| 19 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 20 | return matches[1].strip if matches 21 | end 22 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" 23 | end 24 | 25 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 26 | 27 | flutter_macos_podfile_setup 28 | 29 | target 'Runner' do 30 | use_frameworks! 31 | use_modular_headers! 32 | 33 | flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) 34 | target 'RunnerTests' do 35 | inherit! :search_paths 36 | end 37 | end 38 | 39 | post_install do |installer| 40 | installer.pods_project.targets.each do |target| 41 | flutter_additional_macos_build_settings(target) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | @NSApplicationMain 5 | class AppDelegate: FlutterAppDelegate { 6 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 7 | return true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "version": 1, 4 | "author": "xcode" 5 | }, 6 | "images": [ 7 | { 8 | "size": "16x16", 9 | "idiom": "mac", 10 | "filename": "app_icon_16.png", 11 | "scale": "1x" 12 | }, 13 | { 14 | "size": "16x16", 15 | "idiom": "mac", 16 | "filename": "app_icon_32.png", 17 | "scale": "2x" 18 | }, 19 | { 20 | "size": "32x32", 21 | "idiom": "mac", 22 | "filename": "app_icon_32.png", 23 | "scale": "1x" 24 | }, 25 | { 26 | "size": "32x32", 27 | "idiom": "mac", 28 | "filename": "app_icon_64.png", 29 | "scale": "2x" 30 | }, 31 | { 32 | "size": "128x128", 33 | "idiom": "mac", 34 | "filename": "app_icon_128.png", 35 | "scale": "1x" 36 | }, 37 | { 38 | "size": "128x128", 39 | "idiom": "mac", 40 | "filename": "app_icon_256.png", 41 | "scale": "2x" 42 | }, 43 | { 44 | "size": "256x256", 45 | "idiom": "mac", 46 | "filename": "app_icon_256.png", 47 | "scale": "1x" 48 | }, 49 | { 50 | "size": "256x256", 51 | "idiom": "mac", 52 | "filename": "app_icon_512.png", 53 | "scale": "2x" 54 | }, 55 | { 56 | "size": "512x512", 57 | "idiom": "mac", 58 | "filename": "app_icon_512.png", 59 | "scale": "1x" 60 | }, 61 | { 62 | "size": "512x512", 63 | "idiom": "mac", 64 | "filename": "app_icon_1024.png", 65 | "scale": "2x" 66 | } 67 | ] 68 | } -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png -------------------------------------------------------------------------------- /macos/Runner/Configs/AppInfo.xcconfig: -------------------------------------------------------------------------------- 1 | // Application-level settings for the Runner target. 2 | // 3 | // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the 4 | // future. If not, the values below would default to using the project name when this becomes a 5 | // 'flutter create' template. 6 | 7 | // The application's name. By default this is also the title of the Flutter window. 8 | PRODUCT_NAME = MoeKey 9 | 10 | // The application's bundle identifier 11 | PRODUCT_BUNDLE_IDENTIFIER = love.moegirl.moekey 12 | 13 | // The copyright displayed in application information 14 | PRODUCT_COPYRIGHT = Copyright © 2024 love.moegirl. All rights reserved. 15 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Debug.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Release.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Warnings.xcconfig: -------------------------------------------------------------------------------- 1 | WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings 2 | GCC_WARN_UNDECLARED_SELECTOR = YES 3 | CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES 4 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE 5 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES 6 | CLANG_WARN_PRAGMA_PACK = YES 7 | CLANG_WARN_STRICT_PROTOTYPES = YES 8 | CLANG_WARN_COMMA = YES 9 | GCC_WARN_STRICT_SELECTOR_MATCH = YES 10 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES 11 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES 12 | GCC_WARN_SHADOW = YES 13 | CLANG_WARN_UNREACHABLE_CODE = YES 14 | -------------------------------------------------------------------------------- /macos/Runner/DebugProfile.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | com.apple.security.network.client 10 | 11 | com.apple.security.network.server 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /macos/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | $(PRODUCT_COPYRIGHT) 27 | NSMainNibFile 28 | MainMenu 29 | NSPrincipalClass 30 | NSApplication 31 | 32 | 33 | -------------------------------------------------------------------------------- /macos/Runner/MainFlutterWindow.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | class MainFlutterWindow: NSWindow { 5 | override func awakeFromNib() { 6 | let flutterViewController = FlutterViewController() 7 | let windowFrame = self.frame 8 | self.contentViewController = flutterViewController 9 | self.setFrame(windowFrame, display: true) 10 | 11 | RegisterGeneratedPlugins(registry: flutterViewController) 12 | 13 | super.awakeFromNib() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /macos/Runner/Release.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.network.client 8 | 9 | com.apple.security.network.server 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /macos/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import FlutterMacOS 2 | import Cocoa 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility in the flutter_test package. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | import 'package:moekey/main.dart'; 11 | 12 | void main() { 13 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 14 | // Build our app and trigger a frame. 15 | await tester.pumpWidget(const MyApp()); 16 | 17 | // Verify that our counter starts at 0. 18 | expect(find.text('0'), findsOneWidget); 19 | expect(find.text('1'), findsNothing); 20 | 21 | // Tap the '+' icon and trigger a frame. 22 | await tester.tap(find.byIcon(Icons.add)); 23 | await tester.pump(); 24 | 25 | // Verify that our counter has incremented. 26 | expect(find.text('0'), findsNothing); 27 | expect(find.text('1'), findsOneWidget); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /windows/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral/ 2 | 3 | # Visual Studio user-specific files. 4 | *.suo 5 | *.user 6 | *.userosscache 7 | *.sln.docstates 8 | 9 | # Visual Studio build-related files. 10 | x64/ 11 | x86/ 12 | 13 | # Visual Studio cache files 14 | # files ending in .cache can be ignored 15 | *.[Cc]ache 16 | # but keep track of directories ending in .cache 17 | !*.[Cc]ache/ 18 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | void RegisterPlugins(flutter::PluginRegistry* registry) { 17 | GalPluginCApiRegisterWithRegistrar( 18 | registry->GetRegistrarForPlugin("GalPluginCApi")); 19 | MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar( 20 | registry->GetRegistrarForPlugin("MediaKitLibsWindowsVideoPluginCApi")); 21 | MediaKitVideoPluginCApiRegisterWithRegistrar( 22 | registry->GetRegistrarForPlugin("MediaKitVideoPluginCApi")); 23 | SharePlusWindowsPluginCApiRegisterWithRegistrar( 24 | registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); 25 | UrlLauncherWindowsRegisterWithRegistrar( 26 | registry->GetRegistrarForPlugin("UrlLauncherWindows")); 27 | VolumeControllerPluginCApiRegisterWithRegistrar( 28 | registry->GetRegistrarForPlugin("VolumeControllerPluginCApi")); 29 | } 30 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void RegisterPlugins(flutter::PluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | gal 7 | media_kit_libs_windows_video 8 | media_kit_video 9 | share_plus 10 | url_launcher_windows 11 | volume_controller 12 | ) 13 | 14 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 15 | ) 16 | 17 | set(PLUGIN_BUNDLED_LIBRARIES) 18 | 19 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 20 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) 21 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 22 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 23 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 24 | endforeach(plugin) 25 | 26 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 27 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) 28 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 29 | endforeach(ffi_plugin) 30 | -------------------------------------------------------------------------------- /windows/runner/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | project(runner LANGUAGES CXX) 3 | 4 | # Define the application target. To change its name, change BINARY_NAME in the 5 | # top-level CMakeLists.txt, not the value here, or `flutter run` will no longer 6 | # work. 7 | # 8 | # Any new source files that you add to the application should be added here. 9 | add_executable(${BINARY_NAME} WIN32 10 | "flutter_window.cpp" 11 | "main.cpp" 12 | "utils.cpp" 13 | "win32_window.cpp" 14 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 15 | "Runner.rc" 16 | "runner.exe.manifest" 17 | ) 18 | 19 | # Apply the standard set of build settings. This can be removed for applications 20 | # that need different build settings. 21 | apply_standard_settings(${BINARY_NAME}) 22 | 23 | # Add preprocessor definitions for the build version. 24 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") 25 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") 26 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") 27 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") 28 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") 29 | 30 | # Disable Windows macros that collide with C++ standard library functions. 31 | target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") 32 | 33 | # Add dependency libraries and include directories. Add any application-specific 34 | # dependencies here. 35 | target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) 36 | target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") 37 | target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") 38 | 39 | # Run the Flutter tool portions of the build. This must not be removed. 40 | add_dependencies(${BINARY_NAME} flutter_assemble) 41 | -------------------------------------------------------------------------------- /windows/runner/flutter_window.cpp: -------------------------------------------------------------------------------- 1 | #include "flutter_window.h" 2 | 3 | #include 4 | 5 | #include "flutter/generated_plugin_registrant.h" 6 | 7 | FlutterWindow::FlutterWindow(const flutter::DartProject& project) 8 | : project_(project) {} 9 | 10 | FlutterWindow::~FlutterWindow() {} 11 | 12 | bool FlutterWindow::OnCreate() { 13 | if (!Win32Window::OnCreate()) { 14 | return false; 15 | } 16 | 17 | RECT frame = GetClientArea(); 18 | 19 | // The size here must match the window dimensions to avoid unnecessary surface 20 | // creation / destruction in the startup path. 21 | flutter_controller_ = std::make_unique( 22 | frame.right - frame.left, frame.bottom - frame.top, project_); 23 | // Ensure that basic setup of the controller was successful. 24 | if (!flutter_controller_->engine() || !flutter_controller_->view()) { 25 | return false; 26 | } 27 | RegisterPlugins(flutter_controller_->engine()); 28 | SetChildContent(flutter_controller_->view()->GetNativeWindow()); 29 | 30 | flutter_controller_->engine()->SetNextFrameCallback([&]() { 31 | this->Show(); 32 | }); 33 | 34 | // Flutter can complete the first frame before the "show window" callback is 35 | // registered. The following call ensures a frame is pending to ensure the 36 | // window is shown. It is a no-op if the first frame hasn't completed yet. 37 | flutter_controller_->ForceRedraw(); 38 | 39 | return true; 40 | } 41 | 42 | void FlutterWindow::OnDestroy() { 43 | if (flutter_controller_) { 44 | flutter_controller_ = nullptr; 45 | } 46 | 47 | Win32Window::OnDestroy(); 48 | } 49 | 50 | LRESULT 51 | FlutterWindow::MessageHandler(HWND hwnd, UINT const message, 52 | WPARAM const wparam, 53 | LPARAM const lparam) noexcept { 54 | // Give Flutter, including plugins, an opportunity to handle window messages. 55 | if (flutter_controller_) { 56 | std::optional result = 57 | flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, 58 | lparam); 59 | if (result) { 60 | return *result; 61 | } 62 | } 63 | 64 | switch (message) { 65 | case WM_FONTCHANGE: 66 | flutter_controller_->engine()->ReloadSystemFonts(); 67 | break; 68 | } 69 | 70 | return Win32Window::MessageHandler(hwnd, message, wparam, lparam); 71 | } 72 | -------------------------------------------------------------------------------- /windows/runner/flutter_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_FLUTTER_WINDOW_H_ 2 | #define RUNNER_FLUTTER_WINDOW_H_ 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "win32_window.h" 10 | 11 | // A window that does nothing but host a Flutter view. 12 | class FlutterWindow : public Win32Window { 13 | public: 14 | // Creates a new FlutterWindow hosting a Flutter view running |project|. 15 | explicit FlutterWindow(const flutter::DartProject& project); 16 | virtual ~FlutterWindow(); 17 | 18 | protected: 19 | // Win32Window: 20 | bool OnCreate() override; 21 | void OnDestroy() override; 22 | LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, 23 | LPARAM const lparam) noexcept override; 24 | 25 | private: 26 | // The project to run. 27 | flutter::DartProject project_; 28 | 29 | // The Flutter instance hosted by this window. 30 | std::unique_ptr flutter_controller_; 31 | }; 32 | 33 | #endif // RUNNER_FLUTTER_WINDOW_H_ 34 | -------------------------------------------------------------------------------- /windows/runner/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "flutter_window.h" 6 | #include "utils.h" 7 | 8 | int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, 9 | _In_ wchar_t *command_line, _In_ int show_command) { 10 | // Attach to console when present (e.g., 'flutter run') or create a 11 | // new console when running with a debugger. 12 | if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { 13 | CreateAndAttachConsole(); 14 | } 15 | 16 | // Initialize COM, so that it is available for use in the library and/or 17 | // plugins. 18 | ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); 19 | 20 | flutter::DartProject project(L"data"); 21 | 22 | std::vector command_line_arguments = 23 | GetCommandLineArguments(); 24 | 25 | project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); 26 | 27 | FlutterWindow window(project); 28 | Win32Window::Point origin(10, 10); 29 | Win32Window::Size size(1280, 720); 30 | if (!window.Create(L"MoeKey", origin, size)) { 31 | return EXIT_FAILURE; 32 | } 33 | window.SetQuitOnClose(true); 34 | 35 | ::MSG msg; 36 | while (::GetMessage(&msg, nullptr, 0, 0)) { 37 | ::TranslateMessage(&msg); 38 | ::DispatchMessage(&msg); 39 | } 40 | 41 | ::CoUninitialize(); 42 | return EXIT_SUCCESS; 43 | } 44 | -------------------------------------------------------------------------------- /windows/runner/resource.h: -------------------------------------------------------------------------------- 1 | //{{NO_DEPENDENCIES}} 2 | // Microsoft Visual C++ generated include file. 3 | // Used by Runner.rc 4 | // 5 | #define IDI_APP_ICON 101 6 | 7 | // Next default values for new objects 8 | // 9 | #ifdef APSTUDIO_INVOKED 10 | #ifndef APSTUDIO_READONLY_SYMBOLS 11 | #define _APS_NEXT_RESOURCE_VALUE 102 12 | #define _APS_NEXT_COMMAND_VALUE 40001 13 | #define _APS_NEXT_CONTROL_VALUE 1001 14 | #define _APS_NEXT_SYMED_VALUE 101 15 | #endif 16 | #endif 17 | -------------------------------------------------------------------------------- /windows/runner/resources/app_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeKeyDev/MoeKey/9c13243552caef3ac04fadee244f6521f23cbf9c/windows/runner/resources/app_icon.ico -------------------------------------------------------------------------------- /windows/runner/runner.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PerMonitorV2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /windows/runner/utils.cpp: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | void CreateAndAttachConsole() { 11 | if (::AllocConsole()) { 12 | FILE *unused; 13 | if (freopen_s(&unused, "CONOUT$", "w", stdout)) { 14 | _dup2(_fileno(stdout), 1); 15 | } 16 | if (freopen_s(&unused, "CONOUT$", "w", stderr)) { 17 | _dup2(_fileno(stdout), 2); 18 | } 19 | std::ios::sync_with_stdio(); 20 | FlutterDesktopResyncOutputStreams(); 21 | } 22 | } 23 | 24 | std::vector GetCommandLineArguments() { 25 | // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. 26 | int argc; 27 | wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); 28 | if (argv == nullptr) { 29 | return std::vector(); 30 | } 31 | 32 | std::vector command_line_arguments; 33 | 34 | // Skip the first argument as it's the binary name. 35 | for (int i = 1; i < argc; i++) { 36 | command_line_arguments.push_back(Utf8FromUtf16(argv[i])); 37 | } 38 | 39 | ::LocalFree(argv); 40 | 41 | return command_line_arguments; 42 | } 43 | 44 | std::string Utf8FromUtf16(const wchar_t* utf16_string) { 45 | if (utf16_string == nullptr) { 46 | return std::string(); 47 | } 48 | int target_length = ::WideCharToMultiByte( 49 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 50 | -1, nullptr, 0, nullptr, nullptr) 51 | -1; // remove the trailing null character 52 | int input_length = (int)wcslen(utf16_string); 53 | std::string utf8_string; 54 | if (target_length <= 0 || target_length > utf8_string.max_size()) { 55 | return utf8_string; 56 | } 57 | utf8_string.resize(target_length); 58 | int converted_length = ::WideCharToMultiByte( 59 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 60 | input_length, utf8_string.data(), target_length, nullptr, nullptr); 61 | if (converted_length == 0) { 62 | return std::string(); 63 | } 64 | return utf8_string; 65 | } 66 | -------------------------------------------------------------------------------- /windows/runner/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_UTILS_H_ 2 | #define RUNNER_UTILS_H_ 3 | 4 | #include 5 | #include 6 | 7 | // Creates a console for the process, and redirects stdout and stderr to 8 | // it for both the runner and the Flutter library. 9 | void CreateAndAttachConsole(); 10 | 11 | // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string 12 | // encoded in UTF-8. Returns an empty std::string on failure. 13 | std::string Utf8FromUtf16(const wchar_t* utf16_string); 14 | 15 | // Gets the command line arguments passed in as a std::vector, 16 | // encoded in UTF-8. Returns an empty std::vector on failure. 17 | std::vector GetCommandLineArguments(); 18 | 19 | #endif // RUNNER_UTILS_H_ 20 | --------------------------------------------------------------------------------