├── .github ├── ISSUE_TEMPLATE │ ├── _bug_report.yaml │ ├── _feature_request.yaml │ ├── blank_issue.yaml │ └── config.yml └── workflows │ ├── build_release.yml │ └── pull_request_tests.yml ├── .gitignore ├── LICENSE ├── PRIVACY_POLICY.md ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── one │ │ │ │ └── jwr │ │ │ │ └── interstellar │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-hdpi │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ ├── drawable-mdpi │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable-xhdpi │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ ├── drawable-xxhdpi │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ ├── drawable-xxxhdpi │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ └── ic_launcher.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.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 ├── icons │ ├── github.png │ ├── google-play-feature-graphic.png │ ├── google-play-feature-graphic.svg │ ├── logo-foreground.png │ ├── logo-foreground.svg │ ├── logo-monochrome.png │ ├── logo-monochrome.svg │ ├── logo.png │ ├── logo.svg │ ├── matrix.png │ └── mbin.png ├── readme │ ├── Flathub-badge.png │ ├── GooglePlay-badge.png │ └── IzzyOnDroid-badge.png └── screenshots │ ├── desktop-1.png │ ├── desktop-2.png │ ├── desktop-3.png │ ├── desktop-4.png │ ├── mobile-1.png │ ├── mobile-2.png │ ├── mobile-3.png │ └── mobile-4.png ├── fastlane └── metadata │ └── android │ └── en-US │ ├── full_description.txt │ ├── images │ ├── featureGraphic.png │ ├── icon.png │ └── phoneScreenshots │ │ ├── 01.png │ │ ├── 02.png │ │ ├── 03.png │ │ └── 04.png │ └── short_description.txt ├── ios ├── .gitignore ├── 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 ├── l10n.yaml ├── lib ├── l10n │ ├── app_da.arb │ ├── app_de.arb │ ├── app_en.arb │ ├── app_es.arb │ ├── app_fi.arb │ ├── app_fr.arb │ ├── app_id.arb │ ├── app_it.arb │ ├── app_nl.arb │ ├── app_pl.arb │ ├── app_ru.arb │ ├── app_ta.arb │ ├── app_tr.arb │ ├── app_uk.arb │ └── app_zh.arb ├── main.dart └── src │ ├── api │ ├── api.dart │ ├── bookmark.dart │ ├── client.dart │ ├── comments.dart │ ├── community.dart │ ├── community_moderation.dart │ ├── domains.dart │ ├── feed_source.dart │ ├── messages.dart │ ├── microblogs.dart │ ├── moderation.dart │ ├── notifications.dart │ ├── oauth.dart │ ├── search.dart │ ├── threads.dart │ └── users.dart │ ├── app.dart │ ├── app_home.dart │ ├── controller │ ├── account.dart │ ├── controller.dart │ ├── database.dart │ ├── filter_list.dart │ ├── profile.dart │ └── server.dart │ ├── init_push_notifications.dart │ ├── models │ ├── bookmark_list.dart │ ├── comment.dart │ ├── community.dart │ ├── config_share.dart │ ├── domain.dart │ ├── image.dart │ ├── message.dart │ ├── notification.dart │ ├── post.dart │ ├── search.dart │ └── user.dart │ ├── screens │ ├── account │ │ ├── account_screen.dart │ │ ├── messages │ │ │ ├── message_item.dart │ │ │ ├── message_thread_item.dart │ │ │ ├── message_thread_screen.dart │ │ │ └── messages_screen.dart │ │ ├── notification │ │ │ ├── notification_badge.dart │ │ │ ├── notification_count_controller.dart │ │ │ ├── notification_item.dart │ │ │ └── notification_screen.dart │ │ ├── profile_edit_screen.dart │ │ └── self_feed.dart │ ├── explore │ │ ├── community_mod_panel.dart │ │ ├── community_owner_panel.dart │ │ ├── community_screen.dart │ │ ├── domain_screen.dart │ │ ├── explore_screen.dart │ │ ├── explore_screen_item.dart │ │ ├── user_item.dart │ │ └── user_screen.dart │ ├── feed │ │ ├── create_screen.dart │ │ ├── feed_screen.dart │ │ ├── nav_drawer.dart │ │ ├── post_comment.dart │ │ ├── post_comment_screen.dart │ │ ├── post_item.dart │ │ └── post_page.dart │ └── settings │ │ ├── about_screen.dart │ │ ├── account_migration.dart │ │ ├── account_reset.dart │ │ ├── account_selection.dart │ │ ├── behavior_screen.dart │ │ ├── data_utilities.dart │ │ ├── display_screen.dart │ │ ├── feed_actions_screen.dart │ │ ├── feed_defaults_screen.dart │ │ ├── filter_lists_screen.dart │ │ ├── login_confirm.dart │ │ ├── login_select.dart │ │ ├── notification_screen.dart │ │ ├── profile_selection.dart │ │ └── settings_screen.dart │ ├── utils │ ├── breakpoints.dart │ ├── debouncer.dart │ ├── jwt_http_client.dart │ ├── language.dart │ ├── models.dart │ ├── share.dart │ ├── utils.dart │ └── variables.dart │ └── widgets │ ├── actions.dart │ ├── avatar.dart │ ├── ban_dialog.dart │ ├── blur.dart │ ├── community_picker.dart │ ├── content_item │ ├── content_item.dart │ ├── content_item_link_panel.dart │ ├── content_menu.dart │ └── swipe_item.dart │ ├── display_name.dart │ ├── error_page.dart │ ├── floating_menu.dart │ ├── image.dart │ ├── image_selector.dart │ ├── list_tile_select.dart │ ├── list_tile_switch.dart │ ├── loading_button.dart │ ├── loading_list_tile.dart │ ├── loading_template.dart │ ├── markdown │ ├── drafts_controller.dart │ ├── markdown.dart │ ├── markdown_config_share.dart │ ├── markdown_editor.dart │ ├── markdown_mention.dart │ ├── markdown_spoiler.dart │ ├── markdown_subscript_superscript.dart │ └── markdown_video.dart │ ├── notification_control_segment.dart │ ├── open_webpage.dart │ ├── password_editor.dart │ ├── redirect_listen.dart │ ├── report_content.dart │ ├── scaffold.dart │ ├── selection_menu.dart │ ├── server_software_indicator.dart │ ├── settings_header.dart │ ├── star_button.dart │ ├── subordinate_scroll.dart │ ├── subscription_button.dart │ ├── super_hero.dart │ ├── text_editor.dart │ ├── user_status_icons.dart │ ├── video.dart │ └── wrapper.dart ├── linux ├── .gitignore ├── CMakeLists.txt ├── appimage │ └── interstellar.desktop ├── flutter │ ├── CMakeLists.txt │ ├── generated_plugin_registrant.cc │ ├── generated_plugin_registrant.h │ └── generated_plugins.cmake └── runner │ ├── CMakeLists.txt │ ├── main.cc │ ├── my_application.cc │ └── my_application.h ├── macos ├── .gitignore ├── Flutter │ ├── Flutter-Debug.xcconfig │ ├── Flutter-Release.xcconfig │ └── GeneratedPluginRegistrant.swift ├── Podfile ├── Podfile.lock ├── 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 ├── pubspec.lock ├── pubspec.yaml ├── scripts ├── build-appimage.sh └── build-windows-setup.iss └── 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 /.github/ISSUE_TEMPLATE/_bug_report.yaml: -------------------------------------------------------------------------------- 1 | name: 🐛 Bug report 2 | description: Create a report to help us improve 3 | labels: ['bug'] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for taking the time to fill out this issue 🙏. 9 | Before you submit, please search the issue tracker to prevent duplicates as a similar issue might already exist. 10 | - type: textarea 11 | id: description 12 | attributes: 13 | label: Describe the bug 14 | description: Provide a clear and concise description of the bug. 15 | validations: 16 | required: true 17 | - type: textarea 18 | id: steps 19 | attributes: 20 | label: Steps to Reproduce 21 | description: Describe the steps we have to take to reproduce the behavior. 22 | placeholder: | 23 | 1. Go to '...' 24 | 2. Click on '....' 25 | 3. Scroll down to '....' 26 | 4. See error 27 | - type: input 28 | id: version 29 | attributes: 30 | label: Version 31 | placeholder: 1.2.3 32 | validations: 33 | required: true 34 | - type: dropdown 35 | id: platform-client 36 | attributes: 37 | label: What client platform(s) are you seeing the problem on? 38 | multiple: true 39 | options: 40 | - Android 41 | - iOS 42 | - Linux 43 | - macOS 44 | - Windows 45 | validations: 46 | required: true 47 | - type: dropdown 48 | id: platform-server 49 | attributes: 50 | label: What server platform(s) are you seeing the problem on? 51 | multiple: true 52 | options: 53 | - Mbin 54 | - Lemmy 55 | - PieFed 56 | validations: 57 | required: true 58 | - type: textarea 59 | id: additional 60 | attributes: 61 | label: Additional context 62 | description: Add any other context about the problem here. 63 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/_feature_request.yaml: -------------------------------------------------------------------------------- 1 | name: ✨ Feature request 2 | description: Suggest an idea for this project 3 | labels: ['enhancement'] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for taking the time to fill out this issue 🙏. 9 | Before you submit, please search the issue tracker to prevent duplicates as a similar issue might already exist. 10 | - type: textarea 11 | id: description 12 | attributes: 13 | label: Describe the feature 14 | description: A clear and concise description of what you want and what your use case is. 15 | validations: 16 | required: true 17 | - type: textarea 18 | id: additional 19 | attributes: 20 | label: Additional context 21 | description: Add any other context about the feature request here. 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/blank_issue.yaml: -------------------------------------------------------------------------------- 1 | name: Blank issue 2 | description: Don't see your issue here? Open a blank issue 3 | body: 4 | - type: markdown 5 | attributes: 6 | value: | 7 | Thanks for taking the time to fill out this issue 🙏. 8 | Before you submit, please search the issue tracker to prevent duplicates as a similar issue might already exist. 9 | - type: textarea 10 | id: description 11 | attributes: 12 | label: Description 13 | validations: 14 | required: true 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/workflows/pull_request_tests.yml: -------------------------------------------------------------------------------- 1 | name: Pull request tests 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: pull_request 7 | 8 | jobs: 9 | test-android-build: 10 | name: Test android build 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Install android dependencies 14 | if: matrix.target == 'android' 15 | uses: actions/setup-java@v4 16 | with: 17 | java-version: '21' 18 | distribution: temurin 19 | 20 | - name: Setup Flutter 21 | uses: subosito/flutter-action@v2 22 | 23 | - name: Checkout code 24 | uses: actions/checkout@v4 25 | 26 | - name: Build Flutter app 27 | run: | 28 | dart run build_runner build 29 | flutter build -v apk --split-per-abi 30 | test-linux-build: 31 | name: Test linux build 32 | runs-on: ubuntu-latest 33 | steps: 34 | - name: Install linux dependencies 35 | run: | 36 | sudo apt-get update 37 | sudo apt-get install -y libgtk-3-dev libx11-dev pkg-config cmake ninja-build libblkid-dev libmpv-dev mpv 38 | 39 | - name: Setup Flutter 40 | uses: subosito/flutter-action@v2 41 | 42 | - name: Checkout code 43 | uses: actions/checkout@v3 44 | 45 | - name: Build Flutter app 46 | run: | 47 | dart run build_runner build 48 | flutter build -v linux 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .build/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | .swiftpm/ 13 | migrate_working_dir/ 14 | 15 | # IntelliJ related 16 | *.iml 17 | *.ipr 18 | *.iws 19 | .idea/ 20 | 21 | # The .vscode folder contains launch configuration and tasks you configure in 22 | # VS Code which you may wish to be included in version control, so this line 23 | # is commented out by default. 24 | .vscode/ 25 | 26 | # Flutter/Dart/Pub related 27 | **/doc/api/ 28 | **/ios/Flutter/.last_build_id 29 | .dart_tool/ 30 | .flutter-plugins 31 | .flutter-plugins-dependencies 32 | .packages 33 | .pub-cache/ 34 | .pub/ 35 | /build/ 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | 43 | # Android Studio will place build artifacts here 44 | /android/app/debug 45 | /android/app/profile 46 | /android/app/release 47 | /android/app/.cxx 48 | 49 | # Generated code 50 | *.freezed.dart 51 | *.g.dart 52 | app_localizations*.dart 53 | -------------------------------------------------------------------------------- /PRIVACY_POLICY.md: -------------------------------------------------------------------------------- 1 | # Privacy Policy 2 | 3 | Last updated: January 24, 2024 4 | 5 | This Privacy Policy describes Our policies and procedures on the collection, use and disclosure of Your information when You use the Service and tells You about Your privacy rights and how the law protects You. 6 | 7 | ## Data Collection 8 | 9 | The Interstellar app does not collect any personally identifiable information (PII) or personal data during its usage. We do not track, store, or transmit any data that can be used to identify individual users. 10 | 11 | All data generated or entered within the Interstellar app is stored locally on your device. This includes any files, documents, settings, or other information created or accessed through the app. We do not upload or sync your data to any external servers or cloud services. 12 | 13 | ## Permissions 14 | 15 | The Interstellar app requires certain permissions to access device features necessary for its functionality. These permissions may include access to the device's storage or other relevant hardware components. However, these permissions are solely used within the app and do not involve the collection or transmission of data to external sources. 16 | 17 | ## Links to Other Websites 18 | 19 | Our Service may contain links to other websites that are not operated by Us. If You click on a third party link, You will be directed to that third party's site. We strongly advise You to review the Privacy Policy of every site You visit. 20 | 21 | We have no control over and assume no responsibility for the content, privacy policies or practices of any third party sites or services. 22 | 23 | ## Changes to this Privacy Policy 24 | 25 | We may update Our Privacy Policy from time to time. We will notify You of any changes by posting the new Privacy Policy on this page. 26 | 27 | You are advised to review this Privacy Policy periodically for any changes. Changes to this Privacy Policy are effective when they are posted on this page. 28 | 29 | ## Contact Us 30 | 31 | If you have any questions about this Privacy Policy, You can contact us: 32 | 33 | - By visiting this page: [https://github.com/interstellar-app/interstellar/issues/new/choose](https://github.com/interstellar-app/interstellar/issues/new/choose) 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Interstellar 2 | 3 | An app for Mbin/Lemmy/PieFed, connecting you to the fediverse. 4 | 5 | ## Downloads 6 | 7 | Interstellar supports Android, Linux, and Windows, with more to come. 8 | 9 | [![](assets/readme/GooglePlay-badge.png)](https://play.google.com/store/apps/details?id=one.jwr.interstellar) 10 | [![](assets/readme/IzzyOnDroid-badge.png)](https://apt.izzysoft.de/fdroid/index/apk/one.jwr.interstellar) 11 | [![](assets/readme/Flathub-badge.png)](https://flathub.org/apps/one.jwr.interstellar) 12 | 13 | Available for Arch Linux via the AUR: [interstellar-bin](https://aur.archlinux.org/packages/interstellar-bin). 14 | 15 | See the [latest release](https://github.com/interstellar-app/interstellar/releases/latest) for more downloads (.APK, .AppImage, .exe, etc.). 16 | 17 | ## Discussion 18 | 19 | You can ask questions, report bugs, make suggestions, etc., to any of the following: 20 | 21 | - [GitHub](https://github.com/interstellar-app/interstellar/issues) 22 | - [Mbin](https://kbin.earth/m/interstellar) 23 | - [Matrix](https://matrix.to/#/#interstellar-space:matrix.org) 24 | 25 | ## Screenshots 26 | 27 |
28 | 29 | 30 | 31 | 32 |
33 | 34 | ## Contributing 35 | 36 | Interstellar uses [Flutter](https://flutter.dev) as its framework, so make sure you have the [Flutter SDK installed](https://docs.flutter.dev/get-started/install) before doing anything else. Then, run `flutter doctor -v` to see instructions for setting up different build platforms (e.g. android studio for APKs). While developing on Linux, you will also need to install `libmpv` from your distro. Once all that's done, use `dart run build_runner build -d` to build the generated code for models (this only needs to run once unless you modify one of the models). Finally, you can use `flutter run` to develop, and `flutter build {platform}` for release files. 37 | 38 | ### Generating app icon 39 | 40 | The app icon is under the `assets/icons` folder, where the `logo.png` file is just the transparent one overlayed on the current background color `#423862`. This is generated with the [flutter_launcher_icons](https://pub.dev/packages/flutter_launcher_icons) package, and all relevant configuration is in the `pubspec.yaml` file. 41 | 42 | Icons created by [Benjamin Mathis](https://github.com/BenjMathis1) 43 | 44 | To generate a new icon, simply run the following: `dart run flutter_launcher_icons` 45 | 46 | ## Translating 47 | 48 | 49 | Translation status 50 | 51 | 52 | 53 | Translation status 54 | 55 | 56 | Interstellar uses the [Hosted Weblate](https://hosted.weblate.org/engage/interstellar/) to make translating as easy as possible. If you'd like to help, feel free to create an account there and start translating! 57 | -------------------------------------------------------------------------------- /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 | analyzer: 28 | errors: 29 | invalid_annotation_target: ignore 30 | -------------------------------------------------------------------------------- /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/to/reference-keystore 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. 5 | id "dev.flutter.flutter-gradle-plugin" 6 | } 7 | 8 | def keystoreProperties = new Properties() 9 | def keystorePropertiesFile = rootProject.file("key.properties") 10 | if (keystorePropertiesFile.exists()) { 11 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 12 | } 13 | 14 | android { 15 | namespace = "one.jwr.interstellar" 16 | compileSdk = flutter.compileSdkVersion 17 | ndkVersion = flutter.ndkVersion 18 | 19 | compileOptions { 20 | sourceCompatibility = JavaVersion.VERSION_1_8 21 | targetCompatibility = JavaVersion.VERSION_1_8 22 | 23 | coreLibraryDesugaringEnabled true 24 | } 25 | 26 | kotlinOptions { 27 | jvmTarget = JavaVersion.VERSION_1_8 28 | } 29 | 30 | defaultConfig { 31 | applicationId = "one.jwr.interstellar" 32 | minSdk = flutter.minSdkVersion 33 | targetSdk = flutter.targetSdkVersion 34 | versionCode = flutter.versionCode 35 | versionName = flutter.versionName 36 | 37 | if(keystoreProperties["includeNDK"] == "true") { 38 | ndk { 39 | // Filter for architectures supported by Flutter. 40 | abiFilters "armeabi-v7a", "arm64-v8a", "x86_64" 41 | } 42 | } 43 | 44 | multiDexEnabled = true 45 | } 46 | 47 | signingConfigs { 48 | release { 49 | keyAlias keystoreProperties["keyAlias"] 50 | keyPassword keystoreProperties["keyPassword"] 51 | storeFile keystoreProperties["storeFile"] ? file(keystoreProperties["storeFile"]) : null 52 | storePassword keystoreProperties["storePassword"] 53 | } 54 | } 55 | 56 | buildTypes { 57 | release { 58 | signingConfig = keystorePropertiesFile.exists() ? signingConfigs.release : signingConfigs.debug 59 | } 60 | 61 | debug { 62 | applicationIdSuffix = ".dev" 63 | } 64 | } 65 | 66 | dependenciesInfo { 67 | includeInApk = false 68 | includeInBundle = false 69 | } 70 | } 71 | 72 | flutter { 73 | source = "../.." 74 | } 75 | 76 | dependencies { 77 | coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.1.4" 78 | } 79 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 15 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 33 | 34 | 35 | 36 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/one/jwr/interstellar/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package one.jwr.interstellar 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/android/app/src/main/res/drawable-hdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/android/app/src/main/res/drawable-mdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /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-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/android/app/src/main/res/drawable-xhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/android/app/src/main/res/drawable-xxhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /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 | 8 | 9 | 10 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.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 | #294062 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 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | rootProject.buildDir = "../build" 9 | subprojects { 10 | project.buildDir = "${rootProject.buildDir}/${project.name}" 11 | } 12 | subprojects { 13 | project.evaluationDependsOn(":app") 14 | } 15 | 16 | tasks.register("clean", Delete) { 17 | delete rootProject.buildDir 18 | } 19 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError 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.4-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 | 10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 11 | 12 | repositories { 13 | google() 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | } 18 | 19 | plugins { 20 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 21 | id "com.android.application" version "8.3.1" apply false 22 | id "org.jetbrains.kotlin.android" version "1.8.22" apply false 23 | } 24 | 25 | include ":app" 26 | -------------------------------------------------------------------------------- /assets/icons/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/assets/icons/github.png -------------------------------------------------------------------------------- /assets/icons/google-play-feature-graphic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/assets/icons/google-play-feature-graphic.png -------------------------------------------------------------------------------- /assets/icons/logo-foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/assets/icons/logo-foreground.png -------------------------------------------------------------------------------- /assets/icons/logo-monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/assets/icons/logo-monochrome.png -------------------------------------------------------------------------------- /assets/icons/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/assets/icons/logo.png -------------------------------------------------------------------------------- /assets/icons/matrix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/assets/icons/matrix.png -------------------------------------------------------------------------------- /assets/icons/mbin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/assets/icons/mbin.png -------------------------------------------------------------------------------- /assets/readme/Flathub-badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/assets/readme/Flathub-badge.png -------------------------------------------------------------------------------- /assets/readme/GooglePlay-badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/assets/readme/GooglePlay-badge.png -------------------------------------------------------------------------------- /assets/readme/IzzyOnDroid-badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/assets/readme/IzzyOnDroid-badge.png -------------------------------------------------------------------------------- /assets/screenshots/desktop-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/assets/screenshots/desktop-1.png -------------------------------------------------------------------------------- /assets/screenshots/desktop-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/assets/screenshots/desktop-2.png -------------------------------------------------------------------------------- /assets/screenshots/desktop-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/assets/screenshots/desktop-3.png -------------------------------------------------------------------------------- /assets/screenshots/desktop-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/assets/screenshots/desktop-4.png -------------------------------------------------------------------------------- /assets/screenshots/mobile-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/assets/screenshots/mobile-1.png -------------------------------------------------------------------------------- /assets/screenshots/mobile-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/assets/screenshots/mobile-2.png -------------------------------------------------------------------------------- /assets/screenshots/mobile-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/assets/screenshots/mobile-3.png -------------------------------------------------------------------------------- /assets/screenshots/mobile-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/assets/screenshots/mobile-4.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/full_description.txt: -------------------------------------------------------------------------------- 1 |

Interstellar is a free and open source, Fediverse client, allowing you to access your Mbin/Lemmy/PieFed accounts and interact with your favorite communities.

-------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/featureGraphic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/fastlane/metadata/android/en-US/images/featureGraphic.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/fastlane/metadata/android/en-US/images/icon.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/fastlane/metadata/android/en-US/images/phoneScreenshots/01.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/fastlane/metadata/android/en-US/images/phoneScreenshots/02.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/fastlane/metadata/android/en-US/images/phoneScreenshots/03.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/fastlane/metadata/android/en-US/images/phoneScreenshots/04.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | An open source Mbin & Lemmy client, connecting you to the fediverse. -------------------------------------------------------------------------------- /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/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 | 12.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, '12.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 | 33 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 34 | end 35 | 36 | post_install do |installer| 37 | installer.pods_project.targets.each do |target| 38 | flutter_additional_ios_build_settings(target) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /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.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /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 Flutter 2 | import UIKit 3 | 4 | @main 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/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/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/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/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/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/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/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/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/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/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/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/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/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/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/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/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/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/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/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/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/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/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/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/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/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/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/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/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/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/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/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/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/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/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/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/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/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/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/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/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/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/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/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/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 | CADisableMinimumFrameDurationOnPhone 6 | 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleDisplayName 10 | Interstellar 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | interstellar 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | $(FLUTTER_BUILD_NAME) 23 | CFBundleSignature 24 | ???? 25 | CFBundleVersion 26 | $(FLUTTER_BUILD_NUMBER) 27 | LSRequiresIPhoneOS 28 | 29 | NSCameraUsageDescription 30 | Needed in order to allow taking photos and uploading them. 31 | NSPhotoLibraryUsageDescription 32 | Needed in order to allow uploading photos. 33 | UIApplicationSupportsIndirectInputEvents 34 | 35 | UILaunchStoryboardName 36 | LaunchScreen 37 | UIMainStoryboardFile 38 | Main 39 | UISupportedInterfaceOrientations 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationLandscapeLeft 43 | UIInterfaceOrientationLandscapeRight 44 | 45 | UISupportedInterfaceOrientations~ipad 46 | 47 | UIInterfaceOrientationPortrait 48 | UIInterfaceOrientationPortraitUpsideDown 49 | UIInterfaceOrientationLandscapeLeft 50 | UIInterfaceOrientationLandscapeRight 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /l10n.yaml: -------------------------------------------------------------------------------- 1 | arb-dir: lib/l10n 2 | template-arb-file: app_en.arb 3 | output-localization-file: app_localizations.dart 4 | synthetic-package: false 5 | -------------------------------------------------------------------------------- /lib/l10n/app_da.arb: -------------------------------------------------------------------------------- 1 | { 2 | "removeAccount": "Fjern konto", 3 | "@removeAccount": {}, 4 | "instanceHost": "Instans-vært", 5 | "@instanceHost": {}, 6 | "interstellar": "Interstellar", 7 | "@interstellar": {}, 8 | "feed": "Foder", 9 | "@feed": {}, 10 | "accounts": "Regnskaber", 11 | "@accounts": {}, 12 | "addAccount": "Tilføj konto", 13 | "@addAccount": {}, 14 | "login": "Login", 15 | "@login": {}, 16 | "recommendedInstances": "Anbefalede instanser", 17 | "@recommendedInstances": {}, 18 | "usernameOrEmail": "Brugernavn eller email", 19 | "@usernameOrEmail": {}, 20 | "password": "Adgangskode", 21 | "@password": {}, 22 | "explore": "Udforsk", 23 | "@explore": {}, 24 | "totpToken": "TOTP-token", 25 | "@totpToken": {}, 26 | "cancel": "Annuller", 27 | "@cancel": {}, 28 | "continue_": "Fortsæt", 29 | "@continue_": {}, 30 | "guest": "Gæst", 31 | "@guest": {}, 32 | "remove": "Fjerne", 33 | "@remove": {}, 34 | "botAccount": "Bot-konto", 35 | "@botAccount": {}, 36 | "newUser": "Ny bruger", 37 | "@newUser": {}, 38 | "cakeDay": "Kagens dag", 39 | "@cakeDay": {}, 40 | "filter": "Filtrer efter", 41 | "@filter": {}, 42 | "filter_all": "Alle", 43 | "@filter_all": {}, 44 | "filter_subscribed": "Abonneret", 45 | "@filter_subscribed": {}, 46 | "filter_moderated": "Modereret", 47 | "@filter_moderated": {}, 48 | "filter_favorited": "Favoriseret", 49 | "@filter_favorited": {}, 50 | "filter_blocked": "Blokeret", 51 | "@filter_blocked": {} 52 | } 53 | -------------------------------------------------------------------------------- /lib/l10n/app_id.arb: -------------------------------------------------------------------------------- 1 | { 2 | "interstellar": "Interstellar", 3 | "@interstellar": {}, 4 | "explore": "Jelajah", 5 | "@explore": {}, 6 | "feed": "Linimasa", 7 | "@feed": {}, 8 | "accounts": "Akun-akun", 9 | "@accounts": {}, 10 | "addAccount": "Tambahkan Akun", 11 | "@addAccount": {}, 12 | "removeAccount": "Hapus Akun", 13 | "@removeAccount": {} 14 | } 15 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:interstellar/src/controller/controller.dart'; 6 | import 'package:interstellar/src/controller/database.dart'; 7 | import 'package:interstellar/src/utils/variables.dart'; 8 | import 'package:interstellar/src/widgets/markdown/drafts_controller.dart'; 9 | import 'package:media_kit/media_kit.dart'; 10 | import 'package:provider/provider.dart'; 11 | import 'package:window_manager/window_manager.dart'; 12 | 13 | import 'src/app.dart'; 14 | import 'src/init_push_notifications.dart'; 15 | 16 | void main() async { 17 | WidgetsFlutterBinding.ensureInitialized(); 18 | MediaKit.ensureInitialized(); 19 | 20 | await initDatabase(); 21 | 22 | if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) { 23 | await windowManager.ensureInitialized(); 24 | 25 | WindowOptions windowOptions = const WindowOptions( 26 | minimumSize: Size(400, 400), 27 | ); 28 | 29 | windowManager.waitUntilReadyToShow(windowOptions, () async { 30 | await windowManager.show(); 31 | await windowManager.focus(); 32 | }); 33 | } 34 | 35 | // Show snackbar on error 36 | FlutterError.onError = (details) { 37 | FlutterError.presentError(details); 38 | 39 | // Don't show error for rendering issues 40 | if (details.library == 'rendering library') return; 41 | // Don't show error for image loading issues 42 | if (details.library == 'image resource service') return; 43 | 44 | scaffoldMessengerKey.currentState?.showSnackBar( 45 | SnackBar(content: Text(details.summary.toString())), 46 | ); 47 | }; 48 | PlatformDispatcher.instance.onError = (error, stack) { 49 | scaffoldMessengerKey.currentState?.showSnackBar( 50 | SnackBar(content: Text(error.toString())), 51 | ); 52 | return false; 53 | }; 54 | 55 | final ac = AppController(); 56 | await ac.init(); 57 | 58 | if (Platform.isAndroid) { 59 | await initPushNotifications(ac); 60 | } 61 | 62 | runApp( 63 | MultiProvider( 64 | providers: [ 65 | ChangeNotifierProvider.value(value: ac), 66 | ChangeNotifierProvider(create: (context) => DraftsController()), 67 | ], 68 | child: const App(), 69 | ), 70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /lib/src/api/api.dart: -------------------------------------------------------------------------------- 1 | import 'package:http/http.dart' as http; 2 | import 'package:interstellar/src/api/bookmark.dart'; 3 | import 'package:interstellar/src/api/client.dart'; 4 | import 'package:interstellar/src/api/comments.dart'; 5 | import 'package:interstellar/src/api/domains.dart'; 6 | import 'package:interstellar/src/api/community_moderation.dart'; 7 | import 'package:interstellar/src/api/community.dart'; 8 | import 'package:interstellar/src/api/messages.dart'; 9 | import 'package:interstellar/src/api/microblogs.dart'; 10 | import 'package:interstellar/src/api/moderation.dart'; 11 | import 'package:interstellar/src/api/notifications.dart'; 12 | import 'package:interstellar/src/api/search.dart'; 13 | import 'package:interstellar/src/api/threads.dart'; 14 | import 'package:interstellar/src/api/users.dart'; 15 | import 'package:interstellar/src/controller/server.dart'; 16 | import 'package:interstellar/src/utils/utils.dart'; 17 | 18 | class API { 19 | final ServerClient client; 20 | 21 | final APIComments comments; 22 | final MbinAPIDomains domains; 23 | final APIThreads threads; 24 | final APICommunity community; 25 | final APICommunityModeration communityModeration; 26 | final APIMessages messages; 27 | final APIModeration moderation; 28 | final APINotifications notifications; 29 | final MbinAPIMicroblogs microblogs; 30 | final APISearch search; 31 | final APIUsers users; 32 | final APIBookmark bookmark; 33 | 34 | API(this.client) 35 | : comments = APIComments(client), 36 | domains = MbinAPIDomains(client), 37 | threads = APIThreads(client), 38 | community = APICommunity(client), 39 | communityModeration = APICommunityModeration(client), 40 | messages = APIMessages(client), 41 | moderation = APIModeration(client), 42 | notifications = APINotifications(client), 43 | microblogs = MbinAPIMicroblogs(client), 44 | search = APISearch(client), 45 | users = APIUsers(client), 46 | bookmark = APIBookmark(client); 47 | } 48 | 49 | Future getServerSoftware(String server) async { 50 | final response = await http.get(Uri.https(server, '/nodeinfo/2.0.json')); 51 | 52 | try { 53 | return ServerSoftware.values.byName( 54 | ((response.bodyJson['software'] as JsonMap)['name'] as String) 55 | .toLowerCase(), 56 | ); 57 | } catch (_) { 58 | return null; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/src/api/domains.dart: -------------------------------------------------------------------------------- 1 | import 'package:interstellar/src/api/client.dart'; 2 | import 'package:interstellar/src/models/domain.dart'; 3 | import 'package:interstellar/src/screens/explore/explore_screen.dart'; 4 | 5 | class MbinAPIDomains { 6 | final ServerClient client; 7 | 8 | MbinAPIDomains(this.client); 9 | 10 | Future list({ 11 | String? page, 12 | ExploreFilter? filter, 13 | String? search, 14 | }) async { 15 | final path = 16 | '/domains${switch (filter) { 17 | null || ExploreFilter.all => '', 18 | ExploreFilter.subscribed => '/subscribed', 19 | ExploreFilter.blocked => '/blocked', 20 | _ => throw Exception('Not allowed filter in domains request'), 21 | }}'; 22 | 23 | final query = { 24 | 'p': page, 25 | if (filter == null || filter == ExploreFilter.all) 'q': search, 26 | }; 27 | 28 | final response = await client.get(path, queryParams: query); 29 | 30 | return DomainListModel.fromMbin(response.bodyJson); 31 | } 32 | 33 | Future get(int domainId) async { 34 | final path = '/domain/$domainId'; 35 | 36 | final response = await client.get(path); 37 | 38 | return DomainModel.fromMbin(response.bodyJson); 39 | } 40 | 41 | Future putSubscribe(int domainId, bool state) async { 42 | final path = '/domain/$domainId/${state ? 'subscribe' : 'unsubscribe'}'; 43 | 44 | final response = await client.put(path); 45 | 46 | return DomainModel.fromMbin(response.bodyJson); 47 | } 48 | 49 | Future putBlock(int domainId, bool state) async { 50 | final path = '/domain/$domainId/${state ? 'block' : 'unblock'}'; 51 | 52 | final response = await client.put(path); 53 | 54 | return DomainModel.fromMbin(response.bodyJson); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/src/api/feed_source.dart: -------------------------------------------------------------------------------- 1 | enum FeedSource { 2 | all, 3 | local, 4 | subscribed, 5 | moderated, 6 | favorited, 7 | community, 8 | user, 9 | domain, 10 | } 11 | 12 | enum FeedSort { 13 | active, 14 | hot, 15 | newest, 16 | oldest, 17 | top, 18 | commented, 19 | // mbin specific 20 | commentedThreeHour, 21 | commentedSixHour, 22 | commentedTwelveHour, 23 | commentedDay, 24 | commentedWeek, 25 | commentedMonth, 26 | commentedYear, 27 | 28 | //lemmy specific 29 | topDay, 30 | topWeek, 31 | topMonth, 32 | topYear, 33 | newComments, 34 | topHour, 35 | topThreeHour, 36 | topSixHour, 37 | topTwelveHour, 38 | topThreeMonths, 39 | topSixMonths, 40 | topNineMonths, 41 | controversial, 42 | scaled, 43 | } 44 | -------------------------------------------------------------------------------- /lib/src/api/oauth.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:http/http.dart' as http; 4 | import 'package:interstellar/src/api/client.dart'; 5 | import 'package:interstellar/src/widgets/redirect_listen.dart'; 6 | 7 | const oauthName = 'Interstellar'; 8 | const oauthContact = 'appstore@jwr.one'; 9 | const oauthGrants = ['authorization_code', 'refresh_token']; 10 | const oauthScopes = [ 11 | 'read', 12 | 'write', 13 | 'delete', 14 | 'subscribe', 15 | 'block', 16 | 'vote', 17 | 'report', 18 | 'user', 19 | 'moderate', 20 | 'bookmark_list', 21 | ]; 22 | 23 | Future registerOauthApp(String instanceHost) async { 24 | const path = '/api/client'; 25 | 26 | final response = await http.post( 27 | Uri.https(instanceHost, path), 28 | headers: {'Content-Type': 'application/json; charset=UTF-8'}, 29 | body: jsonEncode({ 30 | 'name': oauthName, 31 | 'contactEmail': oauthContact, 32 | 'public': true, 33 | 'redirectUris': [redirectUri], 34 | 'grants': oauthGrants, 35 | 'scopes': oauthScopes, 36 | }), 37 | ); 38 | 39 | return response.bodyJson['identifier'] as String; 40 | } 41 | -------------------------------------------------------------------------------- /lib/src/api/search.dart: -------------------------------------------------------------------------------- 1 | import 'package:interstellar/src/api/client.dart'; 2 | import 'package:interstellar/src/controller/server.dart'; 3 | import 'package:interstellar/src/models/search.dart'; 4 | import 'package:interstellar/src/screens/explore/explore_screen.dart'; 5 | 6 | class APISearch { 7 | final ServerClient client; 8 | 9 | APISearch(this.client); 10 | 11 | Future get({ 12 | String? page, 13 | String? search, 14 | ExploreFilter? filter, 15 | }) async { 16 | switch (client.software) { 17 | case ServerSoftware.mbin: 18 | const path = '/search'; 19 | 20 | final response = await client.get( 21 | path, 22 | queryParams: {'p': page, 'q': search}, 23 | ); 24 | 25 | return SearchListModel.fromMbin(response.bodyJson); 26 | 27 | case ServerSoftware.lemmy: 28 | const path = '/search'; 29 | final query = { 30 | 'q': search, 31 | 'page': page ?? '1', 32 | 'type_': 'All', 33 | 'listing_type': switch (filter) { 34 | ExploreFilter.all => 'All', 35 | ExploreFilter.local => 'Local', 36 | _ => 'All', 37 | }, 38 | }; 39 | 40 | final response = await client.get(path, queryParams: query); 41 | 42 | final json = response.bodyJson; 43 | String? nextPage; 44 | if ((json['comments'] as List).isNotEmpty || 45 | (json['posts'] as List).isNotEmpty || 46 | (json['communities'] as List).isNotEmpty || 47 | (json['users'] as List).isNotEmpty) { 48 | nextPage = (int.parse(page ?? '1') + 1).toString(); 49 | } 50 | 51 | json['next_page'] = nextPage; 52 | 53 | return SearchListModel.fromLemmy( 54 | json, 55 | langCodeIdPairs: await client.languageCodeIdPairs(), 56 | ); 57 | 58 | case ServerSoftware.piefed: 59 | const path = '/search'; 60 | final query = { 61 | 'q': search, 62 | 'page': page ?? '1', 63 | // Only use "Posts" type until "All" type is supported in PieFed 64 | 'type_': 'Posts', 65 | 'listing_type': switch (filter) { 66 | ExploreFilter.all => 'All', 67 | ExploreFilter.local => 'Local', 68 | _ => 'All', 69 | }, 70 | }; 71 | 72 | final response = await client.get(path, queryParams: query); 73 | 74 | final json = response.bodyJson; 75 | String? nextPage; 76 | if ((json['comments'] as List).isNotEmpty || 77 | (json['posts'] as List).isNotEmpty || 78 | (json['communities'] as List).isNotEmpty || 79 | (json['users'] as List).isNotEmpty) { 80 | nextPage = (int.parse(page ?? '1') + 1).toString(); 81 | } 82 | 83 | json['next_page'] = nextPage; 84 | 85 | return SearchListModel.fromPiefed( 86 | json, 87 | langCodeIdPairs: await client.languageCodeIdPairs(), 88 | ); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /lib/src/controller/account.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:interstellar/src/utils/utils.dart'; 3 | import 'package:oauth2/oauth2.dart'; 4 | 5 | part 'account.freezed.dart'; 6 | part 'account.g.dart'; 7 | 8 | @freezed 9 | class Account with _$Account { 10 | @JsonSerializable(explicitToJson: true, includeIfNull: false) 11 | const factory Account({ 12 | Credentials? oauth, 13 | String? jwt, 14 | bool? isPushRegistered, 15 | }) = _Account; 16 | 17 | factory Account.fromJson(JsonMap json) => _$AccountFromJson(json); 18 | } 19 | -------------------------------------------------------------------------------- /lib/src/controller/database.dart: -------------------------------------------------------------------------------- 1 | import 'package:path/path.dart'; 2 | import 'package:path_provider/path_provider.dart'; 3 | import 'package:sembast/sembast_io.dart'; 4 | 5 | late final Database db; 6 | 7 | Future initDatabase() async { 8 | final dir = await getApplicationSupportDirectory(); 9 | 10 | final dbPath = join(dir.path, 'database'); 11 | 12 | db = await databaseFactoryIo.openDatabase(dbPath); 13 | } 14 | -------------------------------------------------------------------------------- /lib/src/controller/filter_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:interstellar/src/utils/utils.dart'; 3 | 4 | part 'filter_list.freezed.dart'; 5 | part 'filter_list.g.dart'; 6 | 7 | enum FilterListMatchMode { simple, wholeWords, regex } 8 | 9 | @freezed 10 | class FilterList with _$FilterList { 11 | const FilterList._(); 12 | 13 | @JsonSerializable(explicitToJson: true, includeIfNull: false) 14 | const factory FilterList({ 15 | required Set phrases, 16 | required FilterListMatchMode matchMode, 17 | required bool caseSensitive, 18 | required bool showWithWarning, 19 | }) = _FilterList; 20 | 21 | factory FilterList.fromJson(JsonMap json) => _$FilterListFromJson(json); 22 | 23 | static const nullFilterList = FilterList( 24 | phrases: {}, 25 | matchMode: FilterListMatchMode.simple, 26 | caseSensitive: false, 27 | showWithWarning: false, 28 | ); 29 | 30 | bool hasMatch(String input) { 31 | switch (matchMode) { 32 | case FilterListMatchMode.simple: 33 | if (!caseSensitive) input = input.toLowerCase(); 34 | 35 | for (var phrase in phrases) { 36 | if (!caseSensitive) phrase = phrase.toLowerCase(); 37 | 38 | if (input.contains(phrase)) return true; 39 | } 40 | 41 | return false; 42 | case FilterListMatchMode.wholeWords: 43 | for (var phrase in phrases) { 44 | if (RegExp( 45 | '\\b${RegExp.escape(phrase)}\\b', 46 | caseSensitive: caseSensitive, 47 | ).hasMatch(input)) { 48 | return true; 49 | } 50 | } 51 | 52 | return false; 53 | case FilterListMatchMode.regex: 54 | for (var phrase in phrases) { 55 | if (RegExp(phrase, caseSensitive: caseSensitive).hasMatch(input)) { 56 | return true; 57 | } 58 | } 59 | 60 | return false; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/src/controller/server.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | import 'package:interstellar/src/utils/utils.dart'; 4 | 5 | part 'server.freezed.dart'; 6 | part 'server.g.dart'; 7 | 8 | enum ServerSoftware { 9 | mbin, 10 | lemmy, 11 | piefed; 12 | 13 | String get apiPathPrefix => switch (this) { 14 | ServerSoftware.mbin => '/api', 15 | ServerSoftware.lemmy => '/api/v3', 16 | ServerSoftware.piefed => '/api/alpha', 17 | }; 18 | 19 | String get title => switch (this) { 20 | ServerSoftware.mbin => 'Mbin', 21 | ServerSoftware.lemmy => 'Lemmy', 22 | ServerSoftware.piefed => 'PieFed', 23 | }; 24 | 25 | Color get color => switch (this) { 26 | ServerSoftware.mbin => Color(0xff4f2696), 27 | ServerSoftware.lemmy => Color(0xff03a80e), 28 | ServerSoftware.piefed => Color(0xff0e6ef9), 29 | }; 30 | } 31 | 32 | @freezed 33 | class Server with _$Server { 34 | @JsonSerializable(explicitToJson: true, includeIfNull: false) 35 | const factory Server({ 36 | required ServerSoftware software, 37 | String? oauthIdentifier, 38 | }) = _Server; 39 | 40 | factory Server.fromJson(JsonMap json) => _$ServerFromJson(json); 41 | } 42 | -------------------------------------------------------------------------------- /lib/src/init_push_notifications.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:math'; 3 | import 'dart:typed_data'; 4 | 5 | import 'package:flutter_local_notifications/flutter_local_notifications.dart'; 6 | import 'package:http/http.dart' as http; 7 | import 'package:interstellar/src/controller/controller.dart'; 8 | import 'package:unifiedpush/unifiedpush.dart'; 9 | import 'package:webpush_encryption/webpush_encryption.dart'; 10 | 11 | Future _downloadImageToAndroidBitmap(String url) async { 12 | final res = await http.get(Uri.parse(url)); 13 | 14 | final enc = base64.encode(res.bodyBytes); 15 | 16 | final androidBitmap = ByteArrayAndroidBitmap.fromBase64String(enc); 17 | 18 | return androidBitmap; 19 | } 20 | 21 | Future initPushNotifications(AppController ac) async { 22 | FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = 23 | FlutterLocalNotificationsPlugin(); 24 | 25 | await flutterLocalNotificationsPlugin.initialize( 26 | const InitializationSettings( 27 | android: AndroidInitializationSettings( 28 | '@drawable/ic_launcher_monochrome', 29 | ), 30 | ), 31 | ); 32 | 33 | final random = Random(); 34 | 35 | await UnifiedPush.initialize( 36 | onNewEndpoint: (String endpoint, String instance) async { 37 | await ac.api.notifications.pushRegister( 38 | endpoint: endpoint, 39 | serverKey: ac.webPushKeys.publicKey.auth, 40 | contentPublicKey: ac.webPushKeys.publicKey.p256dh, 41 | ); 42 | }, 43 | onRegistrationFailed: (String instance) { 44 | ac.removePushRegistrationStatus(instance); 45 | }, 46 | onUnregistered: (String instance) { 47 | ac.removePushRegistrationStatus(instance); 48 | }, 49 | onMessage: (Uint8List message, String instance) async { 50 | final data = jsonDecode( 51 | utf8.decode(await WebPush().decrypt(ac.webPushKeys, message)), 52 | ); 53 | 54 | final hostDomain = instance.split('@').last; 55 | final avatarUrl = data['avatarUrl'] as String?; 56 | 57 | await flutterLocalNotificationsPlugin.show( 58 | random.nextInt(2 ^ 31 - 1), 59 | data['title'], 60 | data['message'], 61 | NotificationDetails( 62 | android: AndroidNotificationDetails( 63 | data['category'] as String, 64 | data['category'] as String, 65 | largeIcon: avatarUrl != null 66 | ? await _downloadImageToAndroidBitmap( 67 | 'https://$hostDomain$avatarUrl', 68 | ) 69 | : null, 70 | ), 71 | ), 72 | ); 73 | }, 74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /lib/src/models/bookmark_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:interstellar/src/utils/utils.dart'; 3 | 4 | part 'bookmark_list.freezed.dart'; 5 | 6 | @freezed 7 | class BookmarkListListModel with _$BookmarkListListModel { 8 | const factory BookmarkListListModel({ 9 | required List items, 10 | }) = _BookmarkListListModel; 11 | 12 | factory BookmarkListListModel.fromMbin(JsonMap json) => BookmarkListListModel( 13 | items: (json['items'] as List) 14 | .map((post) => BookmarkListModel.fromMbin(post as JsonMap)) 15 | .toList(), 16 | ); 17 | } 18 | 19 | @freezed 20 | class BookmarkListModel with _$BookmarkListModel { 21 | const factory BookmarkListModel({ 22 | required String name, 23 | required bool isDefault, 24 | required int count, 25 | }) = _BookmarkListModel; 26 | 27 | factory BookmarkListModel.fromMbin(JsonMap json) => BookmarkListModel( 28 | name: json['name'] as String, 29 | isDefault: json['isDefault'] as bool, 30 | count: json['count'] as int, 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /lib/src/models/config_share.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:freezed_annotation/freezed_annotation.dart'; 4 | import 'package:interstellar/src/utils/utils.dart'; 5 | import 'package:package_info_plus/package_info_plus.dart'; 6 | 7 | part 'config_share.freezed.dart'; 8 | part 'config_share.g.dart'; 9 | 10 | enum ConfigShareType { profile, filterList } 11 | 12 | @freezed 13 | class ConfigShare with _$ConfigShare { 14 | const ConfigShare._(); 15 | 16 | @JsonSerializable(explicitToJson: true, includeIfNull: false) 17 | const factory ConfigShare({ 18 | // Interstellar version 19 | required String interstellar, 20 | required ConfigShareType type, 21 | required String name, 22 | required DateTime date, 23 | required JsonMap payload, 24 | required String hash, 25 | }) = _ConfigShare; 26 | 27 | factory ConfigShare.fromJson(JsonMap json) => _$ConfigShareFromJson(json); 28 | 29 | static Future create({ 30 | required ConfigShareType type, 31 | required String name, 32 | required JsonMap payload, 33 | }) async { 34 | final packageInfo = await PackageInfo.fromPlatform(); 35 | 36 | final config = ConfigShare( 37 | interstellar: packageInfo.version, 38 | type: type, 39 | name: name, 40 | date: DateTime.now(), 41 | payload: payload, 42 | hash: '', 43 | ); 44 | 45 | final hash = strToMd5Base64(jsonEncode(config.toJson())); 46 | 47 | return config.copyWith(hash: hash); 48 | } 49 | 50 | // Once the config is parsed, use this to pass in the original json string and verify the hash. 51 | bool verifyHash(String jsonStr) { 52 | // Remove instance of hash from original string 53 | final hashToCheck = strToMd5Base64(jsonStr.replaceFirst(hash, '')); 54 | 55 | return hash == hashToCheck; 56 | } 57 | 58 | String toMarkdown() => '```interstellar\n${jsonEncode(toJson())}\n```'; 59 | } 60 | -------------------------------------------------------------------------------- /lib/src/models/domain.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:interstellar/src/utils/models.dart'; 3 | import 'package:interstellar/src/utils/utils.dart'; 4 | 5 | part 'domain.freezed.dart'; 6 | 7 | @freezed 8 | class DomainListModel with _$DomainListModel { 9 | const factory DomainListModel({ 10 | required List items, 11 | required String? nextPage, 12 | }) = _DomainListModel; 13 | 14 | factory DomainListModel.fromMbin(JsonMap json) => DomainListModel( 15 | items: (json['items'] as List) 16 | .map((post) => DomainModel.fromMbin(post as JsonMap)) 17 | .toList(), 18 | nextPage: mbinCalcNextPaginationPage(json['pagination'] as JsonMap), 19 | ); 20 | } 21 | 22 | @freezed 23 | class DomainModel with _$DomainModel { 24 | const factory DomainModel({ 25 | required int id, 26 | required String name, 27 | required int entryCount, 28 | required int subscriptionsCount, 29 | required bool? isUserSubscribed, 30 | required bool? isBlockedByUser, 31 | }) = _DomainModel; 32 | 33 | factory DomainModel.fromMbin(JsonMap json) => DomainModel( 34 | id: json['domainId'] as int, 35 | name: json['name'] as String, 36 | entryCount: json['entryCount'] as int, 37 | subscriptionsCount: json['subscriptionsCount'] as int, 38 | isUserSubscribed: json['isUserSubscribed'] as bool?, 39 | isBlockedByUser: json['isBlockedByUser'] as bool?, 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /lib/src/models/image.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:interstellar/src/utils/utils.dart'; 3 | 4 | part 'image.freezed.dart'; 5 | 6 | @freezed 7 | class ImageModel with _$ImageModel { 8 | const factory ImageModel({ 9 | required String src, 10 | required String? altText, 11 | required String? blurHash, 12 | required int? blurHashWidth, 13 | required int? blurHashHeight, 14 | }) = _ImageModel; 15 | 16 | factory ImageModel.fromMbin(JsonMap json) => ImageModel( 17 | src: (json['storageUrl'] ?? json['sourceUrl']) as String, 18 | altText: json['altText'] as String?, 19 | blurHash: json['blurHash'] as String?, 20 | blurHashWidth: json['width'] as int?, 21 | blurHashHeight: json['height'] as int?, 22 | ); 23 | 24 | factory ImageModel.fromLemmy(String src, [String? altText]) => ImageModel( 25 | src: src, 26 | altText: altText, 27 | blurHash: null, 28 | blurHashWidth: null, 29 | blurHashHeight: null, 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /lib/src/screens/account/account_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:interstellar/src/controller/controller.dart'; 3 | import 'package:interstellar/src/screens/account/notification/notification_badge.dart'; 4 | import 'package:interstellar/src/screens/account/notification/notification_screen.dart'; 5 | import 'package:interstellar/src/screens/account/self_feed.dart'; 6 | import 'package:interstellar/src/utils/utils.dart'; 7 | import 'package:material_symbols_icons/symbols.dart'; 8 | import 'package:provider/provider.dart'; 9 | 10 | import 'messages/messages_screen.dart'; 11 | 12 | class AccountScreen extends StatefulWidget { 13 | const AccountScreen({super.key}); 14 | 15 | @override 16 | State createState() => _AccountScreenState(); 17 | } 18 | 19 | class _AccountScreenState extends State 20 | with AutomaticKeepAliveClientMixin { 21 | @override 22 | bool get wantKeepAlive => true; 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | super.build(context); 27 | return whenLoggedIn( 28 | context, 29 | DefaultTabController( 30 | length: 3, 31 | child: Scaffold( 32 | appBar: AppBar( 33 | title: Text(context.watch().selectedAccount), 34 | bottom: TabBar( 35 | tabs: [ 36 | Tab( 37 | text: l(context).notifications, 38 | icon: const NotificationBadge( 39 | child: Icon(Symbols.notifications_rounded), 40 | ), 41 | ), 42 | Tab( 43 | text: l(context).messages, 44 | icon: const Icon(Symbols.message_rounded), 45 | ), 46 | Tab( 47 | text: l(context).account_overview, 48 | icon: const Icon(Symbols.person_rounded), 49 | ), 50 | ], 51 | ), 52 | ), 53 | body: TabBarView( 54 | physics: appTabViewPhysics(context), 55 | children: const [ 56 | NotificationsScreen(), 57 | MessagesScreen(), 58 | SelfFeed(), 59 | ], 60 | ), 61 | ), 62 | ), 63 | ) ?? 64 | Center(child: Text(l(context).notLoggedIn)); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/src/screens/account/messages/message_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:interstellar/src/controller/controller.dart'; 3 | import 'package:interstellar/src/models/message.dart'; 4 | import 'package:interstellar/src/utils/utils.dart'; 5 | import 'package:interstellar/src/widgets/avatar.dart'; 6 | import 'package:provider/provider.dart'; 7 | 8 | class MessageItem extends StatelessWidget { 9 | const MessageItem(this.item, this.onUpdate, {this.onClick, super.key}); 10 | 11 | final MessageThreadModel item; 12 | final void Function(MessageThreadModel) onUpdate; 13 | final void Function()? onClick; 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | final messageUser = item.participants.firstWhere( 18 | (user) => user.name != context.watch().localName, 19 | orElse: () => item.participants.first, 20 | ); 21 | 22 | return ListTile( 23 | title: Text( 24 | messageUser.name, 25 | softWrap: false, 26 | overflow: TextOverflow.fade, 27 | ), 28 | subtitle: Text( 29 | item.messages.first.body.replaceAll('\n', ' '), 30 | softWrap: false, 31 | overflow: TextOverflow.ellipsis, 32 | ), 33 | leading: Avatar(messageUser.avatar), 34 | trailing: Text('${dateDiffFormat(item.messages.first.createdAt)} ago'), 35 | onTap: onClick, 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/src/screens/account/notification/notification_badge.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:interstellar/src/utils/utils.dart'; 3 | import 'package:interstellar/src/widgets/wrapper.dart'; 4 | import 'package:provider/provider.dart'; 5 | 6 | import 'notification_count_controller.dart'; 7 | 8 | class NotificationBadge extends StatefulWidget { 9 | final Widget child; 10 | 11 | const NotificationBadge({super.key, required this.child}); 12 | 13 | @override 14 | State createState() => _NotificationBadgeState(); 15 | } 16 | 17 | class _NotificationBadgeState extends State { 18 | @override 19 | Widget build(BuildContext context) { 20 | final count = context.watch().value; 21 | 22 | return Wrapper( 23 | shouldWrap: count != 0, 24 | parentBuilder: (child) => 25 | Badge(label: Text(intFormat(count)), child: child), 26 | child: widget.child, 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/src/screens/account/notification/notification_count_controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:interstellar/src/api/api.dart'; 5 | import 'package:interstellar/src/controller/controller.dart'; 6 | 7 | class NotificationCountController with ChangeNotifier { 8 | int _value = 0; 9 | int get value => _value; 10 | 11 | String? _account; 12 | late API _api; 13 | Timer? _timer; 14 | 15 | void updateAppController(AppController ac) { 16 | _api = ac.api; 17 | 18 | final newAccount = ac.isLoggedIn ? ac.selectedAccount : null; 19 | 20 | if (_account != newAccount) { 21 | _account = newAccount; 22 | 23 | reload(); 24 | } 25 | } 26 | 27 | void reload() async { 28 | _timer?.cancel(); 29 | 30 | if (_account != null) { 31 | _timer = Timer.periodic(const Duration(minutes: 1), (timer) { 32 | _update(); 33 | }); 34 | } 35 | 36 | _update(); 37 | } 38 | 39 | void _update() async { 40 | try { 41 | int newValue = _account == null ? 0 : await _api.notifications.getCount(); 42 | 43 | if (_value != newValue) { 44 | _value = newValue; 45 | notifyListeners(); 46 | } 47 | } catch (_) { 48 | // Do not throw error if unsuccessful due to the spam of pop ups received 49 | // when going from background to foreground visibility 50 | } 51 | } 52 | 53 | @override 54 | void dispose() { 55 | _timer?.cancel(); 56 | 57 | super.dispose(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/src/screens/account/self_feed.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:interstellar/src/controller/controller.dart'; 3 | import 'package:interstellar/src/models/user.dart'; 4 | import 'package:interstellar/src/screens/explore/user_screen.dart'; 5 | import 'package:interstellar/src/widgets/loading_template.dart'; 6 | import 'package:provider/provider.dart'; 7 | 8 | class SelfFeed extends StatefulWidget { 9 | const SelfFeed({super.key}); 10 | 11 | @override 12 | State createState() => _SelfFeedState(); 13 | } 14 | 15 | class _SelfFeedState extends State 16 | with AutomaticKeepAliveClientMixin { 17 | DetailedUserModel? _meUser; 18 | 19 | @override 20 | bool get wantKeepAlive => true; 21 | 22 | @override 23 | void initState() { 24 | super.initState(); 25 | 26 | context.read().api.users.getMe().then( 27 | (value) => setState(() { 28 | _meUser = value; 29 | }), 30 | ); 31 | } 32 | 33 | @override 34 | Widget build(BuildContext context) { 35 | super.build(context); 36 | if (_meUser == null) return const LoadingTemplate(); 37 | 38 | final user = _meUser!; 39 | 40 | return UserScreen(user.id, initData: _meUser); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/src/screens/explore/user_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:interstellar/src/models/user.dart'; 3 | import 'package:interstellar/src/screens/explore/user_screen.dart'; 4 | import 'package:interstellar/src/utils/utils.dart'; 5 | import 'package:interstellar/src/widgets/avatar.dart'; 6 | 7 | class UserItemSimple extends StatelessWidget { 8 | final UserModel user; 9 | final bool isOwner; 10 | final List? trailingWidgets; 11 | final bool noTap; 12 | 13 | const UserItemSimple( 14 | this.user, { 15 | this.isOwner = false, 16 | this.trailingWidgets, 17 | this.noTap = false, 18 | super.key, 19 | }); 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | return InkWell( 24 | onTap: () { 25 | Navigator.of( 26 | context, 27 | ).push(MaterialPageRoute(builder: (context) => UserScreen(user.id))); 28 | }, 29 | child: Padding( 30 | padding: const EdgeInsets.all(12), 31 | child: Row( 32 | children: [ 33 | if (user.avatar != null) Avatar(user.avatar, radius: 16), 34 | Container(width: 8 + (user.avatar != null ? 0 : 32)), 35 | Expanded( 36 | child: Column( 37 | crossAxisAlignment: CrossAxisAlignment.start, 38 | children: [ 39 | Text( 40 | user.name, 41 | overflow: TextOverflow.ellipsis, 42 | style: TextStyle( 43 | fontWeight: isOwner ? FontWeight.bold : null, 44 | ), 45 | ), 46 | if (isOwner) Text(l(context).owner), 47 | ], 48 | ), 49 | ), 50 | ...trailingWidgets ?? [], 51 | ], 52 | ), 53 | ), 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/src/screens/settings/data_utilities.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:interstellar/src/screens/settings/account_migration.dart'; 3 | import 'package:interstellar/src/screens/settings/account_reset.dart'; 4 | import 'package:interstellar/src/utils/utils.dart'; 5 | 6 | class DataUtilitiesScreen extends StatelessWidget { 7 | const DataUtilitiesScreen({super.key}); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return Scaffold( 12 | appBar: AppBar(title: Text(l(context).settings_dataUtilities)), 13 | body: ListView( 14 | children: [ 15 | ListTile( 16 | title: Text(l(context).settings_accountMigration), 17 | subtitle: Text(l(context).settings_accountMigration_help), 18 | onTap: () => Navigator.of(context).push( 19 | MaterialPageRoute( 20 | builder: (context) => const AccountMigrationScreen(), 21 | ), 22 | ), 23 | ), 24 | ListTile( 25 | title: Text(l(context).settings_accountReset), 26 | subtitle: Text(l(context).settings_accountReset_help), 27 | onTap: () => Navigator.of(context).push( 28 | MaterialPageRoute( 29 | builder: (context) => const AccountResetScreen(), 30 | ), 31 | ), 32 | ), 33 | ], 34 | ), 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/src/utils/breakpoints.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | /// Breakpoint sizes are as specified here: 4 | /// https://m3.material.io/foundations/layout/applying-layout/window-size-classes 5 | class Breakpoints { 6 | static const widthCompact = 0; 7 | static const widthMedium = 600; 8 | static const widthExpanded = 840; 9 | 10 | static double screenWidth(BuildContext context) => 11 | MediaQuery.sizeOf(context).width; 12 | 13 | static bool isCompact(BuildContext context) { 14 | final width = screenWidth(context); 15 | return widthCompact <= width && width < widthMedium; 16 | } 17 | 18 | static bool isMedium(BuildContext context) { 19 | final width = screenWidth(context); 20 | return widthMedium <= width && width < widthExpanded; 21 | } 22 | 23 | static bool isExpanded(BuildContext context) { 24 | final width = screenWidth(context); 25 | return widthExpanded <= width; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/src/utils/debouncer.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | class Debouncer { 4 | final Duration duration; 5 | Timer? _timer; 6 | 7 | Debouncer({required this.duration}); 8 | 9 | void run(void Function() cb) { 10 | _timer?.cancel(); 11 | _timer = Timer(duration, cb); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/src/utils/jwt_http_client.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:http/http.dart' as http; 4 | 5 | class JwtHttpClient extends http.BaseClient { 6 | final String _jwt; 7 | 8 | http.Client? _httpClient; 9 | 10 | JwtHttpClient(this._jwt); 11 | 12 | @override 13 | Future send(http.BaseRequest request) async { 14 | request.headers['authorization'] = 'Bearer $_jwt'; 15 | _httpClient ??= http.Client(); 16 | var response = await _httpClient!.send(request); 17 | 18 | return response; 19 | } 20 | 21 | /// Closes this client and its underlying HTTP client. 22 | @override 23 | void close() { 24 | _httpClient?.close(); 25 | _httpClient = null; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/src/utils/language.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:interstellar/l10n/app_localizations.dart'; 3 | import 'package:flutter_localizations/flutter_localizations.dart'; 4 | import 'package:flutter_localized_locales/flutter_localized_locales.dart'; 5 | import 'package:interstellar/src/utils/utils.dart'; 6 | import 'package:interstellar/src/widgets/selection_menu.dart'; 7 | 8 | String getLanguageName( 9 | BuildContext context, 10 | String langCode, [ 11 | bool useNativeNames = false, 12 | ]) => langCode.isEmpty 13 | ? l(context).systemLanguage 14 | : ((useNativeNames 15 | ? LocaleNamesLocalizationsDelegate.nativeLocaleNames[langCode] 16 | : LocaleNames.of(context)!.nameOf(langCode)) ?? 17 | langCode); 18 | 19 | SelectionMenu languageSelectionMenu(BuildContext context) => 20 | SelectionMenu( 21 | l(context).languages, 22 | kMaterialSupportedLanguages 23 | .map( 24 | (langTag) => SelectionMenuItem( 25 | value: langTag, 26 | title: getLanguageName(context, langTag), 27 | ), 28 | ) 29 | .toList() 30 | ..sort((lhs, rhs) => lhs.title.compareTo(rhs.title)), 31 | ); 32 | 33 | SelectionMenu languageSelectionMenuAppSupported(BuildContext context) => 34 | SelectionMenu( 35 | l(context).languages, 36 | [ 37 | '', 38 | ...AppLocalizations.supportedLocales.map( 39 | (locale) => locale.toLanguageTag(), 40 | ), 41 | ] 42 | .map( 43 | (langTag) => SelectionMenuItem( 44 | value: langTag, 45 | title: getLanguageName(context, langTag, true), 46 | ), 47 | ) 48 | .toList(), 49 | ); 50 | -------------------------------------------------------------------------------- /lib/src/utils/models.dart: -------------------------------------------------------------------------------- 1 | import 'package:interstellar/src/models/image.dart'; 2 | import 'package:interstellar/src/utils/utils.dart'; 3 | 4 | DateTime? optionalDateTime(String? value) => 5 | value == null ? null : DateTime.parse(value); 6 | 7 | List? optionalStringList(Object? json) => 8 | json == null ? null : (json as List).cast(); 9 | 10 | String? mbinCalcNextPaginationPage(JsonMap pagination) { 11 | return (pagination['currentPage'] as int) < (pagination['maxPage'] as int) 12 | ? ((pagination['currentPage'] as int) + 1).toString() 13 | : null; 14 | } 15 | 16 | ImageModel? mbinGetOptionalImage(JsonMap? json) { 17 | return json == null || (json['storageUrl'] ?? json['sourceUrl']) == null 18 | ? null 19 | : ImageModel.fromMbin(json); 20 | } 21 | 22 | ImageModel? lemmyGetOptionalImage(String? src, [String? altText]) { 23 | return src == null ? null : ImageModel.fromLemmy(src, altText); 24 | } 25 | 26 | String mbinNormalizeUsername(String username) { 27 | return username.startsWith('@') ? username.substring(1) : username; 28 | } 29 | 30 | /// Converts lemmy and piefed's local name to Mbin's standard name 31 | String getLemmyPiefedActorName(JsonMap json) { 32 | final name = (json['user_name'] ?? json['name']) as String; 33 | 34 | return (json['local'] as bool) 35 | ? name 36 | : '$name@${Uri.parse(json['actor_id'] as String).host}'; 37 | } 38 | 39 | String? lemmyCalcNextIntPage(List list, String? currentPage) => 40 | list.isEmpty ? null : (int.parse(currentPage ?? '1') + 1).toString(); 41 | -------------------------------------------------------------------------------- /lib/src/utils/share.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:file_picker/file_picker.dart'; 4 | import 'package:http/http.dart' as http; 5 | import 'package:path_provider/path_provider.dart'; 6 | import 'package:share_plus/share_plus.dart'; 7 | 8 | Future shareUri(Uri uri) async { 9 | if (Platform.isAndroid || Platform.isIOS) { 10 | return await Share.shareUri(uri); 11 | } else { 12 | return await Share.share(uri.toString()); 13 | } 14 | } 15 | 16 | Future shareFile(Uri uri, String filename) async { 17 | final response = await http.get(uri); 18 | 19 | final dir = await getTemporaryDirectory(); 20 | final file = File('${dir.path}/$filename'); 21 | await file.writeAsBytes(response.bodyBytes); 22 | 23 | final result = await Share.shareXFiles([XFile(file.path)]); 24 | 25 | await file.delete(); 26 | 27 | return result; 28 | } 29 | 30 | Future downloadFile(Uri uri, String filename) async { 31 | final response = await http.get(uri); 32 | 33 | // Whether to use bytes property or need to manually write file 34 | final useBytes = Platform.isAndroid || Platform.isIOS; 35 | 36 | String? filePath; 37 | try { 38 | filePath = await FilePicker.platform.saveFile( 39 | fileName: filename, 40 | bytes: useBytes ? response.bodyBytes : null, 41 | ); 42 | 43 | if (filePath == null) return; 44 | } catch (e) { 45 | // If file saver fails, then try to download to downloads directory 46 | final dir = await getDownloadsDirectory(); 47 | if (dir == null) throw Exception('Downloads directory not found'); 48 | 49 | filePath = '${dir.path}/$filename'; 50 | } 51 | 52 | if (!useBytes) { 53 | final file = File(filePath); 54 | await file.writeAsBytes(response.bodyBytes); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/src/utils/variables.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | final isWebViewSupported = Platform.isAndroid || Platform.isIOS; 6 | 7 | final GlobalKey scaffoldMessengerKey = 8 | GlobalKey(); 9 | -------------------------------------------------------------------------------- /lib/src/widgets/avatar.dart: -------------------------------------------------------------------------------- 1 | import 'package:blurhash_ffi/blurhash_ffi.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:interstellar/src/models/image.dart'; 4 | 5 | class Avatar extends StatelessWidget { 6 | final ImageModel? image; 7 | final ImageProvider? overrideImageProvider; 8 | final double? radius; 9 | final double? borderRadius; 10 | final Color? backgroundColor; 11 | 12 | const Avatar( 13 | this.image, { 14 | super.key, 15 | this.overrideImageProvider, 16 | this.radius, 17 | this.borderRadius, 18 | this.backgroundColor, 19 | }); 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | return CircleAvatar( 24 | radius: radius != null && borderRadius != null 25 | ? radius! + borderRadius! 26 | : radius, 27 | backgroundColor: 28 | backgroundColor ?? 29 | (radius == null || borderRadius == null ? Colors.transparent : null), 30 | child: CircleAvatar( 31 | backgroundColor: Colors.transparent, 32 | foregroundImage: 33 | overrideImageProvider ?? 34 | (image == null ? null : NetworkImage(image!.src)), 35 | backgroundImage: image == null 36 | ? const AssetImage('assets/icons/logo.png') 37 | : (image!.blurHash != null 38 | ? BlurhashFfiImage(image!.blurHash!) as ImageProvider 39 | : null), 40 | radius: radius, 41 | ), 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/src/widgets/ban_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:interstellar/src/controller/controller.dart'; 3 | import 'package:interstellar/src/models/community.dart'; 4 | import 'package:interstellar/src/models/user.dart'; 5 | import 'package:interstellar/src/utils/utils.dart'; 6 | import 'package:interstellar/src/widgets/loading_button.dart'; 7 | import 'package:interstellar/src/widgets/text_editor.dart'; 8 | import 'package:provider/provider.dart'; 9 | 10 | Future openBanDialog( 11 | BuildContext context, { 12 | required UserModel user, 13 | required CommunityModel community, 14 | }) async { 15 | await showDialog( 16 | context: context, 17 | builder: (BuildContext context) => 18 | BanDialog(user: user, community: community), 19 | ); 20 | } 21 | 22 | class BanDialog extends StatefulWidget { 23 | final UserModel user; 24 | final CommunityModel community; 25 | 26 | const BanDialog({required this.user, required this.community, super.key}); 27 | 28 | @override 29 | State createState() => _BanDialogState(); 30 | } 31 | 32 | class _BanDialogState extends State { 33 | final _reasonTextEditingController = TextEditingController(); 34 | 35 | @override 36 | Widget build(BuildContext context) { 37 | return AlertDialog( 38 | title: Text(l(context).banUser), 39 | content: Column( 40 | mainAxisSize: MainAxisSize.min, 41 | children: [ 42 | Text( 43 | l(context).banUser_help(widget.user.name, widget.community.name), 44 | ), 45 | const SizedBox(height: 16), 46 | TextEditor( 47 | _reasonTextEditingController, 48 | label: l(context).reason, 49 | onChanged: (_) => setState(() {}), 50 | ), 51 | ], 52 | ), 53 | actions: [ 54 | OutlinedButton( 55 | onPressed: () { 56 | Navigator.of(context).pop(); 57 | }, 58 | child: Text(l(context).cancel), 59 | ), 60 | LoadingFilledButton( 61 | onPressed: _reasonTextEditingController.text.isEmpty 62 | ? null 63 | : () async { 64 | await context 65 | .read() 66 | .api 67 | .communityModeration 68 | .createBan( 69 | widget.community.id, 70 | widget.user.id, 71 | reason: _reasonTextEditingController.text, 72 | ); 73 | 74 | if (!mounted) return; 75 | Navigator.of(context).pop(); 76 | }, 77 | label: Text(l(context).banUserX(widget.user.name)), 78 | uesHaptics: true, 79 | ), 80 | ], 81 | ); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /lib/src/widgets/blur.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | class Blur extends StatelessWidget { 6 | const Blur( 7 | this.child, { 8 | super.key, 9 | this.blur = 16, 10 | this.blurColor = Colors.white, 11 | this.borderRadius, 12 | this.colorOpacity = 0.2, 13 | this.overlay, 14 | this.alignment = Alignment.center, 15 | }); 16 | 17 | final Widget child; 18 | final double blur; 19 | final Color blurColor; 20 | final BorderRadius? borderRadius; 21 | final double colorOpacity; 22 | final Widget? overlay; 23 | final AlignmentGeometry alignment; 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | return ClipRRect( 28 | borderRadius: borderRadius ?? BorderRadius.zero, 29 | child: Stack( 30 | children: [ 31 | child, 32 | Positioned.fill( 33 | child: BackdropFilter( 34 | filter: ImageFilter.blur(sigmaX: blur, sigmaY: blur), 35 | child: Container( 36 | decoration: BoxDecoration( 37 | color: blurColor.withOpacity(colorOpacity), 38 | ), 39 | alignment: alignment, 40 | child: overlay, 41 | ), 42 | ), 43 | ), 44 | ], 45 | ), 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/src/widgets/display_name.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:interstellar/src/controller/controller.dart'; 3 | import 'package:interstellar/src/models/image.dart'; 4 | import 'package:interstellar/src/widgets/avatar.dart'; 5 | import 'package:provider/provider.dart'; 6 | 7 | class DisplayName extends StatelessWidget { 8 | const DisplayName(this.name, {super.key, this.icon, this.onTap}); 9 | 10 | final String name; 11 | final ImageModel? icon; 12 | final void Function()? onTap; 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | var nameTuple = name.split('@'); 17 | String localName = nameTuple.first; 18 | String? hostName = nameTuple.length > 1 ? nameTuple[1] : null; 19 | 20 | return Row( 21 | mainAxisSize: MainAxisSize.min, 22 | children: [ 23 | if (icon != null) 24 | Padding( 25 | padding: const EdgeInsets.only(right: 3), 26 | child: Avatar(icon!, radius: 14), 27 | ), 28 | Flexible( 29 | child: InkWell( 30 | onTap: onTap, 31 | child: Padding( 32 | padding: const EdgeInsets.all(3.0), 33 | child: Text( 34 | localName + 35 | (context.watch().profile.alwaysShowInstance 36 | ? '@${hostName ?? context.watch().instanceHost}' 37 | : ''), 38 | style: Theme.of(context).textTheme.labelLarge, 39 | softWrap: false, 40 | overflow: TextOverflow.fade, 41 | ), 42 | ), 43 | ), 44 | ), 45 | if (!context.watch().profile.alwaysShowInstance && 46 | hostName != null) 47 | Tooltip( 48 | message: hostName, 49 | triggerMode: TooltipTriggerMode.tap, 50 | child: const Padding( 51 | padding: EdgeInsets.fromLTRB(2, 3, 3, 3), 52 | child: Text('@'), 53 | ), 54 | ), 55 | ], 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/src/widgets/list_tile_select.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:interstellar/src/widgets/selection_menu.dart'; 3 | import 'package:material_symbols_icons/symbols.dart'; 4 | 5 | class ListTileSelect extends StatelessWidget { 6 | final String title; 7 | final IconData? icon; 8 | final SelectionMenu selectionMenu; 9 | final T value; 10 | final T? oldValue; 11 | final void Function(T newValue) onChange; 12 | 13 | const ListTileSelect({ 14 | super.key, 15 | required this.title, 16 | this.icon, 17 | required this.selectionMenu, 18 | required this.value, 19 | required this.oldValue, 20 | required this.onChange, 21 | }); 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | final curOption = selectionMenu.getOption(value); 26 | 27 | return ListTile( 28 | leading: icon != null ? Icon(icon) : null, 29 | title: Text(title), 30 | trailing: Row( 31 | mainAxisSize: MainAxisSize.min, 32 | children: [ 33 | Icon(curOption.icon, size: 20), 34 | const SizedBox(width: 4), 35 | Text(curOption.title), 36 | const Icon(Symbols.arrow_drop_down_rounded), 37 | ], 38 | ), 39 | onTap: () async { 40 | final newValue = await selectionMenu.askSelection(context, oldValue); 41 | 42 | if (newValue == null) return; 43 | 44 | onChange(newValue); 45 | }, 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/src/widgets/list_tile_switch.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ListTileSwitch extends StatelessWidget { 4 | final bool value; 5 | final void Function(bool)? onChanged; 6 | final Widget? leading; 7 | final Widget? title; 8 | final Widget? subtitle; 9 | 10 | const ListTileSwitch({ 11 | required this.value, 12 | required this.onChanged, 13 | this.leading, 14 | this.title, 15 | this.subtitle, 16 | super.key, 17 | }); 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | final color = onChanged == null ? Theme.of(context).disabledColor : null; 22 | 23 | return ListTile( 24 | leading: leading, 25 | title: title, 26 | subtitle: subtitle, 27 | onTap: onChanged == null ? null : () => onChanged!(!value), 28 | trailing: Switch(value: value, onChanged: onChanged), 29 | textColor: color, 30 | iconColor: color, 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/src/widgets/loading_list_tile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class _LoadingTileIndicator extends StatelessWidget { 4 | const _LoadingTileIndicator(); 5 | 6 | @override 7 | Widget build(BuildContext context) { 8 | return Container( 9 | width: 24, 10 | height: 24, 11 | padding: const EdgeInsets.all(2.0), 12 | child: const CircularProgressIndicator( 13 | color: Colors.white, 14 | strokeWidth: 3, 15 | ), 16 | ); 17 | } 18 | } 19 | 20 | class LoadingListTile extends StatefulWidget { 21 | final Future Function()? onTap; 22 | final Widget? leading; 23 | final Widget? title; 24 | final Widget? subtitle; 25 | final Widget? trailing; 26 | 27 | const LoadingListTile({ 28 | required this.onTap, 29 | this.leading, 30 | this.title, 31 | this.subtitle, 32 | this.trailing, 33 | super.key, 34 | }); 35 | 36 | @override 37 | State createState() => _LoadingListTileState(); 38 | } 39 | 40 | class _LoadingListTileState extends State { 41 | bool _isLoading = false; 42 | 43 | @override 44 | Widget build(BuildContext context) { 45 | final color = widget.onTap == null ? Theme.of(context).disabledColor : null; 46 | 47 | return ListTile( 48 | leading: widget.leading, 49 | title: widget.title, 50 | subtitle: widget.subtitle, 51 | onTap: widget.onTap == null 52 | ? null 53 | : () async { 54 | setState(() => _isLoading = true); 55 | try { 56 | await widget.onTap!(); 57 | } catch (e) { 58 | rethrow; 59 | } finally { 60 | if (mounted) setState(() => _isLoading = false); 61 | } 62 | }, 63 | trailing: _isLoading ? _LoadingTileIndicator() : widget.trailing, 64 | textColor: color, 65 | iconColor: color, 66 | ); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /lib/src/widgets/loading_template.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class LoadingTemplate extends StatelessWidget { 4 | final Widget? title; 5 | 6 | const LoadingTemplate({this.title, super.key}); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return Scaffold( 11 | appBar: AppBar(title: title), 12 | body: const Center(child: CircularProgressIndicator()), 13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/src/widgets/markdown/drafts_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | import 'package:interstellar/src/controller/database.dart'; 4 | import 'package:interstellar/src/utils/utils.dart'; 5 | import 'package:sembast/sembast_io.dart'; 6 | 7 | part 'drafts_controller.freezed.dart'; 8 | part 'drafts_controller.g.dart'; 9 | 10 | @freezed 11 | class Draft with _$Draft { 12 | @JsonSerializable(explicitToJson: true, includeIfNull: false) 13 | const factory Draft({ 14 | required DateTime at, 15 | required String body, 16 | String? resourceId, 17 | }) = _Draft; 18 | 19 | factory Draft.fromJson(JsonMap json) => _$DraftFromJson(json); 20 | } 21 | 22 | class DraftAutoController { 23 | final Draft? Function() read; 24 | final Future Function(String body) save; 25 | final Future Function() discard; 26 | 27 | const DraftAutoController({ 28 | required this.read, 29 | required this.save, 30 | required this.discard, 31 | }); 32 | } 33 | 34 | class DraftsController with ChangeNotifier { 35 | final _draftsStore = StoreRef('draft'); 36 | 37 | List _drafts = []; 38 | List get drafts => _drafts; 39 | 40 | DraftsController() { 41 | _init(); 42 | } 43 | 44 | Future _init() async { 45 | _drafts = (await _draftsStore.find( 46 | db, 47 | )).map((record) => Draft.fromJson(record.value)).toList(); 48 | 49 | notifyListeners(); 50 | } 51 | 52 | DraftAutoController auto(String resourceId) { 53 | return DraftAutoController( 54 | read: () { 55 | for (var draft in _drafts) { 56 | if (draft.resourceId == resourceId) return draft; 57 | } 58 | 59 | return null; 60 | }, 61 | save: (body) async { 62 | _removeByResourceId(resourceId); 63 | 64 | final draft = Draft( 65 | at: DateTime.now(), 66 | body: body, 67 | resourceId: resourceId, 68 | ); 69 | 70 | drafts.add(draft); 71 | 72 | notifyListeners(); 73 | await _draftsStore.add(db, draft.toJson()); 74 | }, 75 | discard: () async { 76 | _removeByResourceId(resourceId); 77 | 78 | notifyListeners(); 79 | }, 80 | ); 81 | } 82 | 83 | Draft? readByDate(DateTime at) { 84 | for (var draft in _drafts) { 85 | if (draft.at == at) return draft; 86 | } 87 | 88 | return null; 89 | } 90 | 91 | Future manualSave(String body) async { 92 | final draft = Draft(at: DateTime.now(), body: body); 93 | 94 | drafts.add(draft); 95 | 96 | notifyListeners(); 97 | await _draftsStore.add(db, draft.toJson()); 98 | } 99 | 100 | Future _removeByResourceId(String resourceId) async { 101 | drafts.removeWhere((draft) => draft.resourceId == resourceId); 102 | 103 | await _draftsStore.delete( 104 | db, 105 | finder: Finder(filter: Filter.equals('resourceId', resourceId)), 106 | ); 107 | } 108 | 109 | Future _removeByDate(DateTime at) async { 110 | drafts.removeWhere((draft) => draft.at == at); 111 | 112 | await _draftsStore.delete( 113 | db, 114 | finder: Finder(filter: Filter.equals('at', at.toIso8601String())), 115 | ); 116 | } 117 | 118 | Future removeByDate(DateTime at) async { 119 | _removeByDate(at); 120 | 121 | notifyListeners(); 122 | } 123 | 124 | Future removeAll() async { 125 | drafts.clear(); 126 | 127 | notifyListeners(); 128 | await _draftsStore.drop(db); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /lib/src/widgets/markdown/markdown.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_markdown/flutter_markdown.dart' as mdf; 3 | import 'package:interstellar/src/models/image.dart'; 4 | import 'package:interstellar/src/widgets/image.dart'; 5 | import 'package:interstellar/src/widgets/markdown/markdown_config_share.dart'; 6 | import 'package:interstellar/src/widgets/open_webpage.dart'; 7 | import 'package:interstellar/src/widgets/video.dart'; 8 | 9 | import './markdown_mention.dart'; 10 | import './markdown_spoiler.dart'; 11 | import './markdown_subscript_superscript.dart'; 12 | import './markdown_video.dart'; 13 | 14 | class Markdown extends StatelessWidget { 15 | final String data; 16 | final String originInstance; 17 | final ThemeData? themeData; 18 | final bool nsfw; 19 | 20 | const Markdown( 21 | this.data, 22 | this.originInstance, { 23 | this.themeData, 24 | this.nsfw = false, 25 | super.key, 26 | }); 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | return mdf.MarkdownBody( 31 | data: data, 32 | styleSheet: 33 | mdf.MarkdownStyleSheet.fromTheme( 34 | themeData ?? Theme.of(context), 35 | ).merge( 36 | mdf.MarkdownStyleSheet( 37 | blockquoteDecoration: BoxDecoration( 38 | color: Colors.blue.shade500.withAlpha(50), 39 | borderRadius: BorderRadius.circular(2.0), 40 | ), 41 | ), 42 | ), 43 | onTapLink: (text, href, title) async { 44 | if (href != null) { 45 | openWebpageSecondary(context, Uri.parse(href)); 46 | } 47 | }, 48 | imageBuilder: (uri, title, alt) { 49 | if (uri.path.split('.').last == 'mp4') { 50 | return VideoPlayer(uri); 51 | } 52 | return AdvancedImage( 53 | ImageModel( 54 | src: uri.toString(), 55 | altText: alt, 56 | blurHash: null, 57 | blurHashWidth: null, 58 | blurHashHeight: null, 59 | ), 60 | openTitle: title ?? '', 61 | enableBlur: nsfw, 62 | ); 63 | }, 64 | inlineSyntaxes: [ 65 | SubscriptMarkdownSyntax(), 66 | SuperscriptMarkdownSyntax(), 67 | MentionMarkdownSyntax(), 68 | VideoMarkdownSyntax(), 69 | YoutubeEmbedSyntax(), 70 | ], 71 | blockSyntaxes: [SpoilerMarkdownSyntax(), ConfigShareMarkdownSyntax()], 72 | builders: { 73 | 'sub': SubscriptMarkdownBuilder(), 74 | 'sup': SuperscriptMarkdownBuilder(), 75 | 'mention': MentionMarkdownBuilder(originInstance: originInstance), 76 | 'video': VideoMarkdownBuilder(), 77 | 'spoiler': SpoilerMarkdownBuilder(originInstance: originInstance), 78 | 'config-share': ConfigShareMarkdownBuilder(), 79 | }, 80 | ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /lib/src/widgets/markdown/markdown_subscript_superscript.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_markdown/flutter_markdown.dart' as mdf; 3 | import 'package:markdown/markdown.dart' as md; 4 | 5 | class SubscriptMarkdownSyntax extends md.InlineSyntax { 6 | SubscriptMarkdownSyntax() : super(r'~([^~\s]+)~'); 7 | 8 | @override 9 | bool onMatch(md.InlineParser parser, Match match) { 10 | parser.addNode(md.Element.text('sub', match[1]!)); 11 | return true; 12 | } 13 | } 14 | 15 | class SuperscriptMarkdownSyntax extends md.InlineSyntax { 16 | SuperscriptMarkdownSyntax() : super(r'\^([^\s^]+)\^'); 17 | 18 | @override 19 | bool onMatch(md.InlineParser parser, Match match) { 20 | parser.addNode(md.Element.text('sup', match[1]!)); 21 | return true; 22 | } 23 | } 24 | 25 | class SubscriptMarkdownBuilder extends mdf.MarkdownElementBuilder { 26 | @override 27 | Widget visitElementAfter(md.Element element, TextStyle? preferredStyle) { 28 | final String textContent = element.textContent; 29 | 30 | return SubscriptSuperscriptWidget(text: textContent, isSuperscript: false); 31 | } 32 | } 33 | 34 | class SuperscriptMarkdownBuilder extends mdf.MarkdownElementBuilder { 35 | @override 36 | Widget visitElementAfter(md.Element element, TextStyle? preferredStyle) { 37 | final String textContent = element.textContent; 38 | 39 | return SubscriptSuperscriptWidget(text: textContent, isSuperscript: true); 40 | } 41 | } 42 | 43 | class SubscriptSuperscriptWidget extends StatelessWidget { 44 | final String text; 45 | final bool isSuperscript; 46 | 47 | const SubscriptSuperscriptWidget({ 48 | super.key, 49 | required this.text, 50 | required this.isSuperscript, 51 | }); 52 | 53 | @override 54 | Widget build(BuildContext context) { 55 | return RichText( 56 | text: TextSpan( 57 | children: [ 58 | WidgetSpan( 59 | child: Transform.translate( 60 | offset: Offset(0.0, isSuperscript ? -5.0 : 3.0), 61 | child: Text( 62 | text, 63 | style: Theme.of( 64 | context, 65 | ).textTheme.bodyMedium?.copyWith(fontSize: 11), 66 | ), 67 | ), 68 | ), 69 | ], 70 | ), 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lib/src/widgets/markdown/markdown_video.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_markdown/flutter_markdown.dart' as mdf; 3 | import 'package:markdown/markdown.dart' as md; 4 | import 'package:interstellar/src/widgets/video.dart'; 5 | 6 | class VideoMarkdownSyntax extends md.InlineSyntax { 7 | VideoMarkdownSyntax() : super(r'!\[video\/mp4\]\((https:\/\/[^\s]+\.mp4)\)'); 8 | 9 | @override 10 | bool onMatch(md.InlineParser parser, Match match) { 11 | parser.addNode(md.Element.text('video', match[1]!)); 12 | return true; 13 | } 14 | } 15 | 16 | class YoutubeEmbedSyntax extends md.InlineSyntax { 17 | //from here https://stackoverflow.com/a/61033353 18 | static const _youtubePattern = 19 | r'(?:https?:\/\/)?(?:www\.)?youtu(?:\.be\/|be.com\/\S*(?:watch|embed)(?:(?:(?=\/[-a-zA-Z0-9_]{11,}(?!\S))\/)|(?:\S*v=|v\/)))([-a-zA-Z0-9_]{11,})'; 20 | 21 | static const String _mdLinkPattern = 22 | r'\[(.*?)\]\(\s*' + _youtubePattern + r'(?:\s*".*?")?\s*\)'; 23 | 24 | static final _mdLinkPatternRegExp = RegExp( 25 | _mdLinkPattern, 26 | multiLine: true, 27 | caseSensitive: true, 28 | ); 29 | static final _borderRegExp = RegExp(r'[^a-z0-9@/\\]', caseSensitive: false); 30 | 31 | YoutubeEmbedSyntax() : super(_youtubePattern); 32 | 33 | bool _isMarkdownLink = false; 34 | 35 | @override 36 | bool tryMatch(md.InlineParser parser, [int? startMatchPos]) { 37 | startMatchPos ??= parser.pos; 38 | 39 | _isMarkdownLink = String.fromCharCode(parser.charAt(parser.pos)) == '['; 40 | bool isAutoLink = String.fromCharCode(parser.charAt(parser.pos)) == '<'; 41 | if (isAutoLink) { 42 | startMatchPos += 1; 43 | } 44 | 45 | if (parser.pos > 0 && !_isMarkdownLink && !isAutoLink) { 46 | final precededBy = String.fromCharCode(parser.charAt(parser.pos - 1)); 47 | if (_borderRegExp.matchAsPrefix(precededBy) == null) { 48 | return false; 49 | } 50 | } 51 | 52 | final match = (_isMarkdownLink ? _mdLinkPatternRegExp : pattern) 53 | .matchAsPrefix(parser.source, startMatchPos); 54 | if (match == null) return false; 55 | 56 | if (parser.source.length > match.end && !_isMarkdownLink && !isAutoLink) { 57 | final followedBy = String.fromCharCode(parser.charAt(match.end)); 58 | if (_borderRegExp.matchAsPrefix(followedBy) == null) { 59 | return false; 60 | } 61 | } 62 | if (isAutoLink && String.fromCharCode(parser.charAt(match.end)) == '>') { 63 | parser.consume(2); 64 | startMatchPos += 1; 65 | } 66 | 67 | parser.writeText(); 68 | 69 | if (onMatch(parser, match)) parser.consume(match[0]!.length); 70 | return true; 71 | } 72 | 73 | @override 74 | bool onMatch(md.InlineParser parser, Match match) { 75 | final link = 76 | 'https://www.youtube.com/watch?v=${match[_isMarkdownLink ? 2 : 1]!}'; 77 | 78 | final anchor = md.Element.text('a', match[_isMarkdownLink ? 1 : 0]!); 79 | anchor.attributes['href'] = link; 80 | 81 | parser.addNode(anchor); 82 | 83 | parser.addNode(md.Element.text('video', link)); 84 | return true; 85 | } 86 | } 87 | 88 | class VideoMarkdownBuilder extends mdf.MarkdownElementBuilder { 89 | @override 90 | Widget visitElementAfter(md.Element element, TextStyle? preferredStyle) { 91 | var textContent = element.textContent; 92 | 93 | return VideoPlayer(Uri.parse(textContent)); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lib/src/widgets/notification_control_segment.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:interstellar/src/controller/controller.dart'; 3 | import 'package:interstellar/src/controller/server.dart'; 4 | import 'package:interstellar/src/models/notification.dart'; 5 | import 'package:interstellar/src/utils/utils.dart'; 6 | import 'package:material_symbols_icons/symbols.dart'; 7 | import 'package:provider/provider.dart'; 8 | 9 | class NotificationControlSegment extends StatelessWidget { 10 | final NotificationControlStatus value; 11 | final Future Function(NotificationControlStatus) onChange; 12 | 13 | const NotificationControlSegment(this.value, this.onChange, {super.key}); 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return SegmentedButton( 18 | segments: [ 19 | // Mbin allows muted notification status, but PieFed does not 20 | if (context.read().serverSoftware == ServerSoftware.mbin) 21 | ButtonSegment( 22 | value: NotificationControlStatus.muted, 23 | icon: const Icon(Symbols.notifications_off_rounded), 24 | tooltip: l(context).notificationControlStatus_muted, 25 | ), 26 | ButtonSegment( 27 | value: NotificationControlStatus.default_, 28 | icon: const Icon(Symbols.notifications_rounded), 29 | tooltip: l(context).notificationControlStatus_default, 30 | ), 31 | ButtonSegment( 32 | value: NotificationControlStatus.loud, 33 | icon: const Icon(Symbols.campaign_rounded), 34 | tooltip: l(context).notificationControlStatus_loud, 35 | ), 36 | ], 37 | selected: {value}, 38 | onSelectionChanged: (newSelection) { 39 | onChange(newSelection.first); 40 | }, 41 | showSelectedIcon: false, 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/src/widgets/open_webpage.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:interstellar/src/utils/share.dart'; 3 | import 'package:interstellar/src/utils/utils.dart'; 4 | import 'package:interstellar/src/utils/variables.dart'; 5 | import 'package:url_launcher/url_launcher.dart'; 6 | import 'package:webview_flutter/webview_flutter.dart'; 7 | 8 | void openWebpagePrimary(BuildContext context, Uri uri) { 9 | launchUrl(uri); 10 | } 11 | 12 | void openWebpageSecondary(BuildContext context, Uri uri) { 13 | showDialog( 14 | context: context, 15 | builder: (BuildContext context) => AlertDialog( 16 | title: Text(l(context).openLink), 17 | content: SelectableText(uri.toString()), 18 | actions: [ 19 | OutlinedButton( 20 | onPressed: () => Navigator.pop(context), 21 | child: Text(l(context).cancel), 22 | ), 23 | FilledButton.tonal( 24 | onPressed: () { 25 | Navigator.pop(context); 26 | 27 | shareUri(uri); 28 | }, 29 | child: Text(l(context).share), 30 | ), 31 | if (isWebViewSupported) 32 | FilledButton.tonal( 33 | onPressed: () { 34 | Navigator.pop(context); 35 | 36 | var controller = WebViewController() 37 | ..setJavaScriptMode(JavaScriptMode.unrestricted) 38 | ..loadRequest(uri); 39 | 40 | Navigator.of(context).push( 41 | MaterialPageRoute( 42 | builder: (context) => Scaffold( 43 | appBar: AppBar(), 44 | body: WebViewWidget(controller: controller), 45 | ), 46 | ), 47 | ); 48 | }, 49 | child: Text(l(context).webView), 50 | ), 51 | FilledButton( 52 | onPressed: () { 53 | Navigator.pop(context); 54 | 55 | launchUrl(uri); 56 | }, 57 | child: Text(l(context).browser), 58 | ), 59 | ], 60 | actionsOverflowAlignment: OverflowBarAlignment.center, 61 | actionsOverflowButtonSpacing: 8, 62 | actionsOverflowDirection: VerticalDirection.up, 63 | ), 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /lib/src/widgets/password_editor.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:interstellar/src/utils/utils.dart'; 3 | 4 | class PasswordEditor extends StatefulWidget { 5 | final TextEditingController controller; 6 | final void Function(String)? onChanged; 7 | 8 | const PasswordEditor(this.controller, {this.onChanged, super.key}); 9 | 10 | @override 11 | State createState() => _PasswordEditorState(); 12 | } 13 | 14 | class _PasswordEditorState extends State { 15 | bool obscureText = true; 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return TextField( 20 | controller: widget.controller, 21 | keyboardType: TextInputType.visiblePassword, 22 | decoration: InputDecoration( 23 | border: const OutlineInputBorder(), 24 | labelText: l(context).password, 25 | suffixIcon: IconButton( 26 | icon: Icon( 27 | obscureText 28 | ? Icons.visibility_off_rounded 29 | : Icons.visibility_rounded, 30 | ), 31 | onPressed: () => setState(() => obscureText = !obscureText), 32 | ), 33 | ), 34 | onChanged: widget.onChanged, 35 | autofillHints: [AutofillHints.password], 36 | obscureText: obscureText, 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/src/widgets/redirect_listen.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/services.dart'; 5 | import 'package:interstellar/src/utils/utils.dart'; 6 | import 'package:interstellar/src/utils/variables.dart'; 7 | import 'package:interstellar/src/widgets/loading_button.dart'; 8 | import 'package:url_launcher/url_launcher.dart'; 9 | import 'package:webview_flutter/webview_flutter.dart'; 10 | 11 | const _redirectHost = 'localhost'; 12 | const _redirectPort = 46837; 13 | const redirectUri = 'http://$_redirectHost:$_redirectPort'; 14 | 15 | class RedirectListener extends StatefulWidget { 16 | final Uri initUri; 17 | final String title; 18 | 19 | const RedirectListener(this.initUri, {super.key, this.title = ''}); 20 | 21 | @override 22 | State createState() => _RedirectListenerState(); 23 | } 24 | 25 | class _RedirectListenerState extends State { 26 | WebViewController? _controller; 27 | 28 | Future _listenForAuth() async { 29 | HttpServer server = await HttpServer.bind(_redirectHost, _redirectPort); 30 | await launchUrl(widget.initUri); 31 | final req = await server.first; 32 | 33 | if (!mounted) return Uri(); 34 | 35 | final result = req.uri; 36 | req.response.statusCode = 200; 37 | req.response.headers.set('content-type', 'text/plain'); 38 | req.response.writeln(l(context).redirectReceivedMessage); 39 | await req.response.close(); 40 | await server.close(); 41 | return result; 42 | } 43 | 44 | @override 45 | void initState() { 46 | super.initState(); 47 | if (!isWebViewSupported) { 48 | _listenForAuth().then( 49 | (value) => Navigator.pop(context, value.queryParameters), 50 | ); 51 | } else { 52 | _controller = WebViewController() 53 | ..setJavaScriptMode(JavaScriptMode.unrestricted) 54 | ..setNavigationDelegate( 55 | NavigationDelegate( 56 | onNavigationRequest: (NavigationRequest request) { 57 | if (request.url.startsWith(redirectUri)) { 58 | WebViewCookieManager().clearCookies(); 59 | Navigator.pop(context, Uri.parse(request.url).queryParameters); 60 | return NavigationDecision.prevent; 61 | } 62 | 63 | return NavigationDecision.navigate; 64 | }, 65 | ), 66 | ) 67 | ..loadRequest(widget.initUri); 68 | } 69 | } 70 | 71 | @override 72 | Widget build(BuildContext context) { 73 | if (isWebViewSupported) { 74 | return Scaffold( 75 | appBar: AppBar(title: Text(widget.title)), 76 | body: WebViewWidget(controller: _controller!), 77 | ); 78 | } 79 | 80 | return Scaffold( 81 | appBar: AppBar(title: Text(widget.title)), 82 | body: Center( 83 | child: Column( 84 | mainAxisSize: MainAxisSize.min, 85 | children: [ 86 | Text(l(context).continueInBrowser), 87 | const SizedBox(height: 8), 88 | LoadingTextButton( 89 | onPressed: () async { 90 | await Clipboard.setData( 91 | ClipboardData(text: widget.initUri.toString()), 92 | ); 93 | }, 94 | label: Text(l(context).continueInBrowser_manual), 95 | ), 96 | ], 97 | ), 98 | ), 99 | ); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /lib/src/widgets/report_content.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:interstellar/src/utils/utils.dart'; 3 | import 'package:interstellar/src/widgets/text_editor.dart'; 4 | 5 | Future reportContent(BuildContext context, String contentTypeName) => 6 | showDialog( 7 | context: context, 8 | builder: (BuildContext context) => 9 | ReportContentBody(contentTypeName: contentTypeName), 10 | ); 11 | 12 | class ReportContentBody extends StatefulWidget { 13 | final String contentTypeName; 14 | 15 | const ReportContentBody({required this.contentTypeName, super.key}); 16 | 17 | @override 18 | State createState() => _ReportContentBodyState(); 19 | } 20 | 21 | class _ReportContentBodyState extends State { 22 | final _reasonTextEditingController = TextEditingController(); 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | return AlertDialog( 27 | title: Text(l(context).reportX(widget.contentTypeName)), 28 | content: TextEditor( 29 | _reasonTextEditingController, 30 | label: l(context).reason, 31 | onChanged: (_) => setState(() {}), 32 | ), 33 | actions: [ 34 | OutlinedButton( 35 | onPressed: () => Navigator.pop(context), 36 | child: Text(l(context).cancel), 37 | ), 38 | FilledButton( 39 | onPressed: _reasonTextEditingController.text.isEmpty 40 | ? null 41 | : () => Navigator.pop(context, _reasonTextEditingController.text), 42 | child: Text(l(context).report), 43 | ), 44 | ], 45 | actionsOverflowAlignment: OverflowBarAlignment.center, 46 | actionsOverflowButtonSpacing: 8, 47 | actionsOverflowDirection: VerticalDirection.up, 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/src/widgets/scaffold.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:interstellar/src/utils/breakpoints.dart'; 3 | 4 | /// Wrapper of [Scaffold] which displays the drawer persistently based on screen size. 5 | class AdvancedScaffold extends StatelessWidget { 6 | final Widget body; 7 | final PreferredSizeWidget? appBar; 8 | final Widget? floatingActionButton; 9 | final Widget? drawer; 10 | 11 | const AdvancedScaffold({ 12 | required this.body, 13 | this.appBar, 14 | this.floatingActionButton, 15 | this.drawer, 16 | super.key, 17 | }); 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | final hasDrawer = drawer != null; 22 | final isExpanded = Breakpoints.isExpanded(context); 23 | 24 | return Scaffold( 25 | appBar: appBar, 26 | body: Row( 27 | children: [ 28 | if (hasDrawer && isExpanded) SizedBox(width: 360, child: drawer!), 29 | Expanded(child: body), 30 | ], 31 | ), 32 | floatingActionButton: floatingActionButton, 33 | drawer: hasDrawer && !isExpanded 34 | ? Drawer(child: SafeArea(child: drawer!)) 35 | : null, 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/src/widgets/selection_menu.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class SelectionMenuItem { 4 | final T value; 5 | final String title; 6 | final IconData? icon; 7 | final Color? iconColor; 8 | final String? subtitle; 9 | final List>? subItems; 10 | 11 | const SelectionMenuItem({ 12 | required this.value, 13 | required this.title, 14 | this.icon, 15 | this.iconColor, 16 | this.subtitle, 17 | this.subItems, 18 | }); 19 | 20 | SelectionMenu? get subItemsSelectionMenu => 21 | subItems == null ? null : SelectionMenu(title, subItems!); 22 | } 23 | 24 | class SelectionMenu { 25 | final String title; 26 | final List> options; 27 | 28 | const SelectionMenu(this.title, this.options); 29 | 30 | Future askSelection(BuildContext context, T? oldSelection) async => 31 | showModalBottomSheet( 32 | context: context, 33 | builder: (BuildContext context) { 34 | return Column( 35 | crossAxisAlignment: CrossAxisAlignment.stretch, 36 | mainAxisSize: MainAxisSize.min, 37 | children: [ 38 | Padding( 39 | padding: const EdgeInsets.all(12), 40 | child: Text( 41 | title, 42 | style: Theme.of(context).textTheme.titleLarge, 43 | textAlign: TextAlign.center, 44 | ), 45 | ), 46 | Flexible( 47 | child: ListView( 48 | shrinkWrap: true, 49 | children: [ 50 | ...options.map( 51 | (option) => ListTile( 52 | title: Text(option.title), 53 | onTap: () => Navigator.pop(context, option.value), 54 | leading: Icon(option.icon, color: option.iconColor), 55 | selected: oldSelection == option.value, 56 | selectedTileColor: Theme.of( 57 | context, 58 | ).colorScheme.primaryContainer.withOpacity(0.2), 59 | subtitle: option.subtitle != null 60 | ? Text(option.subtitle!) 61 | : null, 62 | trailing: 63 | option.subItems != null && 64 | option.subItems!.isNotEmpty 65 | ? IconButton( 66 | icon: Icon(Icons.arrow_right), 67 | onPressed: () async { 68 | final subSelection = await option 69 | .subItemsSelectionMenu! 70 | .askSelection(context, oldSelection); 71 | if (!context.mounted) return; 72 | Navigator.pop(context, subSelection); 73 | }, 74 | ) 75 | : null, 76 | ), 77 | ), 78 | const SizedBox(height: 16), 79 | ], 80 | ), 81 | ), 82 | ], 83 | ); 84 | }, 85 | ); 86 | 87 | SelectionMenuItem getOption(T value) { 88 | for (var option in options) { 89 | if (option.subItems == null) continue; 90 | try { 91 | return option.subItemsSelectionMenu!.getOption(value); 92 | } catch (_) {} 93 | } 94 | return options.firstWhere((option) => option.value == value); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /lib/src/widgets/server_software_indicator.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:interstellar/src/controller/server.dart'; 3 | 4 | class ServerSoftwareIndicator extends StatelessWidget { 5 | final String label; 6 | final ServerSoftware software; 7 | 8 | const ServerSoftwareIndicator({ 9 | super.key, 10 | required this.label, 11 | required this.software, 12 | }); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return Badge( 17 | label: Text(software.title), 18 | backgroundColor: software.color, 19 | textColor: Colors.white, 20 | alignment: Alignment.centerRight, 21 | offset: Offset(20, -6), 22 | child: Text(label), 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/src/widgets/settings_header.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class SettingsHeader extends StatelessWidget { 4 | final String text; 5 | 6 | const SettingsHeader(this.text, {super.key}); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return Padding( 11 | padding: const EdgeInsets.symmetric(vertical: 8), 12 | child: Text( 13 | text, 14 | style: Theme.of(context).textTheme.titleMedium!.merge( 15 | const TextStyle(fontWeight: FontWeight.w600), 16 | ), 17 | ), 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/src/widgets/star_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:interstellar/src/controller/controller.dart'; 3 | import 'package:material_symbols_icons/symbols.dart'; 4 | import 'package:provider/provider.dart'; 5 | 6 | class StarButton extends StatelessWidget { 7 | final String name; 8 | 9 | const StarButton(this.name, {super.key}); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | final isStarred = context.watch().stars.contains(name); 14 | 15 | return IconButton( 16 | onPressed: isStarred 17 | ? () => context.read().removeStar(name) 18 | : () => context.read().addStar(name), 19 | icon: context.read().stars.contains(name) 20 | ? const Icon(Symbols.star_rounded, fill: 1) 21 | : const Icon(Symbols.star_rounded), 22 | color: isStarred ? Colors.yellow : null, 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/src/widgets/subordinate_scroll.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | // Helper to prevent multiple scroll positions being attached to 4 | // a single parent scroll controller. 5 | // Attaches/detaches its position from the parent whenever isActive is changed. 6 | // Useful for when having multiple CustomScrollViews under a single 7 | // NestedScrollView. 8 | 9 | class SubordinateScrollController extends ScrollController { 10 | SubordinateScrollController({ 11 | required ScrollController parent, 12 | String? debugLabel, 13 | }) : _parent = parent, 14 | super( 15 | initialScrollOffset: parent.initialScrollOffset, 16 | keepScrollOffset: parent.keepScrollOffset, 17 | debugLabel: switch ((parent.debugLabel, debugLabel)) { 18 | (null, null) => null, 19 | (null, String label) => label, 20 | (String label, null) => '$label/sub', 21 | (String parentLabel, String label) => '$parentLabel/$label', 22 | }, 23 | ); 24 | 25 | final ScrollController _parent; 26 | bool _isActive = false; 27 | 28 | ScrollController get parent => _parent; 29 | 30 | bool get isActive => _isActive; 31 | set isActive(bool value) { 32 | if (_isActive != value) { 33 | _isActive = value; 34 | if (_isActive) { 35 | _attachToParent(); 36 | } else { 37 | _detachFromParent(); 38 | } 39 | } 40 | } 41 | 42 | @override 43 | ScrollPosition createScrollPosition( 44 | ScrollPhysics physics, 45 | ScrollContext context, 46 | ScrollPosition? oldPosition, 47 | ) { 48 | return _parent.createScrollPosition(physics, context, oldPosition); 49 | } 50 | 51 | @override 52 | void attach(ScrollPosition position) { 53 | super.attach(position); 54 | if (_isActive) { 55 | _parent.attach(position); 56 | } 57 | } 58 | 59 | @override 60 | void detach(ScrollPosition position) { 61 | if (_isActive) { 62 | _parent.detach(position); 63 | } 64 | super.detach(position); 65 | } 66 | 67 | void _detachFromParent() { 68 | for (final position in positions) { 69 | _parent.detach(position); 70 | } 71 | } 72 | 73 | void _attachToParent() { 74 | for (final position in positions) { 75 | _parent.attach(position); 76 | } 77 | } 78 | 79 | @override 80 | void dispose() { 81 | if (isActive) { 82 | isActive = false; 83 | } 84 | super.dispose(); 85 | } 86 | } -------------------------------------------------------------------------------- /lib/src/widgets/subscription_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:interstellar/src/controller/controller.dart'; 3 | import 'package:interstellar/src/utils/utils.dart'; 4 | import 'package:interstellar/src/widgets/loading_button.dart'; 5 | import 'package:material_symbols_icons/symbols.dart'; 6 | import 'package:provider/provider.dart'; 7 | 8 | class SubscriptionButton extends StatelessWidget { 9 | final bool? isSubscribed; 10 | final int subscriptionCount; 11 | final Future Function(bool) onSubscribe; 12 | final bool followMode; 13 | 14 | const SubscriptionButton({ 15 | required this.isSubscribed, 16 | required this.subscriptionCount, 17 | required this.onSubscribe, 18 | required this.followMode, 19 | super.key, 20 | }); 21 | 22 | @override 23 | Widget build(BuildContext context) { 24 | return LoadingChip( 25 | selected: isSubscribed ?? false, 26 | icon: const Icon(Symbols.people_rounded), 27 | label: Text(intFormat(subscriptionCount)), 28 | onSelected: whenLoggedIn( 29 | context, 30 | context.watch().profile.askBeforeUnsubscribing 31 | ? (newValue) async { 32 | // Only show confirm dialog for unsubscribes, not subscribes 33 | final confirm = newValue 34 | ? true 35 | : await showDialog( 36 | context: context, 37 | builder: (context) => AlertDialog( 38 | title: Text( 39 | followMode 40 | ? l(context).confirmUnfollow 41 | : l(context).confirmUnsubscribe, 42 | ), 43 | actions: [ 44 | OutlinedButton( 45 | onPressed: () => Navigator.pop(context), 46 | child: Text(l(context).cancel), 47 | ), 48 | FilledButton( 49 | onPressed: () => Navigator.pop(context, true), 50 | child: Text(l(context).continue_), 51 | ), 52 | ], 53 | ), 54 | ); 55 | 56 | if (confirm == true) await onSubscribe(newValue); 57 | } 58 | : onSubscribe, 59 | ), 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/src/widgets/super_hero.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | // Is workaround for having nested hero animations which is disallowed by 4 | // default in flutter but works anyway. 5 | class SuperHero extends Hero { 6 | const SuperHero({ 7 | required super.tag, 8 | super.key, 9 | super.createRectTween, 10 | super.flightShuttleBuilder, 11 | super.placeholderBuilder, 12 | super.transitionOnUserGestures, 13 | required super.child, 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /lib/src/widgets/text_editor.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class TextEditor extends StatelessWidget { 4 | final TextEditingController controller; 5 | final TextInputType? keyboardType; 6 | final String? label; 7 | final String? hint; 8 | final void Function(String)? onChanged; 9 | final bool? enabled; 10 | final int? maxLength; 11 | final List? autofillHints; 12 | 13 | const TextEditor( 14 | this.controller, { 15 | this.keyboardType, 16 | this.label, 17 | this.hint, 18 | this.onChanged, 19 | this.enabled, 20 | this.maxLength, 21 | this.autofillHints, 22 | super.key, 23 | }); 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | return TextField( 28 | controller: controller, 29 | keyboardType: keyboardType, 30 | decoration: InputDecoration( 31 | border: const OutlineInputBorder(), 32 | labelText: label, 33 | hintText: hint, 34 | ), 35 | onChanged: onChanged, 36 | enabled: enabled, 37 | maxLength: maxLength, 38 | autofillHints: autofillHints, 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/src/widgets/user_status_icons.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:interstellar/src/utils/utils.dart'; 5 | import 'package:material_symbols_icons/symbols.dart'; 6 | 7 | class UserStatusIcons extends StatelessWidget { 8 | final DateTime? cakeDay; 9 | final bool isBot; 10 | 11 | const UserStatusIcons({ 12 | required this.cakeDay, 13 | required this.isBot, 14 | super.key, 15 | }); 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | final now = DateTime.now(); 20 | 21 | Widget? botWidget; 22 | Widget? cakeDayWidget; 23 | 24 | if (isBot) { 25 | botWidget = Tooltip( 26 | message: l(context).botAccount, 27 | child: const Icon(Symbols.smart_toy_rounded), 28 | ); 29 | } 30 | 31 | if (cakeDay == null) { 32 | } else if (now.difference(cakeDay!).inDays <= 14) { 33 | cakeDayWidget = Tooltip( 34 | message: l(context).newUser, 35 | child: ShaderMask( 36 | blendMode: BlendMode.srcIn, 37 | shaderCallback: (Rect bounds) => const LinearGradient( 38 | transform: GradientRotation(pi / 2), 39 | stops: [0, 1], 40 | colors: [Colors.green, Colors.lightGreen], 41 | ).createShader(bounds), 42 | child: const Icon(Symbols.psychiatry_rounded), 43 | ), 44 | ); 45 | } else if (cakeDay!.day == now.day && cakeDay!.month == now.month) { 46 | cakeDayWidget = Tooltip( 47 | message: l(context).cakeDay, 48 | child: ShaderMask( 49 | blendMode: BlendMode.srcIn, 50 | shaderCallback: (Rect bounds) => const LinearGradient( 51 | transform: GradientRotation(pi / 2), 52 | stops: [0, 0.5, 1], 53 | colors: [Colors.yellow, Colors.pink, Colors.blue], 54 | ).createShader(bounds), 55 | child: const Icon(Symbols.cake_rounded), 56 | ), 57 | ); 58 | } 59 | 60 | return Row( 61 | children: [?botWidget, ?cakeDayWidget] 62 | .map( 63 | (widget) => 64 | Padding(padding: const EdgeInsets.only(left: 5), child: widget), 65 | ) 66 | .toList(), 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /lib/src/widgets/wrapper.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class Wrapper extends StatelessWidget { 4 | final bool shouldWrap; 5 | final Widget Function(Widget child) parentBuilder; 6 | final Widget child; 7 | 8 | const Wrapper({ 9 | super.key, 10 | required this.shouldWrap, 11 | required this.parentBuilder, 12 | required this.child, 13 | }); 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return shouldWrap ? parentBuilder(child) : child; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /linux/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral 2 | -------------------------------------------------------------------------------- /linux/appimage/interstellar.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Name=Interstellar 4 | Exec=interstellar %u 5 | Icon=interstellar 6 | Categories=Network 7 | StartupWMClass=interstellar 8 | -------------------------------------------------------------------------------- /linux/flutter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This file controls Flutter-level build steps. It should not be edited. 2 | cmake_minimum_required(VERSION 3.10) 3 | 4 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") 5 | 6 | # Configuration provided via flutter tool. 7 | include(${EPHEMERAL_DIR}/generated_config.cmake) 8 | 9 | # TODO: Move the rest of this into files in ephemeral. See 10 | # https://github.com/flutter/flutter/issues/57146. 11 | 12 | # Serves the same purpose as list(TRANSFORM ... PREPEND ...), 13 | # which isn't available in 3.10. 14 | function(list_prepend LIST_NAME PREFIX) 15 | set(NEW_LIST "") 16 | foreach(element ${${LIST_NAME}}) 17 | list(APPEND NEW_LIST "${PREFIX}${element}") 18 | endforeach(element) 19 | set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) 20 | endfunction() 21 | 22 | # === Flutter Library === 23 | # System-level dependencies. 24 | find_package(PkgConfig REQUIRED) 25 | pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) 26 | pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) 27 | pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) 28 | 29 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") 30 | 31 | # Published to parent scope for install step. 32 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) 33 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) 34 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) 35 | set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) 36 | 37 | list(APPEND FLUTTER_LIBRARY_HEADERS 38 | "fl_basic_message_channel.h" 39 | "fl_binary_codec.h" 40 | "fl_binary_messenger.h" 41 | "fl_dart_project.h" 42 | "fl_engine.h" 43 | "fl_json_message_codec.h" 44 | "fl_json_method_codec.h" 45 | "fl_message_codec.h" 46 | "fl_method_call.h" 47 | "fl_method_channel.h" 48 | "fl_method_codec.h" 49 | "fl_method_response.h" 50 | "fl_plugin_registrar.h" 51 | "fl_plugin_registry.h" 52 | "fl_standard_message_codec.h" 53 | "fl_standard_method_codec.h" 54 | "fl_string_codec.h" 55 | "fl_value.h" 56 | "fl_view.h" 57 | "flutter_linux.h" 58 | ) 59 | list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") 60 | add_library(flutter INTERFACE) 61 | target_include_directories(flutter INTERFACE 62 | "${EPHEMERAL_DIR}" 63 | ) 64 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") 65 | target_link_libraries(flutter INTERFACE 66 | PkgConfig::GTK 67 | PkgConfig::GLIB 68 | PkgConfig::GIO 69 | ) 70 | add_dependencies(flutter flutter_assemble) 71 | 72 | # === Flutter tool backend === 73 | # _phony_ is a non-existent file to force this command to run every time, 74 | # since currently there's no way to get a full input/output list from the 75 | # flutter tool. 76 | add_custom_command( 77 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} 78 | ${CMAKE_CURRENT_BINARY_DIR}/_phony_ 79 | COMMAND ${CMAKE_COMMAND} -E env 80 | ${FLUTTER_TOOL_ENVIRONMENT} 81 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" 82 | ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} 83 | VERBATIM 84 | ) 85 | add_custom_target(flutter_assemble DEPENDS 86 | "${FLUTTER_LIBRARY}" 87 | ${FLUTTER_LIBRARY_HEADERS} 88 | ) 89 | -------------------------------------------------------------------------------- /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 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | void fl_register_plugins(FlPluginRegistry* registry) { 19 | g_autoptr(FlPluginRegistrar) dynamic_color_registrar = 20 | fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin"); 21 | dynamic_color_plugin_register_with_registrar(dynamic_color_registrar); 22 | g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = 23 | fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); 24 | file_selector_plugin_register_with_registrar(file_selector_linux_registrar); 25 | g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar = 26 | fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin"); 27 | media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar); 28 | g_autoptr(FlPluginRegistrar) media_kit_video_registrar = 29 | fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitVideoPlugin"); 30 | media_kit_video_plugin_register_with_registrar(media_kit_video_registrar); 31 | g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar = 32 | fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin"); 33 | screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar); 34 | g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = 35 | fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); 36 | url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); 37 | g_autoptr(FlPluginRegistrar) webcrypto_registrar = 38 | fl_plugin_registry_get_registrar_for_plugin(registry, "WebcryptoPlugin"); 39 | webcrypto_plugin_register_with_registrar(webcrypto_registrar); 40 | g_autoptr(FlPluginRegistrar) window_manager_registrar = 41 | fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); 42 | window_manager_plugin_register_with_registrar(window_manager_registrar); 43 | } 44 | -------------------------------------------------------------------------------- /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 | dynamic_color 7 | file_selector_linux 8 | media_kit_libs_linux 9 | media_kit_video 10 | screen_retriever_linux 11 | url_launcher_linux 12 | webcrypto 13 | window_manager 14 | ) 15 | 16 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 17 | blurhash_ffi 18 | ) 19 | 20 | set(PLUGIN_BUNDLED_LIBRARIES) 21 | 22 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 23 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) 24 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 25 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 26 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 27 | endforeach(plugin) 28 | 29 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 30 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) 31 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 32 | endforeach(ffi_plugin) 33 | -------------------------------------------------------------------------------- /linux/runner/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.13) 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} 10 | "main.cc" 11 | "my_application.cc" 12 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 13 | ) 14 | 15 | # Apply the standard set of build settings. This can be removed for applications 16 | # that need different build settings. 17 | apply_standard_settings(${BINARY_NAME}) 18 | 19 | # Add preprocessor definitions for the application ID. 20 | add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") 21 | 22 | # Add dependency libraries. Add any application-specific dependencies here. 23 | target_link_libraries(${BINARY_NAME} PRIVATE flutter) 24 | target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) 25 | 26 | target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") 27 | -------------------------------------------------------------------------------- /linux/runner/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/runner/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 dynamic_color 9 | import file_picker 10 | import file_selector_macos 11 | import flutter_local_notifications 12 | import media_kit_libs_macos_video 13 | import media_kit_video 14 | import package_info_plus 15 | import path_provider_foundation 16 | import screen_brightness_macos 17 | import screen_retriever_macos 18 | import share_plus 19 | import shared_preferences_foundation 20 | import url_launcher_macos 21 | import wakelock_plus 22 | import webcrypto 23 | import webview_flutter_wkwebview 24 | import window_manager 25 | 26 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 27 | DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) 28 | FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) 29 | FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) 30 | FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) 31 | MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin")) 32 | MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin")) 33 | FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) 34 | PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) 35 | ScreenBrightnessMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenBrightnessMacosPlugin")) 36 | ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin")) 37 | SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) 38 | SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) 39 | UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) 40 | WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) 41 | WebcryptoPlugin.register(with: registry.registrar(forPlugin: "WebcryptoPlugin")) 42 | WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin")) 43 | WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) 44 | } 45 | -------------------------------------------------------------------------------- /macos/Podfile: -------------------------------------------------------------------------------- 1 | platform :osx, '10.14' 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 | 32 | flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) 33 | end 34 | 35 | post_install do |installer| 36 | installer.pods_project.targets.each do |target| 37 | flutter_additional_macos_build_settings(target) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /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 | @main 5 | class AppDelegate: FlutterAppDelegate { 6 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 7 | return true 8 | } 9 | 10 | override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { 11 | return true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /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/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/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 = interstellar 9 | 10 | // The application's bundle identifier 11 | PRODUCT_BUNDLE_IDENTIFIER = one.jwr.interstellar 12 | 13 | // The copyright displayed in application information 14 | PRODUCT_COPYRIGHT = Copyright © 2025 one.jwr. 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.files.user-selected.read-write 10 | 11 | com.apple.security.network.client 12 | 13 | com.apple.security.network.server 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /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 | LSApplicationCategoryType 24 | public.app-category.social-networking 25 | LSMinimumSystemVersion 26 | $(MACOSX_DEPLOYMENT_TARGET) 27 | NSHumanReadableCopyright 28 | $(PRODUCT_COPYRIGHT) 29 | NSMainNibFile 30 | MainMenu 31 | NSPrincipalClass 32 | NSApplication 33 | 34 | 35 | -------------------------------------------------------------------------------- /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.files.user-selected.read-write 8 | 9 | com.apple.security.network.client 10 | 11 | com.apple.security.network.server 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: interstellar 2 | 3 | # Prevent accidental publishing to pub.dev. 4 | publish_to: 'none' 5 | 6 | version: 0.0.0 7 | 8 | environment: 9 | sdk: '>=3.8.0 <4.0.0' 10 | 11 | dependencies: 12 | flutter: 13 | sdk: flutter 14 | flutter_localizations: 15 | sdk: flutter 16 | flutter_markdown: ^0.7.7 17 | http: ^1.4.0 18 | http_parser: ^4.1.2 19 | path: ^1.9.1 20 | mime: ^2.0.0 21 | infinite_scroll_pagination: ^4.1.0 22 | intl: any 23 | media_kit: ^1.2.0 24 | media_kit_video: ^1.3.0 25 | media_kit_libs_video: ^1.0.6 26 | oauth2: ^2.0.3 27 | provider: ^6.1.5 28 | url_launcher: ^6.3.1 29 | webview_flutter: ^4.12.0 30 | youtube_explode_dart: ^2.4.0 31 | freezed_annotation: ^2.4.4 32 | image_picker: ^1.1.2 33 | dynamic_color: ^1.7.0 34 | markdown: ^7.3.0 35 | expandable: ^5.0.1 36 | blurhash_ffi: ^1.2.7 37 | unifiedpush: ^5.0.2 38 | webpush_encryption: 1.0.0-rc1 39 | flutter_local_notifications: ^19.2.1 40 | flex_color_scheme: ^8.2.0 41 | share_plus: ^10.1.4 42 | path_provider: ^2.1.5 43 | window_manager: ^0.4.3 44 | material_symbols_icons: ^4.2815.0 45 | json_annotation: ^4.9.0 46 | sembast: ^3.8.5 47 | package_info_plus: ^8.3.0 48 | flutter_localized_locales: ^2.0.5 49 | crypto: ^3.0.6 50 | file_picker: ^10.1.9 51 | any_link_preview: ^3.0.3 52 | simplytranslate: ^2.2.2 53 | visibility_detector: ^0.4.0+2 54 | 55 | dev_dependencies: 56 | flutter_lints: ^5.0.0 57 | flutter_launcher_icons: ^0.14.3 58 | build_runner: ^2.4.15 59 | freezed: ^2.5.8 60 | json_serializable: ^6.9.5 61 | 62 | flutter: 63 | uses-material-design: true 64 | 65 | # Enable generation of localized Strings from arb files. 66 | generate: true 67 | 68 | assets: 69 | # Add assets from the icons directory to the application. 70 | - assets/icons/ 71 | 72 | flutter_launcher_icons: 73 | image_path: 'assets/icons/logo.png' 74 | android: true 75 | adaptive_icon_foreground: 'assets/icons/logo-foreground.png' 76 | adaptive_icon_background: '#294062' 77 | adaptive_icon_monochrome: 'assets/icons/logo-monochrome.png' 78 | ios: true 79 | macos: 80 | generate: true 81 | windows: 82 | generate: true 83 | -------------------------------------------------------------------------------- /scripts/build-appimage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eux 4 | 5 | export APPIMAGE_EXTRACT_AND_RUN=1 6 | export ARCH="$(uname -m)" 7 | 8 | BUILD_DIR=$(mktemp -d) 9 | 10 | LIB4BN_URL="https://github.com/VHSgunzo/sharun/releases/latest/download/sharun-$ARCH-aio" 11 | URUNTIME_URL="https://github.com/VHSgunzo/uruntime/releases/latest/download/uruntime-appimage-dwarfs-$ARCH" 12 | UPINFO="gh-releases-zsync|$(echo "${GITHUB_REPOSITORY}" | tr '/' '|')|latest|*$ARCH.AppImage.zsync" 13 | 14 | # Prepare AppDir 15 | cp -r build/linux/*/release/bundle/. "$BUILD_DIR"/AppDir 16 | cp linux/appimage/interstellar.desktop "$BUILD_DIR"/AppDir 17 | cp assets/icons/logo.png "$BUILD_DIR"/AppDir/interstellar.png 18 | ln -s interstellar.png "$BUILD_DIR"/AppDir/.DirIcon 19 | 20 | # Add libraries 21 | wget "$LIB4BN_URL" -O "$BUILD_DIR"/lib4bin 22 | chmod +x "$BUILD_DIR"/lib4bin 23 | xvfb-run -a -- "$BUILD_DIR"/lib4bin l -s -k -e -v -p -d "$BUILD_DIR"/AppDir "$BUILD_DIR"/AppDir/interstellar /usr/lib/*/libGL* 24 | ln -s ../data "$BUILD_DIR"/AppDir/bin/data 25 | ln -s ../lib "$BUILD_DIR"/AppDir/bin/lib 26 | ln -s ../../data "$BUILD_DIR"/AppDir/shared/bin/data 27 | ln -s ../../lib "$BUILD_DIR"/AppDir/shared/bin/lib 28 | 29 | # Prepare sharun 30 | ln "$BUILD_DIR"/AppDir/sharun "$BUILD_DIR"/AppDir/AppRun 31 | "$BUILD_DIR"/AppDir/sharun -g 32 | 33 | # Make AppImage 34 | wget "$URUNTIME_URL" -O "$BUILD_DIR"/uruntime 35 | chmod +x "$BUILD_DIR"/uruntime 36 | mkdir -p dist 37 | 38 | "$BUILD_DIR"/uruntime --appimage-addupdinfo "${UPINFO}" 39 | 40 | "$BUILD_DIR"/uruntime --appimage-mkdwarfs -f \ 41 | --set-owner 0 --set-group 0 \ 42 | --no-history --no-create-timestamp \ 43 | --compression zstd:level=22 -S26 -B8 \ 44 | --header "$BUILD_DIR"/uruntime \ 45 | -i "$BUILD_DIR"/AppDir -o dist/interstellar-linux-"$ARCH".AppImage 46 | 47 | zsyncmake dist/*.AppImage -u dist/*.AppImage -o "dist/interstellar-linux-${ARCH}.AppImage.zsync" 48 | 49 | # Cleanup 50 | rm -r "$BUILD_DIR" 51 | echo "All done!" 52 | -------------------------------------------------------------------------------- /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 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | void RegisterPlugins(flutter::PluginRegistry* registry) { 21 | DynamicColorPluginCApiRegisterWithRegistrar( 22 | registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); 23 | FileSelectorWindowsRegisterWithRegistrar( 24 | registry->GetRegistrarForPlugin("FileSelectorWindows")); 25 | MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar( 26 | registry->GetRegistrarForPlugin("MediaKitLibsWindowsVideoPluginCApi")); 27 | MediaKitVideoPluginCApiRegisterWithRegistrar( 28 | registry->GetRegistrarForPlugin("MediaKitVideoPluginCApi")); 29 | ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar( 30 | registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi")); 31 | SharePlusWindowsPluginCApiRegisterWithRegistrar( 32 | registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); 33 | UrlLauncherWindowsRegisterWithRegistrar( 34 | registry->GetRegistrarForPlugin("UrlLauncherWindows")); 35 | VolumeControllerPluginCApiRegisterWithRegistrar( 36 | registry->GetRegistrarForPlugin("VolumeControllerPluginCApi")); 37 | WebcryptoPluginRegisterWithRegistrar( 38 | registry->GetRegistrarForPlugin("WebcryptoPlugin")); 39 | WindowManagerPluginRegisterWithRegistrar( 40 | registry->GetRegistrarForPlugin("WindowManagerPlugin")); 41 | } 42 | -------------------------------------------------------------------------------- /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 | dynamic_color 7 | file_selector_windows 8 | media_kit_libs_windows_video 9 | media_kit_video 10 | screen_retriever_windows 11 | share_plus 12 | url_launcher_windows 13 | volume_controller 14 | webcrypto 15 | window_manager 16 | ) 17 | 18 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 19 | blurhash_ffi 20 | flutter_local_notifications_windows 21 | ) 22 | 23 | set(PLUGIN_BUNDLED_LIBRARIES) 24 | 25 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 26 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) 27 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 28 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 29 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 30 | endforeach(plugin) 31 | 32 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 33 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) 34 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 35 | endforeach(ffi_plugin) 36 | -------------------------------------------------------------------------------- /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/Runner.rc: -------------------------------------------------------------------------------- 1 | // Microsoft Visual C++ generated resource script. 2 | // 3 | #pragma code_page(65001) 4 | #include "resource.h" 5 | 6 | #define APSTUDIO_READONLY_SYMBOLS 7 | ///////////////////////////////////////////////////////////////////////////// 8 | // 9 | // Generated from the TEXTINCLUDE 2 resource. 10 | // 11 | #include "winres.h" 12 | 13 | ///////////////////////////////////////////////////////////////////////////// 14 | #undef APSTUDIO_READONLY_SYMBOLS 15 | 16 | ///////////////////////////////////////////////////////////////////////////// 17 | // English (United States) resources 18 | 19 | #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) 20 | LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US 21 | 22 | #ifdef APSTUDIO_INVOKED 23 | ///////////////////////////////////////////////////////////////////////////// 24 | // 25 | // TEXTINCLUDE 26 | // 27 | 28 | 1 TEXTINCLUDE 29 | BEGIN 30 | "resource.h\0" 31 | END 32 | 33 | 2 TEXTINCLUDE 34 | BEGIN 35 | "#include ""winres.h""\r\n" 36 | "\0" 37 | END 38 | 39 | 3 TEXTINCLUDE 40 | BEGIN 41 | "\r\n" 42 | "\0" 43 | END 44 | 45 | #endif // APSTUDIO_INVOKED 46 | 47 | 48 | ///////////////////////////////////////////////////////////////////////////// 49 | // 50 | // Icon 51 | // 52 | 53 | // Icon with lowest ID value placed first to ensure application icon 54 | // remains consistent on all systems. 55 | IDI_APP_ICON ICON "resources\\app_icon.ico" 56 | 57 | 58 | ///////////////////////////////////////////////////////////////////////////// 59 | // 60 | // Version 61 | // 62 | 63 | #if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) 64 | #define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD 65 | #else 66 | #define VERSION_AS_NUMBER 1,0,0,0 67 | #endif 68 | 69 | #if defined(FLUTTER_VERSION) 70 | #define VERSION_AS_STRING FLUTTER_VERSION 71 | #else 72 | #define VERSION_AS_STRING "1.0.0" 73 | #endif 74 | 75 | VS_VERSION_INFO VERSIONINFO 76 | FILEVERSION VERSION_AS_NUMBER 77 | PRODUCTVERSION VERSION_AS_NUMBER 78 | FILEFLAGSMASK VS_FFI_FILEFLAGSMASK 79 | #ifdef _DEBUG 80 | FILEFLAGS VS_FF_DEBUG 81 | #else 82 | FILEFLAGS 0x0L 83 | #endif 84 | FILEOS VOS__WINDOWS32 85 | FILETYPE VFT_APP 86 | FILESUBTYPE 0x0L 87 | BEGIN 88 | BLOCK "StringFileInfo" 89 | BEGIN 90 | BLOCK "040904e4" 91 | BEGIN 92 | VALUE "CompanyName", "jwr1" "\0" 93 | VALUE "FileDescription", "Interstellar" "\0" 94 | VALUE "FileVersion", VERSION_AS_STRING "\0" 95 | VALUE "InternalName", "Interstellar" "\0" 96 | VALUE "LegalCopyright", "Copyright (C) 2023 jwr1. All rights reserved." "\0" 97 | VALUE "OriginalFilename", "interstellar.exe" "\0" 98 | VALUE "ProductName", "Interstellar" "\0" 99 | VALUE "ProductVersion", VERSION_AS_STRING "\0" 100 | END 101 | END 102 | BLOCK "VarFileInfo" 103 | BEGIN 104 | VALUE "Translation", 0x409, 1252 105 | END 106 | END 107 | 108 | #endif // English (United States) resources 109 | ///////////////////////////////////////////////////////////////////////////// 110 | 111 | 112 | 113 | #ifndef APSTUDIO_INVOKED 114 | ///////////////////////////////////////////////////////////////////////////// 115 | // 116 | // Generated from the TEXTINCLUDE 3 resource. 117 | // 118 | 119 | 120 | ///////////////////////////////////////////////////////////////////////////// 121 | #endif // not APSTUDIO_INVOKED 122 | -------------------------------------------------------------------------------- /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"Interstellar", 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/interstellar-app/interstellar/52e684e02219347473644a77aa7f0f1d5168294c/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 | -------------------------------------------------------------------------------- /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 | unsigned 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 | --------------------------------------------------------------------------------