├── .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 | [](https://play.google.com/store/apps/details?id=one.jwr.interstellar)
10 | [](https://apt.izzysoft.de/fdroid/index/apk/one.jwr.interstellar)
11 | [](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 |
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 |
50 |
51 |
52 |
53 |
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 |
--------------------------------------------------------------------------------