Compare commits

..

No commits in common. "wails-rewrite" and "master" have entirely different histories.

116 changed files with 18082 additions and 3517 deletions

40
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,40 @@
# run this action when the repository is pushed to
on: [push]
# the name of our workflow
name: FanslySync Build & Test
jobs:
# a single job named test
test:
# the display name of the test job
name: FanslySync Test Runner
# we want to run on the latest linux environment
runs-on: ubuntu-latest
# the steps our job runs **in order**
steps:
# checkout the code on the workflow runner
- uses: actions/checkout@v2
# install system dependencies that Tauri needs to compile on Linux.
# note the extra dependencies for `tauri-driver` to run which are: `webkit2gtk-driver` and `xvfb`
- name: Tauri dependencies
run: >-
sudo apt-get update &&
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
# install the latest Rust stable
- name: Rust stable
uses: actions-rs/toolchain@v1
with:
toolchain: stable
# Run our cargo commands in `src-tauri` directory
- name: Build And Test
run: >-
cd src-tauri &&
cargo test &&
cargo build --release

81
.github/workflows/deploy.yml vendored Normal file
View File

@ -0,0 +1,81 @@
name: Release FanslySync
on:
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
CN_APPLICATION: "fansly-creator-bot/fansly-sync"
jobs:
draft:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: create draft release
uses: crabnebula-dev/cloud-release@v0
with:
command: release draft ${{ env.CN_APPLICATION }} --framework tauri
api-key: ${{ secrets.CN_API_KEY }}
build_desktop:
needs: draft
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-22.04
- os: windows
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "lts/*"
- name: Install stable toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
cache: true
- name: install Linux dependencies
if: matrix.os == 'ubuntu-22.04'
run: |
sudo apt-get update
sudo apt-get install -y webkit2gtk-4.1
- name: build Tauri app for Windows, Linux
if: matrix.os != 'macos-latest'
run: |
npm ci
npm exec tauri build
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
- name: upload assets
uses: crabnebula-dev/cloud-release@v0
with:
command: release upload ${{ env.CN_APPLICATION }} --framework tauri
api-key: ${{ secrets.CN_API_KEY }}
publish:
needs: [build_desktop]
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: publish release
uses: crabnebula-dev/cloud-release@v0
with:
command: release publish ${{ env.CN_APPLICATION }} --framework tauri
api-key: ${{ secrets.CN_API_KEY }}

25
.gitignore vendored
View File

@ -1,3 +1,24 @@
build/bin
node_modules
frontend/dist
# Output
.output
.vercel
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
# Pacakge lock
package-lock.json

8
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

12
.idea/FanslySync.iml generated Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src-tauri/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/src-tauri/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

63
.idea/codeStyles/Project.xml generated Normal file
View File

@ -0,0 +1,63 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<HTMLCodeStyleSettings>
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
</HTMLCodeStyleSettings>
<JSCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="USE_DOUBLE_QUOTES" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</JSCodeStyleSettings>
<TypeScriptCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="USE_DOUBLE_QUOTES" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</TypeScriptCodeStyleSettings>
<VueCodeStyleSettings>
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
</VueCodeStyleSettings>
<codeStyleSettings language="HTML">
<option name="SOFT_MARGINS" value="100" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JavaScript">
<option name="SOFT_MARGINS" value="100" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="TypeScript">
<option name="SOFT_MARGINS" value="100" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="Vue">
<option name="SOFT_MARGINS" value="100" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

5
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,5 @@
<project version="4">
<component name="ProjectRootManager">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/FanslySync.iml" filepath="$PROJECT_DIR$/.idea/FanslySync.iml" />
</modules>
</component>
</project>

6
.idea/prettier.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PrettierConfiguration">
<option name="myConfigurationMode" value="AUTOMATIC" />
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

1
.npmrc Normal file
View File

@ -0,0 +1 @@
engine-strict=true

4
.prettierignore Normal file
View File

@ -0,0 +1,4 @@
# Package Managers
package-lock.json
pnpm-lock.yaml
yarn.lock

8
.prettierrc Normal file
View File

@ -0,0 +1,8 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

56
DEVELOPERS.md Normal file
View File

@ -0,0 +1,56 @@
# FanslySync Developer Documentation
Welcome. We'll tell you how to integrate with FanslySync.
## Our JSON Schema
FanslySync uses a JSON schema to sync data with 3rd party services. Here's an example of the JSON schema:
```json
{
"followers": [{ "followerId": "123456" }],
"subscribers": [
// An array of subscriber objects. See below for the schema.
]
}
```
### Subscriber Schema
```json
{
{
"id": "0", // The ID of the subscription, usually unique
"historyId": "<history_id>", // The ID of the subscription history
"subscriberId": "<user_id>", // The User ID of the subscriber
"subscriptionTierId": "<tier_id>", // The ID of the subscription tier
"subscriptionTierName": "<tier_name>", // The name of the subscription tier
"subscriptionTierColor": "#2699f7", // The color of the subscription tier
"planId": "0", // The ID of the subscription plan
"promoId": "0", // The ID of the promotion, if applicable
"giftCodeId": null, // The ID of the gift code, if applicable
"paymentMethodId": "0", // The ID of the payment method
"status": 3, // The status of the subscription. 3 = active, 4 = ?
"price": 7000, // The price of the subscription, in cents
"renewPrice": 7000, // The price of the subscription renewal, in cents
"renewCorrelationId": "673162822363914240", // The correlation ID of the renewal
"autoRenew": 1, // Whether the subscription is set to auto-renew
"billingCycle": 30, // The billing cycle of the subscription, in days
"duration": 30, // The duration of the subscription, in days
"renewDate": 1721988883000, // The date the subscription will renew (UNIX timestamp)
"version": 3, // The version of the subscription schema from fansly
"createdAt": 1721988883000, // The date the subscription was created (UNIX timestamp)
"updatedAt": 1721988883000, // The date the subscription was last updated (UNIX timestamp)
"endsAt": 1724667283000, // The date the subscription will end (UNIX timestamp)
"promoPrice": null, // The price of the subscription with the promotion, in cents
"promoDuration": null, // The duration of the subscription with the promotion, in days
"promoStatus": null, // The status of the promotion
"promoStartsAt": null, // The date the promotion starts (UNIX timestamp)
"promoEndsAt": null // The date the promotion ends (UNIX timestamp)
},
}
```
# Closing
That's it! If you have any questions, feel free to reach out to us at our [support email](mailto:tanner@fanslycreatorbot.com) if you have any questions. We're happy to help you integrate with FanslySync.

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Sticks
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,19 +1,33 @@
# README
<div align="center">
<h1>FanslySync</h1>
A simple tool to sync your fansly data with 3rd party services, securely.
</div>
## About
This is the official Wails React-TS template.
FanslySync was created for the [Fansly Creator Bot](https://fanslycreatorbot.com) project to securely sync Fansly data with our service and others. We wanted to create a tool that would allow creators to sync their data with our service without having to give us their Fansly credentials. This tool is open source and can be used by anyone to sync their Fansly data with any service they want -- not just ours.
You can configure the project by editing `wails.json`. More information about the project settings can be found
here: https://wails.io/docs/reference/project-config
## How it works
## Live Development
We use your Fansly API key to sync your data with our service. This way, you don't have to give us your Fansly credentials. While it does provide the same access to your data as your credentials would, it's a more secure way to sync your data. **Your API key is never stored outside of your own computer.**
To run in live development mode, run `wails dev` in the project directory. This will run a Vite development
server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser
and have access to your Go methods, there is also a dev server that runs on http://localhost:34115. Connect
to this in your browser, and you can call your Go code from devtools.
## Getting Started & Installation
## Building
We have a documentation page that explains how to get started with FanslySync. You can find it [here](https://docs.fanslycreatorbot.com/docs/for-creators/sync).
To build a redistributable, production mode package, use `wails build`.
If you aren't using Fansly Creator Bot, you can ignore the parts about setting up the bot and just follow the FanslySync installation instructions.
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## Contributing
Contributions are welcome! Please read our [CONTRIBUTING.md](CONTRIBUTING.md) file for more information.
## For Developers
If you are a 3rd party service that wants to integrate with FanslySync, please read our [DEVELOPERS.md](DEVELOPERS.md) file for more information.

122
app.go
View File

@ -1,122 +0,0 @@
package main
import (
"FanslySync/handlers"
"FanslySync/structs"
"FanslySync/utils"
"context"
"github.com/wailsapp/wails/v2/pkg/logger"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
// App struct
type App struct {
ctx context.Context
ConfigManager handlers.ConfigManager
AppConfig *structs.Config
}
// NewApp creates a new App application struct
func NewApp() *App {
// Return an empty app
return &App{}
}
// startup is called when the app starts. The context is saved
// so we can call the runtime methods
func (a *App) startup(ctx context.Context, logger logger.Logger) {
a.ctx = ctx
// Setup config manager
configPath, cfgErr := handlers.GetConfigPathForRuntime()
if cfgErr != nil {
// Show message box and quit
utils.ShowMessageBox(a.ctx, "FanslySync | Initialization Error", "Could not get config path.\n\nError: "+cfgErr.Error(), utils.WithDialogType(runtime.ErrorDialog))
runtime.Quit(a.ctx)
return
}
// Create our config manager
configMgr, configMgrCreateErr := handlers.NewFileConfigManager(configPath)
if configMgrCreateErr != nil {
// Show message box and quit
utils.ShowMessageBox(a.ctx, "FanslySync | Initialization Error", "Could not create config manager.\n\nError: "+configMgrCreateErr.Error(), utils.WithDialogType(runtime.ErrorDialog))
runtime.Quit(a.ctx)
return
}
// Set the config manager
a.ConfigManager = configMgr
logger.Info("initializing FanslySync...")
// Check our config path to see if it was set correctly. Will not contain FailedConfigPathFetch
// Do we have an old config file?
shouldMigrate, err := a.ConfigManager.ShouldMigrateOldAppConfig()
if err != nil {
// Show the error in a message box
utils.ShowMessageBox(a.ctx, "FanslySync | Initialization Error", "Could not check for old config file.\n\nError: "+err.Error(), utils.WithDialogType(runtime.ErrorDialog))
runtime.Quit(a.ctx)
return
}
if shouldMigrate {
logger.Info("migrating old config file...")
// Migrate the old config file
err := a.ConfigManager.MigrateOldAppConfig()
if err != nil {
// Show the error in a message box
utils.ShowMessageBox(a.ctx, "FanslySync | Initialization Error", "Could not migrate old config file.\n\nError: "+err.Error(), utils.WithDialogType(runtime.ErrorDialog))
runtime.Quit(a.ctx)
return
} else {
// Show success message
logger.Info("old config file migrate ok")
utils.ShowMessageBox(a.ctx, "FanslySync | Notice", "We've detected an old config file (app version < 2.x and below).\n\nThe old config file has been migrated to the new format for you automatically, and the old config file has been deleted.\n\nPlease check your settings to ensure everything is correct.", utils.WithDialogType(runtime.InfoDialog))
// Now grab the new config
cfg, err := a.ConfigManager.GetConfig(false)
if err != nil {
// Show the error in a message box
utils.ShowMessageBox(a.ctx, "FanslySync | Initialization Error", "Could not load config file.\n\nError: "+err.Error(), utils.WithDialogType(runtime.ErrorDialog))
runtime.Quit(a.ctx)
}
// Set the config
a.AppConfig = cfg
}
} else {
// Load config as normal
logger.Info("loading config file...")
cfg, err := a.ConfigManager.LoadConfigOrCreate()
if err != nil {
// Show the error in a message box
utils.ShowMessageBox(a.ctx, "FanslySync | Initialization Error", "Could not load config file.\n\nError: "+err.Error(), utils.WithDialogType(runtime.ErrorDialog))
runtime.Quit(a.ctx)
}
// Set the config
logger.Info("config file loaded ok")
a.AppConfig = cfg
}
logger.Info("FanslySync initialized successfully")
}
// Greet returns a greeting for the given name
func (a *App) Greet(token string) string {
// Create fansly API instance
fanslyAPI, createErr := handlers.NewFanslyAPIController(token)
if createErr != nil {
return "Failed to create Fansly API instance: " + createErr.Error()
}
// Sync
fanslyAPI.Sync(a.ctx, token, false)
return "Sync dispatched, check the logs for more info."
}

View File

@ -1,35 +0,0 @@
# Build Directory
The build directory is used to house all the build files and assets for your application.
The structure is:
* bin - Output directory
* darwin - macOS specific files
* windows - Windows specific files
## Mac
The `darwin` directory holds files specific to Mac builds.
These may be customised and used as part of the build. To return these files to the default state, simply delete them
and
build with `wails build`.
The directory contains the following files:
- `Info.plist` - the main plist file used for Mac builds. It is used when building using `wails build`.
- `Info.dev.plist` - same as the main plist file but used when building using `wails dev`.
## Windows
The `windows` directory contains the manifest and rc files used when building with `wails build`.
These may be customised for your application. To return these files to the default state, simply delete them and
build with `wails build`.
- `icon.ico` - The icon used for the application. This is used when building using `wails build`. If you wish to
use a different icon, simply replace this file with your own. If it is missing, a new `icon.ico` file
will be created using the `appicon.png` file in the build directory.
- `installer/*` - The files used to create the Windows installer. These are used when building using `wails build`.
- `info.json` - Application details used for Windows builds. The data here will be used by the Windows installer,
as well as the application itself (right click the exe -> properties -> details)
- `wails.exe.manifest` - The main application manifest file.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

View File

@ -1,68 +0,0 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleName</key>
<string>{{.Info.ProductName}}</string>
<key>CFBundleExecutable</key>
<string>{{.OutputFilename}}</string>
<key>CFBundleIdentifier</key>
<string>com.wails.{{.Name}}</string>
<key>CFBundleVersion</key>
<string>{{.Info.ProductVersion}}</string>
<key>CFBundleGetInfoString</key>
<string>{{.Info.Comments}}</string>
<key>CFBundleShortVersionString</key>
<string>{{.Info.ProductVersion}}</string>
<key>CFBundleIconFile</key>
<string>iconfile</string>
<key>LSMinimumSystemVersion</key>
<string>10.13.0</string>
<key>NSHighResolutionCapable</key>
<string>true</string>
<key>NSHumanReadableCopyright</key>
<string>{{.Info.Copyright}}</string>
{{if .Info.FileAssociations}}
<key>CFBundleDocumentTypes</key>
<array>
{{range .Info.FileAssociations}}
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>{{.Ext}}</string>
</array>
<key>CFBundleTypeName</key>
<string>{{.Name}}</string>
<key>CFBundleTypeRole</key>
<string>{{.Role}}</string>
<key>CFBundleTypeIconFile</key>
<string>{{.IconName}}</string>
</dict>
{{end}}
</array>
{{end}}
{{if .Info.Protocols}}
<key>CFBundleURLTypes</key>
<array>
{{range .Info.Protocols}}
<dict>
<key>CFBundleURLName</key>
<string>com.wails.{{.Scheme}}</string>
<key>CFBundleURLSchemes</key>
<array>
<string>{{.Scheme}}</string>
</array>
<key>CFBundleTypeRole</key>
<string>{{.Role}}</string>
</dict>
{{end}}
</array>
{{end}}
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
</dict>
</plist>

View File

@ -1,63 +0,0 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleName</key>
<string>{{.Info.ProductName}}</string>
<key>CFBundleExecutable</key>
<string>{{.OutputFilename}}</string>
<key>CFBundleIdentifier</key>
<string>com.wails.{{.Name}}</string>
<key>CFBundleVersion</key>
<string>{{.Info.ProductVersion}}</string>
<key>CFBundleGetInfoString</key>
<string>{{.Info.Comments}}</string>
<key>CFBundleShortVersionString</key>
<string>{{.Info.ProductVersion}}</string>
<key>CFBundleIconFile</key>
<string>iconfile</string>
<key>LSMinimumSystemVersion</key>
<string>10.13.0</string>
<key>NSHighResolutionCapable</key>
<string>true</string>
<key>NSHumanReadableCopyright</key>
<string>{{.Info.Copyright}}</string>
{{if .Info.FileAssociations}}
<key>CFBundleDocumentTypes</key>
<array>
{{range .Info.FileAssociations}}
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>{{.Ext}}</string>
</array>
<key>CFBundleTypeName</key>
<string>{{.Name}}</string>
<key>CFBundleTypeRole</key>
<string>{{.Role}}</string>
<key>CFBundleTypeIconFile</key>
<string>{{.IconName}}</string>
</dict>
{{end}}
</array>
{{end}}
{{if .Info.Protocols}}
<key>CFBundleURLTypes</key>
<array>
{{range .Info.Protocols}}
<dict>
<key>CFBundleURLName</key>
<string>com.wails.{{.Scheme}}</string>
<key>CFBundleURLSchemes</key>
<array>
<string>{{.Scheme}}</string>
</array>
<key>CFBundleTypeRole</key>
<string>{{.Role}}</string>
</dict>
{{end}}
</array>
{{end}}
</dict>
</plist>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

View File

@ -1,15 +0,0 @@
{
"fixed": {
"file_version": "{{.Info.ProductVersion}}"
},
"info": {
"0000": {
"ProductVersion": "{{.Info.ProductVersion}}",
"CompanyName": "{{.Info.CompanyName}}",
"FileDescription": "{{.Info.ProductName}}",
"LegalCopyright": "{{.Info.Copyright}}",
"ProductName": "{{.Info.ProductName}}",
"Comments": "{{.Info.Comments}}"
}
}
}

View File

@ -1,114 +0,0 @@
Unicode true
####
## Please note: Template replacements don't work in this file. They are provided with default defines like
## mentioned underneath.
## If the keyword is not defined, "wails_tools.nsh" will populate them with the values from ProjectInfo.
## If they are defined here, "wails_tools.nsh" will not touch them. This allows to use this project.nsi manually
## from outside of Wails for debugging and development of the installer.
##
## For development first make a wails nsis build to populate the "wails_tools.nsh":
## > wails build --target windows/amd64 --nsis
## Then you can call makensis on this file with specifying the path to your binary:
## For a AMD64 only installer:
## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe
## For a ARM64 only installer:
## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe
## For a installer with both architectures:
## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe
####
## The following information is taken from the ProjectInfo file, but they can be overwritten here.
####
## !define INFO_PROJECTNAME "MyProject" # Default "{{.Name}}"
## !define INFO_COMPANYNAME "MyCompany" # Default "{{.Info.CompanyName}}"
## !define INFO_PRODUCTNAME "MyProduct" # Default "{{.Info.ProductName}}"
## !define INFO_PRODUCTVERSION "1.0.0" # Default "{{.Info.ProductVersion}}"
## !define INFO_COPYRIGHT "Copyright" # Default "{{.Info.Copyright}}"
###
## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe"
## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
####
## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html
####
## Include the wails tools
####
!include "wails_tools.nsh"
# The version information for this two must consist of 4 parts
VIProductVersion "${INFO_PRODUCTVERSION}.0"
VIFileVersion "${INFO_PRODUCTVERSION}.0"
VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}"
VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer"
VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}"
VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}"
VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}"
VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}"
# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware
ManifestDPIAware true
!include "MUI.nsh"
!define MUI_ICON "..\icon.ico"
!define MUI_UNICON "..\icon.ico"
# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314
!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps
!define MUI_ABORTWARNING # This will warn the user if they exit from the installer.
!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page.
# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer
!insertmacro MUI_PAGE_DIRECTORY # In which folder install page.
!insertmacro MUI_PAGE_INSTFILES # Installing page.
!insertmacro MUI_PAGE_FINISH # Finished installation page.
!insertmacro MUI_UNPAGE_INSTFILES # Uinstalling page
!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer
## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1
#!uninstfinalize 'signtool --file "%1"'
#!finalize 'signtool --file "%1"'
Name "${INFO_PRODUCTNAME}"
OutFile "..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file.
InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder).
ShowInstDetails show # This will always show the installation details.
Function .onInit
!insertmacro wails.checkArchitecture
FunctionEnd
Section
!insertmacro wails.setShellContext
!insertmacro wails.webview2runtime
SetOutPath $INSTDIR
!insertmacro wails.files
CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
!insertmacro wails.associateFiles
!insertmacro wails.associateCustomProtocols
!insertmacro wails.writeUninstaller
SectionEnd
Section "uninstall"
!insertmacro wails.setShellContext
RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath
RMDir /r $INSTDIR
Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk"
Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk"
!insertmacro wails.unassociateFiles
!insertmacro wails.unassociateCustomProtocols
!insertmacro wails.deleteUninstaller
SectionEnd

View File

@ -1,249 +0,0 @@
# DO NOT EDIT - Generated automatically by `wails build`
!include "x64.nsh"
!include "WinVer.nsh"
!include "FileFunc.nsh"
!ifndef INFO_PROJECTNAME
!define INFO_PROJECTNAME "{{.Name}}"
!endif
!ifndef INFO_COMPANYNAME
!define INFO_COMPANYNAME "{{.Info.CompanyName}}"
!endif
!ifndef INFO_PRODUCTNAME
!define INFO_PRODUCTNAME "{{.Info.ProductName}}"
!endif
!ifndef INFO_PRODUCTVERSION
!define INFO_PRODUCTVERSION "{{.Info.ProductVersion}}"
!endif
!ifndef INFO_COPYRIGHT
!define INFO_COPYRIGHT "{{.Info.Copyright}}"
!endif
!ifndef PRODUCT_EXECUTABLE
!define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
!endif
!ifndef UNINST_KEY_NAME
!define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
!endif
!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}"
!ifndef REQUEST_EXECUTION_LEVEL
!define REQUEST_EXECUTION_LEVEL "admin"
!endif
RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
!ifdef ARG_WAILS_AMD64_BINARY
!define SUPPORTS_AMD64
!endif
!ifdef ARG_WAILS_ARM64_BINARY
!define SUPPORTS_ARM64
!endif
!ifdef SUPPORTS_AMD64
!ifdef SUPPORTS_ARM64
!define ARCH "amd64_arm64"
!else
!define ARCH "amd64"
!endif
!else
!ifdef SUPPORTS_ARM64
!define ARCH "arm64"
!else
!error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY"
!endif
!endif
!macro wails.checkArchitecture
!ifndef WAILS_WIN10_REQUIRED
!define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later."
!endif
!ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED
!define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}"
!endif
${If} ${AtLeastWin10}
!ifdef SUPPORTS_AMD64
${if} ${IsNativeAMD64}
Goto ok
${EndIf}
!endif
!ifdef SUPPORTS_ARM64
${if} ${IsNativeARM64}
Goto ok
${EndIf}
!endif
IfSilent silentArch notSilentArch
silentArch:
SetErrorLevel 65
Abort
notSilentArch:
MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}"
Quit
${else}
IfSilent silentWin notSilentWin
silentWin:
SetErrorLevel 64
Abort
notSilentWin:
MessageBox MB_OK "${WAILS_WIN10_REQUIRED}"
Quit
${EndIf}
ok:
!macroend
!macro wails.files
!ifdef SUPPORTS_AMD64
${if} ${IsNativeAMD64}
File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}"
${EndIf}
!endif
!ifdef SUPPORTS_ARM64
${if} ${IsNativeARM64}
File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}"
${EndIf}
!endif
!macroend
!macro wails.writeUninstaller
WriteUninstaller "$INSTDIR\uninstall.exe"
SetRegView 64
WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}"
WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}"
WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}"
WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}"
WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
IntFmt $0 "0x%08X" $0
WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0"
!macroend
!macro wails.deleteUninstaller
Delete "$INSTDIR\uninstall.exe"
SetRegView 64
DeleteRegKey HKLM "${UNINST_KEY}"
!macroend
!macro wails.setShellContext
${If} ${REQUEST_EXECUTION_LEVEL} == "admin"
SetShellVarContext all
${else}
SetShellVarContext current
${EndIf}
!macroend
# Install webview2 by launching the bootstrapper
# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment
!macro wails.webview2runtime
!ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT
!define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime"
!endif
SetRegView 64
# If the admin key exists and is not empty then webview2 is already installed
ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
${If} $0 != ""
Goto ok
${EndIf}
${If} ${REQUEST_EXECUTION_LEVEL} == "user"
# If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed
ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
${If} $0 != ""
Goto ok
${EndIf}
${EndIf}
SetDetailsPrint both
DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}"
SetDetailsPrint listonly
InitPluginsDir
CreateDirectory "$pluginsdir\webview2bootstrapper"
SetOutPath "$pluginsdir\webview2bootstrapper"
File "tmp\MicrosoftEdgeWebview2Setup.exe"
ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install'
SetDetailsPrint both
ok:
!macroend
# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b
!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND
; Backup the previously associated file class
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" ""
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0"
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}"
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}`
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}`
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open"
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}`
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}`
!macroend
!macro APP_UNASSOCIATE EXT FILECLASS
; Backup the previously associated file class
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup`
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0"
DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}`
!macroend
!macro wails.associateFiles
; Create file associations
{{range .Info.FileAssociations}}
!insertmacro APP_ASSOCIATE "{{.Ext}}" "{{.Name}}" "{{.Description}}" "$INSTDIR\{{.IconName}}.ico" "Open with ${INFO_PRODUCTNAME}" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
File "..\{{.IconName}}.ico"
{{end}}
!macroend
!macro wails.unassociateFiles
; Delete app associations
{{range .Info.FileAssociations}}
!insertmacro APP_UNASSOCIATE "{{.Ext}}" "{{.Name}}"
Delete "$INSTDIR\{{.IconName}}.ico"
{{end}}
!macroend
!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND
DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}"
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" ""
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}"
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" ""
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" ""
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}"
!macroend
!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL
DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
!macroend
!macro wails.associateCustomProtocols
; Create custom protocols associations
{{range .Info.Protocols}}
!insertmacro CUSTOM_PROTOCOL_ASSOCIATE "{{.Scheme}}" "{{.Description}}" "$INSTDIR\${PRODUCT_EXECUTABLE},0" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
{{end}}
!macroend
!macro wails.unassociateCustomProtocols
; Delete app custom protocol associations
{{range .Info.Protocols}}
!insertmacro CUSTOM_PROTOCOL_UNASSOCIATE "{{.Scheme}}"
{{end}}
!macroend

View File

@ -1,15 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<assemblyIdentity type="win32" name="com.wails.{{.Name}}" version="{{.Info.ProductVersion}}.0" processorArchitecture="*"/>
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
</dependentAssembly>
</dependency>
<asmv3:application>
<asmv3:windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware> <!-- fallback for Windows 7 and 8 -->
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness> <!-- falls back to per-monitor if per-monitor v2 is not supported -->
</asmv3:windowsSettings>
</asmv3:application>
</assembly>

33
eslint.config.js Normal file
View File

@ -0,0 +1,33 @@
import js from '@eslint/js';
import ts from 'typescript-eslint';
import svelte from 'eslint-plugin-svelte';
import prettier from 'eslint-config-prettier';
import globals from 'globals';
/** @type {import('eslint').Linter.FlatConfig[]} */
export default [
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs['flat/recommended'],
prettier,
...svelte.configs['flat/prettier'],
{
languageOptions: {
globals: {
...globals.browser,
...globals.node
}
}
},
{
files: ['**/*.svelte'],
languageOptions: {
parserOptions: {
parser: ts.parser
}
}
},
{
ignores: ['build/', '.svelte-kit/', 'dist/']
}
];

View File

@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>FanslySync</title>
</head>
<body>
<div id="root"></div>
<script src="./src/main.tsx" type="module"></script>
</body>
</html>

View File

@ -1,22 +0,0 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.0.17",
"@types/react-dom": "^18.0.6",
"@vitejs/plugin-react": "^2.0.1",
"typescript": "^4.6.4",
"vite": "^3.0.7"
}
}

View File

@ -1 +0,0 @@
f26173c7304a0bf8ea5c86eb567e7db2

892
frontend/pnpm-lock.yaml generated
View File

@ -1,892 +0,0 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
react:
specifier: ^18.2.0
version: 18.3.1
react-dom:
specifier: ^18.2.0
version: 18.3.1(react@18.3.1)
devDependencies:
'@types/react':
specifier: ^18.0.17
version: 18.3.21
'@types/react-dom':
specifier: ^18.0.6
version: 18.3.7(@types/react@18.3.21)
'@vitejs/plugin-react':
specifier: ^2.0.1
version: 2.2.0(vite@3.2.11)
typescript:
specifier: ^4.6.4
version: 4.9.5
vite:
specifier: ^3.0.7
version: 3.2.11
packages:
'@ampproject/remapping@2.3.0':
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
engines: {node: '>=6.0.0'}
'@babel/code-frame@7.27.1':
resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
engines: {node: '>=6.9.0'}
'@babel/compat-data@7.27.2':
resolution: {integrity: sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==}
engines: {node: '>=6.9.0'}
'@babel/core@7.27.1':
resolution: {integrity: sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==}
engines: {node: '>=6.9.0'}
'@babel/generator@7.27.1':
resolution: {integrity: sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==}
engines: {node: '>=6.9.0'}
'@babel/helper-annotate-as-pure@7.27.1':
resolution: {integrity: sha512-WnuuDILl9oOBbKnb4L+DyODx7iC47XfzmNCpTttFsSp6hTG7XZxu60+4IO+2/hPfcGOoKbFiwoI/+zwARbNQow==}
engines: {node: '>=6.9.0'}
'@babel/helper-compilation-targets@7.27.2':
resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==}
engines: {node: '>=6.9.0'}
'@babel/helper-module-imports@7.27.1':
resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==}
engines: {node: '>=6.9.0'}
'@babel/helper-module-transforms@7.27.1':
resolution: {integrity: sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
'@babel/helper-plugin-utils@7.27.1':
resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==}
engines: {node: '>=6.9.0'}
'@babel/helper-string-parser@7.27.1':
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
engines: {node: '>=6.9.0'}
'@babel/helper-validator-identifier@7.27.1':
resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==}
engines: {node: '>=6.9.0'}
'@babel/helper-validator-option@7.27.1':
resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
engines: {node: '>=6.9.0'}
'@babel/helpers@7.27.1':
resolution: {integrity: sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==}
engines: {node: '>=6.9.0'}
'@babel/parser@7.27.2':
resolution: {integrity: sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==}
engines: {node: '>=6.0.0'}
hasBin: true
'@babel/plugin-syntax-jsx@7.27.1':
resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
'@babel/plugin-transform-react-jsx-development@7.27.1':
resolution: {integrity: sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
'@babel/plugin-transform-react-jsx-self@7.27.1':
resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
'@babel/plugin-transform-react-jsx-source@7.27.1':
resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
'@babel/plugin-transform-react-jsx@7.27.1':
resolution: {integrity: sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
'@babel/template@7.27.2':
resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
engines: {node: '>=6.9.0'}
'@babel/traverse@7.27.1':
resolution: {integrity: sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==}
engines: {node: '>=6.9.0'}
'@babel/types@7.27.1':
resolution: {integrity: sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==}
engines: {node: '>=6.9.0'}
'@esbuild/android-arm@0.15.18':
resolution: {integrity: sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==}
engines: {node: '>=12'}
cpu: [arm]
os: [android]
'@esbuild/linux-loong64@0.15.18':
resolution: {integrity: sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==}
engines: {node: '>=12'}
cpu: [loong64]
os: [linux]
'@jridgewell/gen-mapping@0.3.8':
resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==}
engines: {node: '>=6.0.0'}
'@jridgewell/resolve-uri@3.1.2':
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'}
'@jridgewell/set-array@1.2.1':
resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==}
engines: {node: '>=6.0.0'}
'@jridgewell/sourcemap-codec@1.5.0':
resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
'@jridgewell/trace-mapping@0.3.25':
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
'@types/prop-types@15.7.14':
resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==}
'@types/react-dom@18.3.7':
resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==}
peerDependencies:
'@types/react': ^18.0.0
'@types/react@18.3.21':
resolution: {integrity: sha512-gXLBtmlcRJeT09/sI4PxVwyrku6SaNUj/6cMubjE6T6XdY1fDmBL7r0nX0jbSZPU/Xr0KuwLLZh6aOYY5d91Xw==}
'@vitejs/plugin-react@2.2.0':
resolution: {integrity: sha512-FFpefhvExd1toVRlokZgxgy2JtnBOdp4ZDsq7ldCWaqGSGn9UhWMAVm/1lxPL14JfNS5yGz+s9yFrQY6shoStA==}
engines: {node: ^14.18.0 || >=16.0.0}
peerDependencies:
vite: ^3.0.0
browserslist@4.24.5:
resolution: {integrity: sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
caniuse-lite@1.0.30001718:
resolution: {integrity: sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==}
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
debug@4.4.1:
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
electron-to-chromium@1.5.155:
resolution: {integrity: sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==}
esbuild-android-64@0.15.18:
resolution: {integrity: sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==}
engines: {node: '>=12'}
cpu: [x64]
os: [android]
esbuild-android-arm64@0.15.18:
resolution: {integrity: sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==}
engines: {node: '>=12'}
cpu: [arm64]
os: [android]
esbuild-darwin-64@0.15.18:
resolution: {integrity: sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==}
engines: {node: '>=12'}
cpu: [x64]
os: [darwin]
esbuild-darwin-arm64@0.15.18:
resolution: {integrity: sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==}
engines: {node: '>=12'}
cpu: [arm64]
os: [darwin]
esbuild-freebsd-64@0.15.18:
resolution: {integrity: sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==}
engines: {node: '>=12'}
cpu: [x64]
os: [freebsd]
esbuild-freebsd-arm64@0.15.18:
resolution: {integrity: sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==}
engines: {node: '>=12'}
cpu: [arm64]
os: [freebsd]
esbuild-linux-32@0.15.18:
resolution: {integrity: sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==}
engines: {node: '>=12'}
cpu: [ia32]
os: [linux]
esbuild-linux-64@0.15.18:
resolution: {integrity: sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==}
engines: {node: '>=12'}
cpu: [x64]
os: [linux]
esbuild-linux-arm64@0.15.18:
resolution: {integrity: sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==}
engines: {node: '>=12'}
cpu: [arm64]
os: [linux]
esbuild-linux-arm@0.15.18:
resolution: {integrity: sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==}
engines: {node: '>=12'}
cpu: [arm]
os: [linux]
esbuild-linux-mips64le@0.15.18:
resolution: {integrity: sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==}
engines: {node: '>=12'}
cpu: [mips64el]
os: [linux]
esbuild-linux-ppc64le@0.15.18:
resolution: {integrity: sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==}
engines: {node: '>=12'}
cpu: [ppc64]
os: [linux]
esbuild-linux-riscv64@0.15.18:
resolution: {integrity: sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==}
engines: {node: '>=12'}
cpu: [riscv64]
os: [linux]
esbuild-linux-s390x@0.15.18:
resolution: {integrity: sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==}
engines: {node: '>=12'}
cpu: [s390x]
os: [linux]
esbuild-netbsd-64@0.15.18:
resolution: {integrity: sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==}
engines: {node: '>=12'}
cpu: [x64]
os: [netbsd]
esbuild-openbsd-64@0.15.18:
resolution: {integrity: sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==}
engines: {node: '>=12'}
cpu: [x64]
os: [openbsd]
esbuild-sunos-64@0.15.18:
resolution: {integrity: sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==}
engines: {node: '>=12'}
cpu: [x64]
os: [sunos]
esbuild-windows-32@0.15.18:
resolution: {integrity: sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==}
engines: {node: '>=12'}
cpu: [ia32]
os: [win32]
esbuild-windows-64@0.15.18:
resolution: {integrity: sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==}
engines: {node: '>=12'}
cpu: [x64]
os: [win32]
esbuild-windows-arm64@0.15.18:
resolution: {integrity: sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==}
engines: {node: '>=12'}
cpu: [arm64]
os: [win32]
esbuild@0.15.18:
resolution: {integrity: sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==}
engines: {node: '>=12'}
hasBin: true
escalade@3.2.0:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'}
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
gensync@1.0.0-beta.2:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'}
globals@11.12.0:
resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==}
engines: {node: '>=4'}
hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
is-core-module@2.16.1:
resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==}
engines: {node: '>= 0.4'}
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
jsesc@3.1.0:
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
engines: {node: '>=6'}
hasBin: true
json5@2.2.3:
resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
engines: {node: '>=6'}
hasBin: true
loose-envify@1.4.0:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
magic-string@0.26.7:
resolution: {integrity: sha512-hX9XH3ziStPoPhJxLq1syWuZMxbDvGNbVchfrdCtanC7D13888bMFow61x8axrx+GfHLtVeAx2kxL7tTGRl+Ow==}
engines: {node: '>=12'}
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
nanoid@3.3.11:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
node-releases@2.0.19:
resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
postcss@8.5.3:
resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==}
engines: {node: ^10 || ^12 || >=14}
react-dom@18.3.1:
resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
peerDependencies:
react: ^18.3.1
react-refresh@0.14.2:
resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
engines: {node: '>=0.10.0'}
react@18.3.1:
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
engines: {node: '>=0.10.0'}
resolve@1.22.10:
resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==}
engines: {node: '>= 0.4'}
hasBin: true
rollup@2.79.2:
resolution: {integrity: sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==}
engines: {node: '>=10.0.0'}
hasBin: true
scheduler@0.23.2:
resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
semver@6.3.1:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
hasBin: true
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
sourcemap-codec@1.4.8:
resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==}
deprecated: Please use @jridgewell/sourcemap-codec instead
supports-preserve-symlinks-flag@1.0.0:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
typescript@4.9.5:
resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==}
engines: {node: '>=4.2.0'}
hasBin: true
update-browserslist-db@1.1.3:
resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==}
hasBin: true
peerDependencies:
browserslist: '>= 4.21.0'
vite@3.2.11:
resolution: {integrity: sha512-K/jGKL/PgbIgKCiJo5QbASQhFiV02X9Jh+Qq0AKCRCRKZtOTVi4t6wh75FDpGf2N9rYOnzH87OEFQNaFy6pdxQ==}
engines: {node: ^14.18.0 || >=16.0.0}
hasBin: true
peerDependencies:
'@types/node': '>= 14'
less: '*'
sass: '*'
stylus: '*'
sugarss: '*'
terser: ^5.4.0
peerDependenciesMeta:
'@types/node':
optional: true
less:
optional: true
sass:
optional: true
stylus:
optional: true
sugarss:
optional: true
terser:
optional: true
yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
snapshots:
'@ampproject/remapping@2.3.0':
dependencies:
'@jridgewell/gen-mapping': 0.3.8
'@jridgewell/trace-mapping': 0.3.25
'@babel/code-frame@7.27.1':
dependencies:
'@babel/helper-validator-identifier': 7.27.1
js-tokens: 4.0.0
picocolors: 1.1.1
'@babel/compat-data@7.27.2': {}
'@babel/core@7.27.1':
dependencies:
'@ampproject/remapping': 2.3.0
'@babel/code-frame': 7.27.1
'@babel/generator': 7.27.1
'@babel/helper-compilation-targets': 7.27.2
'@babel/helper-module-transforms': 7.27.1(@babel/core@7.27.1)
'@babel/helpers': 7.27.1
'@babel/parser': 7.27.2
'@babel/template': 7.27.2
'@babel/traverse': 7.27.1
'@babel/types': 7.27.1
convert-source-map: 2.0.0
debug: 4.4.1
gensync: 1.0.0-beta.2
json5: 2.2.3
semver: 6.3.1
transitivePeerDependencies:
- supports-color
'@babel/generator@7.27.1':
dependencies:
'@babel/parser': 7.27.2
'@babel/types': 7.27.1
'@jridgewell/gen-mapping': 0.3.8
'@jridgewell/trace-mapping': 0.3.25
jsesc: 3.1.0
'@babel/helper-annotate-as-pure@7.27.1':
dependencies:
'@babel/types': 7.27.1
'@babel/helper-compilation-targets@7.27.2':
dependencies:
'@babel/compat-data': 7.27.2
'@babel/helper-validator-option': 7.27.1
browserslist: 4.24.5
lru-cache: 5.1.1
semver: 6.3.1
'@babel/helper-module-imports@7.27.1':
dependencies:
'@babel/traverse': 7.27.1
'@babel/types': 7.27.1
transitivePeerDependencies:
- supports-color
'@babel/helper-module-transforms@7.27.1(@babel/core@7.27.1)':
dependencies:
'@babel/core': 7.27.1
'@babel/helper-module-imports': 7.27.1
'@babel/helper-validator-identifier': 7.27.1
'@babel/traverse': 7.27.1
transitivePeerDependencies:
- supports-color
'@babel/helper-plugin-utils@7.27.1': {}
'@babel/helper-string-parser@7.27.1': {}
'@babel/helper-validator-identifier@7.27.1': {}
'@babel/helper-validator-option@7.27.1': {}
'@babel/helpers@7.27.1':
dependencies:
'@babel/template': 7.27.2
'@babel/types': 7.27.1
'@babel/parser@7.27.2':
dependencies:
'@babel/types': 7.27.1
'@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.27.1)':
dependencies:
'@babel/core': 7.27.1
'@babel/helper-plugin-utils': 7.27.1
'@babel/plugin-transform-react-jsx-development@7.27.1(@babel/core@7.27.1)':
dependencies:
'@babel/core': 7.27.1
'@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.27.1)
transitivePeerDependencies:
- supports-color
'@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.27.1)':
dependencies:
'@babel/core': 7.27.1
'@babel/helper-plugin-utils': 7.27.1
'@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.27.1)':
dependencies:
'@babel/core': 7.27.1
'@babel/helper-plugin-utils': 7.27.1
'@babel/plugin-transform-react-jsx@7.27.1(@babel/core@7.27.1)':
dependencies:
'@babel/core': 7.27.1
'@babel/helper-annotate-as-pure': 7.27.1
'@babel/helper-module-imports': 7.27.1
'@babel/helper-plugin-utils': 7.27.1
'@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.27.1)
'@babel/types': 7.27.1
transitivePeerDependencies:
- supports-color
'@babel/template@7.27.2':
dependencies:
'@babel/code-frame': 7.27.1
'@babel/parser': 7.27.2
'@babel/types': 7.27.1
'@babel/traverse@7.27.1':
dependencies:
'@babel/code-frame': 7.27.1
'@babel/generator': 7.27.1
'@babel/parser': 7.27.2
'@babel/template': 7.27.2
'@babel/types': 7.27.1
debug: 4.4.1
globals: 11.12.0
transitivePeerDependencies:
- supports-color
'@babel/types@7.27.1':
dependencies:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.27.1
'@esbuild/android-arm@0.15.18':
optional: true
'@esbuild/linux-loong64@0.15.18':
optional: true
'@jridgewell/gen-mapping@0.3.8':
dependencies:
'@jridgewell/set-array': 1.2.1
'@jridgewell/sourcemap-codec': 1.5.0
'@jridgewell/trace-mapping': 0.3.25
'@jridgewell/resolve-uri@3.1.2': {}
'@jridgewell/set-array@1.2.1': {}
'@jridgewell/sourcemap-codec@1.5.0': {}
'@jridgewell/trace-mapping@0.3.25':
dependencies:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.0
'@types/prop-types@15.7.14': {}
'@types/react-dom@18.3.7(@types/react@18.3.21)':
dependencies:
'@types/react': 18.3.21
'@types/react@18.3.21':
dependencies:
'@types/prop-types': 15.7.14
csstype: 3.1.3
'@vitejs/plugin-react@2.2.0(vite@3.2.11)':
dependencies:
'@babel/core': 7.27.1
'@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.27.1)
'@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.27.1)
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.1)
'@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.27.1)
magic-string: 0.26.7
react-refresh: 0.14.2
vite: 3.2.11
transitivePeerDependencies:
- supports-color
browserslist@4.24.5:
dependencies:
caniuse-lite: 1.0.30001718
electron-to-chromium: 1.5.155
node-releases: 2.0.19
update-browserslist-db: 1.1.3(browserslist@4.24.5)
caniuse-lite@1.0.30001718: {}
convert-source-map@2.0.0: {}
csstype@3.1.3: {}
debug@4.4.1:
dependencies:
ms: 2.1.3
electron-to-chromium@1.5.155: {}
esbuild-android-64@0.15.18:
optional: true
esbuild-android-arm64@0.15.18:
optional: true
esbuild-darwin-64@0.15.18:
optional: true
esbuild-darwin-arm64@0.15.18:
optional: true
esbuild-freebsd-64@0.15.18:
optional: true
esbuild-freebsd-arm64@0.15.18:
optional: true
esbuild-linux-32@0.15.18:
optional: true
esbuild-linux-64@0.15.18:
optional: true
esbuild-linux-arm64@0.15.18:
optional: true
esbuild-linux-arm@0.15.18:
optional: true
esbuild-linux-mips64le@0.15.18:
optional: true
esbuild-linux-ppc64le@0.15.18:
optional: true
esbuild-linux-riscv64@0.15.18:
optional: true
esbuild-linux-s390x@0.15.18:
optional: true
esbuild-netbsd-64@0.15.18:
optional: true
esbuild-openbsd-64@0.15.18:
optional: true
esbuild-sunos-64@0.15.18:
optional: true
esbuild-windows-32@0.15.18:
optional: true
esbuild-windows-64@0.15.18:
optional: true
esbuild-windows-arm64@0.15.18:
optional: true
esbuild@0.15.18:
optionalDependencies:
'@esbuild/android-arm': 0.15.18
'@esbuild/linux-loong64': 0.15.18
esbuild-android-64: 0.15.18
esbuild-android-arm64: 0.15.18
esbuild-darwin-64: 0.15.18
esbuild-darwin-arm64: 0.15.18
esbuild-freebsd-64: 0.15.18
esbuild-freebsd-arm64: 0.15.18
esbuild-linux-32: 0.15.18
esbuild-linux-64: 0.15.18
esbuild-linux-arm: 0.15.18
esbuild-linux-arm64: 0.15.18
esbuild-linux-mips64le: 0.15.18
esbuild-linux-ppc64le: 0.15.18
esbuild-linux-riscv64: 0.15.18
esbuild-linux-s390x: 0.15.18
esbuild-netbsd-64: 0.15.18
esbuild-openbsd-64: 0.15.18
esbuild-sunos-64: 0.15.18
esbuild-windows-32: 0.15.18
esbuild-windows-64: 0.15.18
esbuild-windows-arm64: 0.15.18
escalade@3.2.0: {}
fsevents@2.3.3:
optional: true
function-bind@1.1.2: {}
gensync@1.0.0-beta.2: {}
globals@11.12.0: {}
hasown@2.0.2:
dependencies:
function-bind: 1.1.2
is-core-module@2.16.1:
dependencies:
hasown: 2.0.2
js-tokens@4.0.0: {}
jsesc@3.1.0: {}
json5@2.2.3: {}
loose-envify@1.4.0:
dependencies:
js-tokens: 4.0.0
lru-cache@5.1.1:
dependencies:
yallist: 3.1.1
magic-string@0.26.7:
dependencies:
sourcemap-codec: 1.4.8
ms@2.1.3: {}
nanoid@3.3.11: {}
node-releases@2.0.19: {}
path-parse@1.0.7: {}
picocolors@1.1.1: {}
postcss@8.5.3:
dependencies:
nanoid: 3.3.11
picocolors: 1.1.1
source-map-js: 1.2.1
react-dom@18.3.1(react@18.3.1):
dependencies:
loose-envify: 1.4.0
react: 18.3.1
scheduler: 0.23.2
react-refresh@0.14.2: {}
react@18.3.1:
dependencies:
loose-envify: 1.4.0
resolve@1.22.10:
dependencies:
is-core-module: 2.16.1
path-parse: 1.0.7
supports-preserve-symlinks-flag: 1.0.0
rollup@2.79.2:
optionalDependencies:
fsevents: 2.3.3
scheduler@0.23.2:
dependencies:
loose-envify: 1.4.0
semver@6.3.1: {}
source-map-js@1.2.1: {}
sourcemap-codec@1.4.8: {}
supports-preserve-symlinks-flag@1.0.0: {}
typescript@4.9.5: {}
update-browserslist-db@1.1.3(browserslist@4.24.5):
dependencies:
browserslist: 4.24.5
escalade: 3.2.0
picocolors: 1.1.1
vite@3.2.11:
dependencies:
esbuild: 0.15.18
postcss: 8.5.3
resolve: 1.22.10
rollup: 2.79.2
optionalDependencies:
fsevents: 2.3.3
yallist@3.1.1: {}

View File

@ -1,59 +0,0 @@
#app {
height: 100vh;
text-align: center;
}
#logo {
display: block;
width: 50%;
height: 50%;
margin: auto;
padding: 10% 0 0;
background-position: center;
background-repeat: no-repeat;
background-size: 100% 100%;
background-origin: content-box;
}
.result {
height: 20px;
line-height: 20px;
margin: 1.5rem auto;
}
.input-box .btn {
width: 60px;
height: 30px;
line-height: 30px;
border-radius: 3px;
border: none;
margin: 0 0 0 20px;
padding: 0 8px;
cursor: pointer;
}
.input-box .btn:hover {
background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%);
color: #333333;
}
.input-box .input {
border: none;
border-radius: 3px;
outline: none;
height: 30px;
line-height: 30px;
padding: 0 10px;
background-color: rgba(240, 240, 240, 1);
-webkit-font-smoothing: antialiased;
}
.input-box .input:hover {
border: none;
background-color: rgba(255, 255, 255, 1);
}
.input-box .input:focus {
border: none;
background-color: rgba(255, 255, 255, 1);
}

View File

@ -1,41 +0,0 @@
import { useState } from 'react';
import logo from './assets/images/logo-universal.png';
import './App.css';
import { Greet } from '../wailsjs/go/main/App';
function App() {
const [resultText, setResultText] = useState(
'Enter your fansly API token, then press Go!',
);
const [name, setName] = useState('');
const updateName = (e: any) => setName(e.target.value);
const updateResultText = (result: string) => setResultText(result);
function greet() {
Greet(name).then(updateResultText);
}
return (
<div id='App'>
<img src={logo} id='logo' alt='logo' />
<div id='result' className='result'>
{resultText}
</div>
<div id='input' className='input-box'>
<input
id='name'
className='input'
onChange={updateName}
autoComplete='off'
name='input'
type='text'
/>
<button className='btn' onClick={greet}>
Go!
</button>
</div>
</div>
);
}
export default App;

View File

@ -1,93 +0,0 @@
Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com),
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

View File

@ -1,14 +0,0 @@
import React from 'react'
import {createRoot} from 'react-dom/client'
import './style.css'
import App from './App'
const container = document.getElementById('root')
const root = createRoot(container!)
root.render(
<React.StrictMode>
<App/>
</React.StrictMode>
)

View File

@ -1,26 +0,0 @@
html {
background-color: rgba(27, 38, 54, 1);
text-align: center;
color: white;
}
body {
margin: 0;
color: white;
font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
}
@font-face {
font-family: "Nunito";
font-style: normal;
font-weight: 400;
src: local(""),
url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2");
}
#app {
height: 100vh;
text-align: center;
}

View File

@ -1 +0,0 @@
/// <reference types="vite/client" />

View File

@ -1,31 +0,0 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": [
"DOM",
"DOM.Iterable",
"ESNext"
],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
],
"references": [
{
"path": "./tsconfig.node.json"
}
]
}

View File

@ -1,11 +0,0 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": [
"vite.config.ts"
]
}

View File

@ -1,7 +0,0 @@
import {defineConfig} from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()]
})

View File

@ -1,4 +0,0 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export function Greet(arg1:string):Promise<string>;

View File

@ -1,7 +0,0 @@
// @ts-check
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export function Greet(arg1) {
return window['go']['main']['App']['Greet'](arg1);
}

View File

@ -1,24 +0,0 @@
{
"name": "@wailsapp/runtime",
"version": "2.0.0",
"description": "Wails Javascript runtime library",
"main": "runtime.js",
"types": "runtime.d.ts",
"scripts": {
},
"repository": {
"type": "git",
"url": "git+https://github.com/wailsapp/wails.git"
},
"keywords": [
"Wails",
"Javascript",
"Go"
],
"author": "Lea Anthony <lea.anthony@gmail.com>",
"license": "MIT",
"bugs": {
"url": "https://github.com/wailsapp/wails/issues"
},
"homepage": "https://github.com/wailsapp/wails#readme"
}

View File

@ -1,249 +0,0 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The electron alternative for Go
(c) Lea Anthony 2019-present
*/
export interface Position {
x: number;
y: number;
}
export interface Size {
w: number;
h: number;
}
export interface Screen {
isCurrent: boolean;
isPrimary: boolean;
width : number
height : number
}
// Environment information such as platform, buildtype, ...
export interface EnvironmentInfo {
buildType: string;
platform: string;
arch: string;
}
// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit)
// emits the given event. Optional data may be passed with the event.
// This will trigger any event listeners.
export function EventsEmit(eventName: string, ...data: any): void;
// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name.
export function EventsOn(eventName: string, callback: (...data: any) => void): () => void;
// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple)
// sets up a listener for the given event name, but will only trigger a given number times.
export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void;
// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce)
// sets up a listener for the given event name, but will only trigger once.
export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void;
// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff)
// unregisters the listener for the given event name.
export function EventsOff(eventName: string, ...additionalEventNames: string[]): void;
// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
// unregisters all listeners.
export function EventsOffAll(): void;
// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
// logs the given message as a raw message
export function LogPrint(message: string): void;
// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace)
// logs the given message at the `trace` log level.
export function LogTrace(message: string): void;
// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug)
// logs the given message at the `debug` log level.
export function LogDebug(message: string): void;
// [LogError](https://wails.io/docs/reference/runtime/log#logerror)
// logs the given message at the `error` log level.
export function LogError(message: string): void;
// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal)
// logs the given message at the `fatal` log level.
// The application will quit after calling this method.
export function LogFatal(message: string): void;
// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo)
// logs the given message at the `info` log level.
export function LogInfo(message: string): void;
// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning)
// logs the given message at the `warning` log level.
export function LogWarning(message: string): void;
// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload)
// Forces a reload by the main application as well as connected browsers.
export function WindowReload(): void;
// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp)
// Reloads the application frontend.
export function WindowReloadApp(): void;
// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop)
// Sets the window AlwaysOnTop or not on top.
export function WindowSetAlwaysOnTop(b: boolean): void;
// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme)
// *Windows only*
// Sets window theme to system default (dark/light).
export function WindowSetSystemDefaultTheme(): void;
// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme)
// *Windows only*
// Sets window to light theme.
export function WindowSetLightTheme(): void;
// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme)
// *Windows only*
// Sets window to dark theme.
export function WindowSetDarkTheme(): void;
// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter)
// Centers the window on the monitor the window is currently on.
export function WindowCenter(): void;
// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle)
// Sets the text in the window title bar.
export function WindowSetTitle(title: string): void;
// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen)
// Makes the window full screen.
export function WindowFullscreen(): void;
// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen)
// Restores the previous window dimensions and position prior to full screen.
export function WindowUnfullscreen(): void;
// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen)
// Returns the state of the window, i.e. whether the window is in full screen mode or not.
export function WindowIsFullscreen(): Promise<boolean>;
// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
// Sets the width and height of the window.
export function WindowSetSize(width: number, height: number): void;
// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
// Gets the width and height of the window.
export function WindowGetSize(): Promise<Size>;
// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize)
// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions.
// Setting a size of 0,0 will disable this constraint.
export function WindowSetMaxSize(width: number, height: number): void;
// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize)
// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions.
// Setting a size of 0,0 will disable this constraint.
export function WindowSetMinSize(width: number, height: number): void;
// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition)
// Sets the window position relative to the monitor the window is currently on.
export function WindowSetPosition(x: number, y: number): void;
// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition)
// Gets the window position relative to the monitor the window is currently on.
export function WindowGetPosition(): Promise<Position>;
// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide)
// Hides the window.
export function WindowHide(): void;
// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow)
// Shows the window, if it is currently hidden.
export function WindowShow(): void;
// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise)
// Maximises the window to fill the screen.
export function WindowMaximise(): void;
// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise)
// Toggles between Maximised and UnMaximised.
export function WindowToggleMaximise(): void;
// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise)
// Restores the window to the dimensions and position prior to maximising.
export function WindowUnmaximise(): void;
// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised)
// Returns the state of the window, i.e. whether the window is maximised or not.
export function WindowIsMaximised(): Promise<boolean>;
// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise)
// Minimises the window.
export function WindowMinimise(): void;
// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise)
// Restores the window to the dimensions and position prior to minimising.
export function WindowUnminimise(): void;
// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised)
// Returns the state of the window, i.e. whether the window is minimised or not.
export function WindowIsMinimised(): Promise<boolean>;
// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal)
// Returns the state of the window, i.e. whether the window is normal or not.
export function WindowIsNormal(): Promise<boolean>;
// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour)
// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels.
export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void;
// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall)
// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system.
export function ScreenGetAll(): Promise<Screen[]>;
// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl)
// Opens the given URL in the system browser.
export function BrowserOpenURL(url: string): void;
// [Environment](https://wails.io/docs/reference/runtime/intro#environment)
// Returns information about the environment
export function Environment(): Promise<EnvironmentInfo>;
// [Quit](https://wails.io/docs/reference/runtime/intro#quit)
// Quits the application.
export function Quit(): void;
// [Hide](https://wails.io/docs/reference/runtime/intro#hide)
// Hides the application.
export function Hide(): void;
// [Show](https://wails.io/docs/reference/runtime/intro#show)
// Shows the application.
export function Show(): void;
// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext)
// Returns the current text stored on clipboard
export function ClipboardGetText(): Promise<string>;
// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
// Sets a text on the clipboard
export function ClipboardSetText(text: string): Promise<boolean>;
// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop)
// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void
// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff)
// OnFileDropOff removes the drag and drop listeners and handlers.
export function OnFileDropOff() :void
// Check if the file path resolver is available
export function CanResolveFilePaths(): boolean;
// Resolves file paths for an array of files
export function ResolveFilePaths(files: File[]): void

View File

@ -1,238 +0,0 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The electron alternative for Go
(c) Lea Anthony 2019-present
*/
export function LogPrint(message) {
window.runtime.LogPrint(message);
}
export function LogTrace(message) {
window.runtime.LogTrace(message);
}
export function LogDebug(message) {
window.runtime.LogDebug(message);
}
export function LogInfo(message) {
window.runtime.LogInfo(message);
}
export function LogWarning(message) {
window.runtime.LogWarning(message);
}
export function LogError(message) {
window.runtime.LogError(message);
}
export function LogFatal(message) {
window.runtime.LogFatal(message);
}
export function EventsOnMultiple(eventName, callback, maxCallbacks) {
return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
}
export function EventsOn(eventName, callback) {
return EventsOnMultiple(eventName, callback, -1);
}
export function EventsOff(eventName, ...additionalEventNames) {
return window.runtime.EventsOff(eventName, ...additionalEventNames);
}
export function EventsOnce(eventName, callback) {
return EventsOnMultiple(eventName, callback, 1);
}
export function EventsEmit(eventName) {
let args = [eventName].slice.call(arguments);
return window.runtime.EventsEmit.apply(null, args);
}
export function WindowReload() {
window.runtime.WindowReload();
}
export function WindowReloadApp() {
window.runtime.WindowReloadApp();
}
export function WindowSetAlwaysOnTop(b) {
window.runtime.WindowSetAlwaysOnTop(b);
}
export function WindowSetSystemDefaultTheme() {
window.runtime.WindowSetSystemDefaultTheme();
}
export function WindowSetLightTheme() {
window.runtime.WindowSetLightTheme();
}
export function WindowSetDarkTheme() {
window.runtime.WindowSetDarkTheme();
}
export function WindowCenter() {
window.runtime.WindowCenter();
}
export function WindowSetTitle(title) {
window.runtime.WindowSetTitle(title);
}
export function WindowFullscreen() {
window.runtime.WindowFullscreen();
}
export function WindowUnfullscreen() {
window.runtime.WindowUnfullscreen();
}
export function WindowIsFullscreen() {
return window.runtime.WindowIsFullscreen();
}
export function WindowGetSize() {
return window.runtime.WindowGetSize();
}
export function WindowSetSize(width, height) {
window.runtime.WindowSetSize(width, height);
}
export function WindowSetMaxSize(width, height) {
window.runtime.WindowSetMaxSize(width, height);
}
export function WindowSetMinSize(width, height) {
window.runtime.WindowSetMinSize(width, height);
}
export function WindowSetPosition(x, y) {
window.runtime.WindowSetPosition(x, y);
}
export function WindowGetPosition() {
return window.runtime.WindowGetPosition();
}
export function WindowHide() {
window.runtime.WindowHide();
}
export function WindowShow() {
window.runtime.WindowShow();
}
export function WindowMaximise() {
window.runtime.WindowMaximise();
}
export function WindowToggleMaximise() {
window.runtime.WindowToggleMaximise();
}
export function WindowUnmaximise() {
window.runtime.WindowUnmaximise();
}
export function WindowIsMaximised() {
return window.runtime.WindowIsMaximised();
}
export function WindowMinimise() {
window.runtime.WindowMinimise();
}
export function WindowUnminimise() {
window.runtime.WindowUnminimise();
}
export function WindowSetBackgroundColour(R, G, B, A) {
window.runtime.WindowSetBackgroundColour(R, G, B, A);
}
export function ScreenGetAll() {
return window.runtime.ScreenGetAll();
}
export function WindowIsMinimised() {
return window.runtime.WindowIsMinimised();
}
export function WindowIsNormal() {
return window.runtime.WindowIsNormal();
}
export function BrowserOpenURL(url) {
window.runtime.BrowserOpenURL(url);
}
export function Environment() {
return window.runtime.Environment();
}
export function Quit() {
window.runtime.Quit();
}
export function Hide() {
window.runtime.Hide();
}
export function Show() {
window.runtime.Show();
}
export function ClipboardGetText() {
return window.runtime.ClipboardGetText();
}
export function ClipboardSetText(text) {
return window.runtime.ClipboardSetText(text);
}
/**
* Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
*
* @export
* @callback OnFileDropCallback
* @param {number} x - x coordinate of the drop
* @param {number} y - y coordinate of the drop
* @param {string[]} paths - A list of file paths.
*/
/**
* OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
*
* @export
* @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
* @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target)
*/
export function OnFileDrop(callback, useDropTarget) {
return window.runtime.OnFileDrop(callback, useDropTarget);
}
/**
* OnFileDropOff removes the drag and drop listeners and handlers.
*/
export function OnFileDropOff() {
return window.runtime.OnFileDropOff();
}
export function CanResolveFilePaths() {
return window.runtime.CanResolveFilePaths();
}
export function ResolveFilePaths(files) {
return window.runtime.ResolveFilePaths(files);
}

36
go.mod
View File

@ -1,36 +0,0 @@
module FanslySync
go 1.23
require github.com/wailsapp/wails/v2 v2.10.1
require (
github.com/bep/debounce v1.2.1 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
github.com/labstack/echo/v4 v4.13.3 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
github.com/leaanthony/gosod v1.0.4 // indirect
github.com/leaanthony/slicer v1.6.0 // indirect
github.com/leaanthony/u v1.1.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/samber/lo v1.49.1 // indirect
github.com/tkrajina/go-reflector v0.5.8 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/wailsapp/go-webview2 v1.0.19 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect
golang.org/x/crypto v0.33.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
)
// replace github.com/wailsapp/wails/v2 v2.10.1 => C:\Users\tsomm\go\pkg\mod

79
go.sum
View File

@ -1,79 +0,0 @@
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI=
github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw=
github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js=
github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=
github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/wailsapp/go-webview2 v1.0.19 h1:7U3QcDj1PrBPaxJNCui2k1SkWml+Q5kvFUFyTImA6NU=
github.com/wailsapp/go-webview2 v1.0.19/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
github.com/wailsapp/wails/v2 v2.10.1 h1:QWHvWMXII2nI/nXz77gpPG8P3ehl6zKe+u4su5BWIns=
github.com/wailsapp/wails/v2 v2.10.1/go.mod h1:zrebnFV6MQf9kx8HI4iAv63vsR5v67oS7GTEZ7Pz1TY=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -1,213 +0,0 @@
package handlers
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"FanslySync/structs"
"FanslySync/utils"
"github.com/wailsapp/wails/v2/pkg/logger"
)
// ConfigManager defines methods for accessing and persisting application configuration.
type ConfigManager interface {
// GetConfig returns the current configuration. If forceReload is true,
// it re-reads the file from disk even if a cached copy exists.
GetConfig(forceReload bool) (*structs.Config, error)
// LoadConfigOrCreate loads the config or, if the file doesn't exist,
// creates a default config, saves it, and returns it.
LoadConfigOrCreate() (*structs.Config, error)
// SaveConfig writes the given config to disk and updates the in-memory cache.
SaveConfig(config *structs.Config) error
// GetConfigPath returns the filesystem path where the config is stored.
GetConfigPath() string
// ShouldMigrateOldAppConfig checks if the old app config file exists
// and returns true if it should be migrated.
ShouldMigrateOldAppConfig() (bool, error)
// MigrateOldAppConfig migrates the old app config file to the new format.
MigrateOldAppConfig() error
}
// FileConfigManager implements ConfigManager using a JSON file on disk
// with in-memory caching and custom logging.
type FileConfigManager struct {
path string
config *structs.Config
log logger.Logger
}
func NewFileConfigManager(path string) (ConfigManager, error) {
// Create our logger
fileLogger, loggerCreateErr := utils.NewLogger("ConfigManager")
if loggerCreateErr != nil {
// Log the error and return nil
return nil, loggerCreateErr
}
return &FileConfigManager{
path: path,
log: fileLogger,
}, nil
}
// GetConfigPath returns the path to the config file.
func (mgr *FileConfigManager) GetConfigPath() string {
return mgr.path
}
func GetConfigPathForRuntime() (string, error) {
dir, err := os.UserConfigDir()
if err != nil {
return "", err
}
return filepath.Join(dir, "FanslySync", "appconfig.json"), nil
}
// ShouldMigrateOldAppConfig checks for an existing legacy config.json and logs the result.
func (mgr *FileConfigManager) ShouldMigrateOldAppConfig() (bool, error) {
mgr.log.Info("Checking for old config file")
dir, err := os.UserConfigDir()
if err != nil {
mgr.log.Error(fmt.Sprintf("Error getting user config dir: %v", err))
return false, err
}
oldConfigPath := filepath.Join(dir, "FanslySync", "config.json")
if _, err := os.Stat(oldConfigPath); os.IsNotExist(err) {
mgr.log.Info(fmt.Sprintf("No old config at %s", oldConfigPath))
return false, nil
} else if err != nil {
mgr.log.Error(fmt.Sprintf("Error checking old config: %v", err))
return false, err
}
mgr.log.Info(fmt.Sprintf("Old config exists at %s", oldConfigPath))
return true, nil
}
// MigrateOldAppConfig reads the legacy config.json, converts it, saves the new format,
// and removes the old file, logging each step.
func (mgr *FileConfigManager) MigrateOldAppConfig() error {
mgr.log.Info("Migrating old config file")
dir, err := os.UserConfigDir()
if err != nil {
mgr.log.Error(fmt.Sprintf("Error getting user config dir: %v", err))
return err
}
oldConfigPath := filepath.Join(dir, "FanslySync", "config.json")
if _, err := os.Stat(oldConfigPath); os.IsNotExist(err) {
mgr.log.Info("No old config to migrate")
return nil
} else if err != nil {
mgr.log.Error(fmt.Sprintf("Error checking old config: %v", err))
return err
}
data, err := os.ReadFile(oldConfigPath)
if err != nil {
mgr.log.Error(fmt.Sprintf("Error reading old config: %v", err))
return err
}
var oldCfg structs.OldConfig
if err := json.Unmarshal(data, &oldCfg); err != nil {
mgr.log.Error(fmt.Sprintf("Error unmarshaling old config: %v", err))
return err
}
newCfg := structs.NewConfigFromOld(&oldCfg)
if err := mgr.SaveConfig(newCfg); err != nil {
mgr.log.Error(fmt.Sprintf("Error saving new config: %v", err))
return err
}
if err := os.Remove(oldConfigPath); err != nil {
mgr.log.Error(fmt.Sprintf("Error removing old config: %v", err))
return fmt.Errorf("could not remove old config file: %w", err)
}
mgr.log.Info("Migration complete; old config removed")
return nil
}
// GetConfig loads the config from disk if forceReload is true or no cache exists.
// It logs each step and errors encountered.
func (mgr *FileConfigManager) GetConfig(forceReload bool) (*structs.Config, error) {
mgr.log.Debug(fmt.Sprintf("GetConfig(forceReload=%v)", forceReload))
if mgr.config != nil && !forceReload {
mgr.log.Debug("Returning cached config")
return mgr.config, nil
}
data, err := os.ReadFile(mgr.path)
if err != nil {
mgr.log.Error(fmt.Sprintf("Error reading config file: %v", err))
return nil, err
}
var cfg structs.Config
if err := json.Unmarshal(data, &cfg); err != nil {
mgr.log.Error(fmt.Sprintf("Error unmarshaling config: %v", err))
return nil, err
}
mgr.config = &cfg
mgr.log.Info("Config loaded from disk. Cache updated.")
mgr.log.Debug(fmt.Sprintf("Config: %+v", cfg))
return mgr.config, nil
}
// LoadConfigOrCreate loads an existing config or creates/saves a default if none exists.
func (mgr *FileConfigManager) LoadConfigOrCreate() (*structs.Config, error) {
cfg, err := mgr.GetConfig(false)
if err == nil {
mgr.log.Info("Existing config loaded")
return cfg, nil
}
if os.IsNotExist(err) {
mgr.log.Warning("Config missing; creating default")
defaultCfg := structs.NewConfig()
if saveErr := mgr.SaveConfig(defaultCfg); saveErr != nil {
mgr.log.Error(fmt.Sprintf("Error saving default config: %v", saveErr))
return nil, saveErr
}
mgr.log.Info("Default config created and saved")
return defaultCfg, nil
}
mgr.log.Error(fmt.Sprintf("Error loading config: %v", err))
return nil, err
}
// SaveConfig writes the config to disk, updates cache, and logs the process.
func (mgr *FileConfigManager) SaveConfig(cfg *structs.Config) error {
mgr.log.Info(fmt.Sprintf("Saving config to %s", mgr.path))
dir := filepath.Dir(mgr.path)
if err := os.MkdirAll(dir, 0o755); err != nil {
mgr.log.Error(fmt.Sprintf("Error creating config directory: %v", err))
return fmt.Errorf("could not create config directory: %w", err)
}
data, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
mgr.log.Error(fmt.Sprintf("Error marshaling config: %v", err))
return fmt.Errorf("could not marshal config: %w", err)
}
if err := os.WriteFile(mgr.path, data, 0o644); err != nil {
mgr.log.Error(fmt.Sprintf("Error writing config file: %v", err))
return fmt.Errorf("could not write config file: %w", err)
}
mgr.config = cfg
mgr.log.Info("Config saved and cache updated")
return nil
}

View File

@ -1,267 +0,0 @@
package handlers
import (
"FanslySync/structs"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"FanslySync/utils"
"github.com/wailsapp/wails/v2/pkg/logger"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
type FanslyAPIController struct {
client *http.Client
token string
logger logger.Logger
}
// New creates a new Fansly client with the provided token (optional).
func NewFanslyAPIController(token string) (*FanslyAPIController, error) {
client := &http.Client{
Timeout: 30 * time.Second,
}
apiLogger, apiLoggerCreateErr := utils.NewLogger("FanslyAPIController")
if apiLoggerCreateErr != nil {
// Log the error and return nil
fmt.Println("Failed to create logger: ", apiLoggerCreateErr)
return nil, apiLoggerCreateErr
}
return &FanslyAPIController{
client: client,
token: token,
logger: apiLogger,
}, nil
}
// GET issues a GET to /api/v1/{path}, optionally adding the Auth header,
// and unmarshals the JSON response into the result parameter.
func (f *FanslyAPIController) GET(path string, needsAuth bool, out interface{}) error {
// build request
url := "https://apiv3.fansly.com/api/v1/" + path
req, err := http.NewRequest("GET", url, nil)
if err != nil {
f.logger.Error("NewRequest GET " + path + ": " + err.Error())
return err
}
// Set headers
req.Header.Set("User-Agent", "FanslySync/3.0 sticks@teamhydra.dev")
req.Header.Set("Accept", "application/json")
// set auth
if needsAuth && f.token != "" {
req.Header.Set("Authorization", f.token)
}
// send
resp, err := f.client.Do(req)
if err != nil {
f.logger.Error("Do GET " + path + ": " + err.Error())
return err
}
defer resp.Body.Close()
// non-200
if resp.StatusCode != http.StatusOK {
f.logger.Error(fmt.Sprintf("GET %s failed: %s", path, resp.Status))
// read body for logs
body, _ := io.ReadAll(resp.Body)
f.logger.Info("Response body: " + string(body))
return fmt.Errorf("unexpected status %s", resp.Status)
}
// 200 ok, log
f.logger.Debug(fmt.Sprintf("GET %s succeeded: %s", path, resp.Status))
// read body and unmarshal
body, err := io.ReadAll(resp.Body)
if err != nil {
f.logger.Error("Request was OK, but failed to read body: " + err.Error())
return err
}
// unmarshal into our expected response type
err = json.Unmarshal(body, &out)
if err != nil {
f.logger.Error("GET " + path + " failed to unmarshal response: " + err.Error())
return err
}
return nil
}
// SetToken updates the authorization token for subsequent requests.
func (f *FanslyAPIController) SetToken(token string) {
f.token = token
}
// Returns the current user's account information from the Fansly API.
//
// Will error if the token is not set or the request fails.
//
// Returns a FanslyAccount struct containing the account information.
func (f *FanslyAPIController) GetMe() (*structs.FanslyAccount, error) {
var response structs.FanslyBaseResponse[structs.FanslyAccountResponse]
err := f.GET("account/me", true, &response)
if err != nil {
f.logger.Error("GetMe failed: " + err.Error())
return nil, err
}
// Return the account info
return &response.Response.Account, nil
}
func (f *FanslyAPIController) GetFollowersWithOffset(acctId string, offset int) ([]structs.FanslyFollowResponse, error) {
var response structs.FanslyBaseResponseAsArray[structs.FanslyFollowResponse]
err := f.GET(fmt.Sprintf("account/%s/followers?limit=300&offset=%d", acctId, offset), true, &response)
if err != nil {
f.logger.Error("GetFollowersWithOffset failed: " + err.Error())
return nil, err
}
// Return the followers
return response.Response, nil
}
func (f *FanslyAPIController) GetSubscribersWithOffset(offset int) (structs.FanslySubscriptionResponse, error) {
var response structs.FanslyBaseResponse[structs.FanslySubscriptionResponse]
err := f.GET(fmt.Sprintf("subscribers?status=3,4&limit=300&offset=%d", offset), true, &response)
if err != nil {
f.logger.Error("GetSubscribersWithOffset failed: " + err.Error())
return structs.FanslySubscriptionResponse{}, err
}
// Return the subscribers
return response.Response, nil
}
func (f *FanslyAPIController) Sync(ctx context.Context, token string, auto bool) {
// Start the sync worker
go syncWorker(ctx, token, auto)
}
func syncWorker(ctx context.Context, token string, auto bool) {
runtime.EventsEmit(ctx, "sync:started", nil)
progress := func(step string, curr, total int, done bool) {
p := structs.SyncProgressEvent{
Step: step,
PercentDone: utils.Percentage(curr, total),
Count: curr,
TotalCount: total,
Complete: done,
}
runtime.EventsEmit(ctx, "sync:progress", p)
}
// Create a logger dedicated to this sync run
syncLogger, err := utils.NewLogger("SyncWorker")
if err != nil {
runtime.EventsEmit(ctx, "sync:error", err.Error())
return
}
var startTime time.Time = time.Now()
syncLogger.Info(fmt.Sprintf("Starting sync at %s, auto=%t", startTime.Format(time.RFC3339), auto))
syncLogger.Info("[1/4] Fetching profile…")
// Instantiate API controller
c, err := NewFanslyAPIController(token)
if err != nil {
runtime.EventsEmit(ctx, "sync:error", err.Error())
return
}
// 1. Profile -----------------------------------------------------------
progress("Fetching profile", 0, 100, false)
acct, err := c.GetMe()
if err != nil {
runtime.EventsEmit(ctx, "sync:error", err.Error())
return
}
progress("Fetched profile", 0, 100, false)
syncLogger.Info(fmt.Sprintf("Fetched profile. %s %d followers, %d subscribers", acct.Username, acct.FollowCount, acct.SubscriberCount))
// 2. Followers ---------------------------------------------------------
syncLogger.Info("[2/4] Fetching followers…")
followers := make([]string, 0, acct.FollowCount)
offset := 0
for len(followers) < acct.FollowCount {
batch, err := c.GetFollowersWithOffset(acct.ID, offset)
if err != nil {
runtime.EventsEmit(ctx, "sync:error", err.Error())
return
}
for _, f := range batch {
followers = append(followers, f.FollowerID)
}
offset += 300
progress("Fetching followers", len(followers), acct.FollowCount, false)
syncLogger.Info(fmt.Sprintf("[followers] %d/%d (offset=%d)", len(followers), acct.FollowCount, offset))
if len(batch) == 0 || len(followers) >= acct.FollowCount {
break
}
time.Sleep(250 * time.Millisecond) // gentle throttle
}
// 3. Subscribers -------------------------------------------------------
syncLogger.Info("[3/4] Fetching subscribers…")
subs := make([]structs.Subscription, 0, acct.SubscriberCount)
offset = 0
for len(subs) < acct.SubscriberCount {
res, err := c.GetSubscribersWithOffset(offset)
if err != nil {
runtime.EventsEmit(ctx, "sync:error", err.Error())
return
}
subs = append(subs, res.Subscriptions...)
offset += 300
progress("Fetching subscribers", len(subs), acct.SubscriberCount, false)
syncLogger.Info(fmt.Sprintf("[subs] %d/%d (offset=%d)", len(subs), acct.SubscriberCount, offset))
if len(res.Subscriptions) == 0 || len(subs) >= acct.SubscriberCount {
break
}
time.Sleep(250 * time.Millisecond)
}
// 4. Upload & finish ---------------------------------------------------
syncLogger.Info("[4/4] Uploading data…")
data := structs.SyncData{
Followers: followers,
Subscriptions: subs,
}
if !auto {
payload, err := json.Marshal(data)
if err != nil {
runtime.EventsEmit(ctx, "sync:error", err.Error())
return
}
url, err := utils.UploadPaste(payload)
if err != nil {
runtime.EventsEmit(ctx, "sync:error", err.Error())
return
}
data.PasteURL = url
syncLogger.Info(fmt.Sprintf("Uploaded data to %s", url))
}
progress("Sync complete", 100, 100, true)
syncLogger.Info(fmt.Sprintf("Sync done at %s. Took %s, fetched %d followers and %d subscribers", time.Now().Format(time.RFC3339), time.Since(startTime).String(), len(followers), len(subs)))
runtime.EventsEmit(ctx, "sync:complete", data)
}

57
main.go
View File

@ -1,57 +0,0 @@
package main
import (
"FanslySync/utils"
"context"
"embed"
"log"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/logger"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
//go:embed all:frontend/dist
var assets embed.FS
func main() {
// Create an instance of the app structure
app := NewApp()
// Create our custom file logger
fileLogger, loggerCreateErr := utils.NewLogger("runtime")
startupLogger, startupLoggerCreateErr := utils.NewLogger("startup")
if loggerCreateErr != nil || startupLoggerCreateErr != nil {
log.Fatal("Failed to create one or more loggers: ", loggerCreateErr, startupLoggerCreateErr)
}
// Create application with options
err := wails.Run(&options.App{
Title: "FanslySync",
Width: 1024,
Height: 768,
AssetServer: &assetserver.Options{
Assets: assets,
},
DisableResize: true,
Logger: fileLogger,
LogLevel: logger.ERROR,
LogLevelProduction: logger.INFO,
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
OnStartup: func(ctx context.Context) {
app.startup(ctx, startupLogger)
},
Bind: []interface{}{
app,
},
})
if err != nil {
// Build a message box with the error
utils.ShowMessageBox(app.ctx, "FanslySync | Initialization Error", "Could not start application.\n\nError: "+err.Error(), utils.WithDialogType(runtime.ErrorDialog))
runtime.Quit(app.ctx)
}
}

57
package.json Normal file
View File

@ -0,0 +1,57 @@
{
"name": "fanslysync-desktop",
"version": "0.2.0",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check . && eslint .",
"format": "prettier --write .",
"tauri": "tauri",
"build_prod": "tauri build",
"publish": "npm run build_prod && npm run publish:makedraft && npm run publish:upload && npm run pubish:publishcn",
"publish_nobuild": "npm run publish:makedraft && npm run publish:upload && npm run pubish:publishcn",
"publish:makedraft": "cn release draft fansly-creator-bot/fansly-sync --framework tauri",
"publish:upload": "cn release upload fansly-creator-bot/fansly-sync --framework tauri",
"pubish:publishcn": "cn release publish fansly-creator-bot/fansly-sync --framework tauri"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^3.2.5",
"@sveltejs/adapter-static": "^3.0.5",
"@sveltejs/kit": "^2.6.1",
"@sveltejs/vite-plugin-svelte": "^3.1.2",
"@tauri-apps/cli": "^2.4.1",
"@types/eslint": "^9.6.1",
"autoprefixer": "^10.4.20",
"eslint": "^9.12.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.44.1",
"globals": "^15.10.0",
"postcss": "^8.4.47",
"prettier": "^3.3.3",
"prettier-plugin-svelte": "^3.2.7",
"svelte": "^4.2.19",
"svelte-check": "^4.0.4",
"tailwindcss": "^3.4.13",
"tslib": "^2.7.0",
"typescript": "^5.6.2",
"typescript-eslint": "^8.8.0",
"vite": "^5.4.8"
},
"type": "module",
"dependencies": {
"@tauri-apps/api": "^2.1.1",
"@tauri-apps/plugin-autostart": "^2.0.0",
"@tauri-apps/plugin-clipboard-manager": "^2.0.1",
"@tauri-apps/plugin-dialog": "^2.0.1",
"@tauri-apps/plugin-log": "^2.0.1",
"@tauri-apps/plugin-notification": "^2.0.0",
"@tauri-apps/plugin-os": "^2.0.0",
"@tauri-apps/plugin-process": "^2.2.1",
"@tauri-apps/plugin-updater": "^2.7.1",
"svelte-french-toast": "^1.2.0"
}
}

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

3
renovate.json Normal file
View File

@ -0,0 +1,3 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
}

3
src-tauri/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
# Generated by Cargo
# will have compiled files and executables
/target/

6800
src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

45
src-tauri/Cargo.toml Normal file
View File

@ -0,0 +1,45 @@
[package]
name = "app"
version = "0.1.6"
description = "A Tauri App"
authors = ["SticksDev"]
license = "MIT"
repository = ""
default-run = "app"
edition = "2021"
rust-version = "1.80"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "2.0.0", features = [] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "2.4.1", features = ["tray-icon"] }
dirs = "5.0.1"
reqwest = { version = "0.11.18", features = ["json", "multipart"] }
lazy_static = "1.5.0"
tokio = { version = "1.29.1", features = ["full"] }
tokio-macros = "2.3.0"
tauri-plugin-os = { version = "2.2.1" }
tauri-plugin-dialog = "2.2.1"
tauri-plugin-clipboard-manager = { version = "2.2.1" }
tauri-plugin-notification = { version = "2.2.1" }
tauri-plugin-updater = "2.2.1"
tauri-plugin-log = { version = "2.2.1" }
log = "0.4.27"
thiserror = "2.0.12"
tauri-plugin-process = "2"
[features]
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
# If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes.
# DO NOT REMOVE!!
custom-protocol = ["tauri/custom-protocol"]
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-autostart = "2.3.0"
tauri-plugin-single-instance = "2"
tauri-plugin-updater = "2"

3
src-tauri/build.rs Normal file
View File

@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

View File

@ -0,0 +1,37 @@
{
"identifier": "migrated",
"description": "permissions that were migrated from v1",
"local": true,
"windows": [
"main"
],
"permissions": [
"core:default",
"dialog:allow-message",
"dialog:allow-ask",
"dialog:allow-confirm",
"notification:default",
"os:allow-platform",
"os:allow-version",
"os:allow-os-type",
"os:allow-family",
"os:allow-arch",
"os:allow-exe-extension",
"os:allow-locale",
"os:allow-hostname",
"clipboard-manager:allow-read-text",
"clipboard-manager:allow-write-text",
"core:app:allow-app-show",
"core:app:allow-app-hide",
"os:default",
"dialog:default",
"clipboard-manager:default",
"notification:default",
"updater:default",
"log:default",
"autostart:default",
"process:default",
"process:allow-restart",
"process:default"
]
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"migrated":{"identifier":"migrated","description":"permissions that were migrated from v1","local":true,"windows":["main"],"permissions":["core:default","dialog:allow-message","dialog:allow-ask","dialog:allow-confirm","notification:default","os:allow-platform","os:allow-version","os:allow-os-type","os:allow-family","os:allow-arch","os:allow-exe-extension","os:allow-locale","os:allow-hostname","clipboard-manager:allow-read-text","clipboard-manager:allow-write-text","core:app:allow-app-show","core:app:allow-app-hide","os:default","dialog:default","clipboard-manager:default","notification:default","updater:default","log:default","autostart:default","process:default","process:allow-restart","process:default"]}}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

BIN
src-tauri/icons/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

BIN
src-tauri/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1011 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 957 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
src-tauri/icons/icon.icns Normal file

Binary file not shown.

BIN
src-tauri/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
src-tauri/icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

10
src-tauri/install.sh Normal file
View File

@ -0,0 +1,10 @@
sudo apt update
sudo apt install libwebkit2gtk-4.1-dev \
build-essential \
curl \
wget \
file \
libxdo-dev \
libssl-dev \
libayatana-appindicator3-dev \
librsvg2-dev

View File

@ -0,0 +1,43 @@
use crate::handlers::config::{get_config_path, Config};
#[tauri::command]
pub fn init_config() -> Result<(), String> {
log::info!("[commands::config::init_config] Initializing config...");
let config_path = get_config_path().map_err(|e| e.to_string())?;
log::info!(
"[commands::config::init_config] Config path: {}",
config_path.display()
);
Config::load_or_create(&config_path).map_err(|e| e.to_string())?;
log::info!("[commands::config::init_config] Config initialized successfully");
Ok(())
}
#[tauri::command]
pub fn get_config() -> Result<Config, String> {
let config_path = get_config_path().map_err(|e| e.to_string())?;
let config = Config::load_or_create(&config_path).map_err(|e| e.to_string())?;
log::info!(
"[commands::config::get_config] Config loaded successfully: {:?} from path: {}",
config,
config_path.display()
);
Ok(config)
}
#[tauri::command]
pub fn save_config(config: Config) -> Result<(), String> {
let config_path = get_config_path().map_err(|e| e.to_string())?;
log::info!(
"[commands::config::save_config] Saving config: {:?} to path: {}",
config,
config_path.display()
);
config.save(&config_path).map_err(|e| e.to_string())?;
Ok(())
}

View File

@ -0,0 +1,69 @@
use crate::handlers::fansly::{SyncProgress, PROGRESS};
use crate::{
handlers::fansly::Fansly,
structs::{FanslyAccountResponse, FanslyBaseResponse, SyncDataResponse},
};
use lazy_static::lazy_static;
use serde_json::Value;
use tokio::sync::Mutex;
lazy_static! {
static ref FANSLY: Mutex<Fansly> = Mutex::new(Fansly::new(None));
}
#[tauri::command]
pub async fn fansly_set_token(token: Option<String>) {
FANSLY.lock().await.set_token(token);
}
#[tauri::command]
pub async fn fansly_get_me() -> Result<FanslyBaseResponse<FanslyAccountResponse>, String> {
let fansly = FANSLY.lock().await;
let response = fansly.get_profile().await;
match response {
Ok(response) => Ok(response),
Err(e) => Err(e.to_string()),
}
}
#[tauri::command]
pub async fn fansly_sync(auto: bool) -> Result<SyncDataResponse, String> {
let mut fansly = FANSLY.lock().await;
let response = fansly.sync(auto).await;
match response {
Ok(response) => Ok(response),
Err(e) => Err(e.to_string()),
}
}
#[tauri::command]
pub async fn fansly_get_sync_status() -> SyncProgress {
PROGRESS.lock().await.clone()
}
#[tauri::command]
pub async fn fansly_upload_auto_sync_data(
data: SyncDataResponse,
token: String,
) -> Result<(), String> {
let fansly: tokio::sync::MutexGuard<Fansly> = FANSLY.lock().await;
let response = fansly.upload_auto_sync_data(data, token).await;
match response {
Ok(_) => Ok(()),
Err(e) => Err(e.to_string()),
}
}
#[tauri::command]
pub async fn fansly_check_sync_token(token: String) -> Result<Value, String> {
let fansly: tokio::sync::MutexGuard<Fansly> = FANSLY.lock().await;
let response = fansly.check_sync_token(token).await;
match response {
Ok(response) => Ok(response),
Err(e) => Err(e.to_string()),
}
}

View File

@ -0,0 +1,3 @@
pub mod config;
pub mod fansly;
pub mod utils;

View File

@ -0,0 +1,4 @@
#[tauri::command]
pub fn quit(code: i32) {
std::process::exit(code);
}

View File

@ -0,0 +1,185 @@
use serde::{Deserialize, Serialize};
use std::fs::{self, File};
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use crate::structs::{FanslyFollowersResponse, Subscription};
const CURRENT_VERSION: i32 = 2; // Set the current version of the config
#[derive(Debug, Serialize, Deserialize)]
pub struct SyncData {
pub followers: Vec<FanslyFollowersResponse>,
pub subscribers: Vec<Subscription>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Config {
pub version: i32, // Add a version field to the config (1, 2, 3, etc.)
pub is_first_run: bool,
pub fansly_token: String,
pub auto_sync_enabled: bool,
pub sync_token: String,
pub sync_interval: u64,
pub last_sync: u64,
pub last_sync_data: SyncData,
}
impl Default for Config {
fn default() -> Self {
Config {
version: CURRENT_VERSION, // Version is set to CURRENT_VERSION by default
is_first_run: true, // First run is set to true by default
fansly_token: String::new(), // Fansly token is stored as a string
sync_interval: 1, // Every hour - sync interval is interpreted as hours
last_sync: 0, // Last sync time is stored as a UNIX timestamp
auto_sync_enabled: false, // Auto sync is disabled by default
sync_token: String::new(), // Sync token is stored as a string
last_sync_data: SyncData {
followers: Vec::new(),
subscribers: Vec::new(),
}, // Last sync data is stored as a list of followers and subscribers
}
}
}
impl Config {
pub fn load_or_create(path: &Path) -> io::Result<Self> {
if path.exists() {
let config_result: Result<Self, _> =
serde_json::from_str(&std::fs::read_to_string(path)?);
let config = match config_result {
Ok(config) => config,
Err(_) => {
// Load raw JSON and attempt to parse it as a JSON object
let config_raw = std::fs::read_to_string(path)?;
let config_json: serde_json::Value = serde_json::from_str(&config_raw)?;
log::info!("[config::migrate] Migrating config file to latest version...");
log::debug!(
"[config::migrate] [DEBUG] config is_object: {}",
config_json.is_object()
);
// Check if the JSON object is valid, if not, return an error
if !config_json.is_object() {
log::error!(
"[config::migrate] [ERROR] Found invalid JSON object in config file"
);
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Tried to migrate a config file, but found an invalid JSON object",
));
}
// Get the version field from the JSON object
let version = config_json["version"].as_i64().unwrap_or(0) as i32;
// Check if the version field is a valid integer, if not, return an error
if version == 0 {
log::error!(
"[config::migrate] [ERROR] Found invalid version field in config JSON"
);
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Tried to migrate a config file, but found an invalid version field",
));
}
log::info!(
"[config::migrate] Found version field in config JSON: {}",
version
);
// Now create a new Config object and set the version field to the value we found
let mut config = Config::default();
config.version = version;
// Retain important fields from the JSON object
config.is_first_run = config_json["is_first_run"].as_bool().unwrap_or(true);
config.fansly_token = config_json["fansly_token"]
.as_str()
.unwrap_or("")
.to_string();
config.sync_token =
config_json["sync_token"].as_str().unwrap_or("").to_string();
config.sync_interval =
config_json["sync_interval"].as_i64().unwrap_or(1) as u64;
// Run migrations on the config object and save it
config = config.migrate()?;
config.save(path)?;
log::info!(
"[config::migrate] Successfully migrated config file to latest version"
);
// Recursively call load_or_create to load the migrated config
return Config::load_or_create(path);
}
};
if config.version != CURRENT_VERSION {
// Should have been migrated by now, error out because it wasn't
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"Config version mismatch: expected {}, got {}. Please try removing the config file and restarting the application.",
CURRENT_VERSION, config.version
),
));
}
Ok(config)
} else {
let saved_config = Config::default().save(path);
saved_config
.and_then(|_| Config::load_or_create(path))
.or_else(|e| Err(e))
}
}
fn migrate(mut self) -> io::Result<Self> {
while self.version < CURRENT_VERSION {
self = match self.version {
1 => {
// Migrate from version 1 to version 2
self.version = 2;
self.auto_sync_enabled = false;
self.sync_token = String::new();
self.sync_interval = 1;
self
}
_ => {
// If we don't have a migration path, return an error
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("No migration path for version {}", self.version),
));
}
};
}
Ok(self)
}
pub fn save(&self, path: &Path) -> io::Result<()> {
let mut file = File::create(path)?;
file.write_all(serde_json::to_string_pretty(self).unwrap().as_bytes())?;
// Return the saved config
Ok(())
}
}
pub fn get_config_path() -> io::Result<PathBuf> {
let mut config_dir = dirs::config_dir().ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
"Could not determine user's config directory",
)
})?;
config_dir.push("FanslySync");
fs::create_dir_all(&config_dir)?;
config_dir.push("config.json");
Ok(config_dir)
}

View File

@ -0,0 +1,521 @@
use lazy_static::lazy_static;
// Create a simple module for handling the Fansly API, using reqwest to make requests to the API.
// This module will contain a struct Fansly, which will have a method to get the user's profile information.
use crate::structs::{
FanslyAccountResponse, FanslyBaseResponse, FanslyBaseResponseList, FanslyFollowersResponse,
FanslySubscriptionsResponse, Subscription, SyncDataResponse,
};
use reqwest::header::{HeaderMap, HeaderValue, USER_AGENT};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use thiserror::Error;
use tokio::sync::Mutex;
// Create a PROGRESS mutex to hold the current sync progress, lazy initialized
lazy_static! {
pub static ref PROGRESS: Mutex<SyncProgress> = Mutex::new(SyncProgress::default());
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SyncProgress {
// Should contain the current progress of the sync operation
pub current_step: String,
pub percentage_done: u32,
pub current_count: u32,
pub total_count: u32,
pub complete: bool,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PasteData {
id: String,
content: String,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PasteResponse {
error: Option<String>,
payload: PasteData,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
struct PasteRequest {
content: String,
}
pub struct Fansly {
client: reqwest::Client,
token: Option<String>,
}
#[derive(Debug, Error)]
pub enum UploadError {
#[error("HTTP error: {0}")]
Http(#[from] reqwest::Error),
}
impl Fansly {
pub fn new(token: Option<String>) -> Self {
let mut headers = HeaderMap::new();
// Set the user agent to the FanslySync/0.1.0 tanner@fanslycreatorbot.com
headers.insert(
USER_AGENT,
HeaderValue::from_static("FanslySync/0.1.0 tanner@fanslycreatorbot.com"), // this sucks, oh well
);
// If we have a token, add it to the headers\
if let Some(token) = &token {
headers.insert(
"Authorization",
HeaderValue::from_str(&format!("{}", token)).unwrap(),
);
}
// Set our default base url to https://apiv3.fansly.com/api/v1/
let client = reqwest::Client::builder()
.default_headers(headers)
.build()
.unwrap();
Self { client, token }
}
// Helper function to set our token on the fly
pub fn set_token(&mut self, token: Option<String>) {
self.token = token;
// Re-create the client with the new token (if it exists)
let mut headers = HeaderMap::new();
headers.insert(
USER_AGENT,
HeaderValue::from_static("FanslySync/0.1.0 tanner@fanslycreatorbot.com"),
);
// If we have a token, add it to the headers
if let Some(token) = &self.token {
headers.insert(
"Authorization",
HeaderValue::from_str(&format!("{}", token)).unwrap(),
);
}
self.client = reqwest::Client::builder()
.default_headers(headers)
.build()
.unwrap();
}
pub async fn get_profile(
&self,
) -> Result<FanslyBaseResponse<FanslyAccountResponse>, reqwest::Error> {
let response = self
.client
.get("https://apiv3.fansly.com/api/v1/account/me")
.send()
.await?;
if !response.status().is_success() {
log::error!("[sync::process::get_profile] No successful response from API. Setting error state.");
return Err(response.error_for_status().unwrap_err());
} else {
log::info!("[sync::process::get_profile] Successfully fetched profile data.");
}
let profile = response
.json::<FanslyBaseResponse<FanslyAccountResponse>>()
.await?;
// Show the profile data
log::info!("[sync::process::get_profile] Profile data: {:?}", profile);
Ok(profile)
}
async fn fetch_followers(
&self,
account_id: &str,
auth_token: &str,
offset: u32,
) -> Result<FanslyBaseResponseList<FanslyFollowersResponse>, reqwest::Error> {
let url = format!("https://apiv3.fansly.com/api/v1/account/{}/followers?ngsw-bypass=true&limit=100&offset={}", account_id, offset);
let mut headers = reqwest::header::HeaderMap::new();
headers.insert(
reqwest::header::AUTHORIZATION,
format!("{}", auth_token).parse().unwrap(),
);
headers.insert(
reqwest::header::USER_AGENT,
"FanslySync/1.0.0 (tanner@fanslycreatorbot.com)"
.parse()
.unwrap(),
);
headers.insert(
reqwest::header::CONTENT_TYPE,
"application/json".parse().unwrap(),
);
let response = self.client.get(url).headers(headers).send().await?;
if !response.status().is_success() {
log::error!("[sync::process::fetch_followers] No successful response from API. Setting error state.");
return Err(response.error_for_status().unwrap_err());
}
let followers: FanslyBaseResponseList<FanslyFollowersResponse> = response.json().await?;
log::info!(
"[sync::process::fetch_followers] Got {} followers from API.",
followers.response.len()
);
Ok(followers)
}
async fn fetch_subscribers(
&self,
auth_token: &str,
offset: u32,
) -> Result<Vec<Subscription>, reqwest::Error> {
let url = format!("https://apiv3.fansly.com/api/v1/subscribers?status=3,4&limit=100&offset={}&ngsw-bypass=true", offset);
let mut headers = reqwest::header::HeaderMap::new();
headers.insert(
reqwest::header::AUTHORIZATION,
format!("{}", auth_token).parse().unwrap(),
);
headers.insert(
reqwest::header::USER_AGENT,
"FanslySync/1.0.0 (tanner@fanslycreatorbot.com)"
.parse()
.unwrap(),
);
headers.insert(
reqwest::header::CONTENT_TYPE,
"application/json".parse().unwrap(),
);
let response = self.client.get(url).headers(headers).send().await?;
if !response.status().is_success() {
log::error!("[sync::process::fetch_subscribers] No successful response from API. Setting error state.");
let error = response.error_for_status().unwrap_err();
return Err(error);
}
let subscriptions: FanslyBaseResponse<FanslySubscriptionsResponse> =
response.json().await?;
log::info!(
"[sync::process::fetch_subscribers] Got {} subscribers from API.",
subscriptions.response.subscriptions.len()
);
Ok(subscriptions.response.subscriptions)
}
async fn update_progress(
&self,
current_step: impl Into<String>,
curr_count: u32,
total_count: u32,
complete: bool,
) {
let mut p = PROGRESS.lock().await;
p.current_step = current_step.into();
p.current_count = curr_count;
p.total_count = total_count;
p.percentage_done = if total_count > 0 {
curr_count * 100 / total_count
} else {
0
};
p.complete = complete;
}
async fn upload_sync_data(&self, data: SyncDataResponse) -> Result<String, UploadError> {
let url = "https://paste.hep.gg/api/";
// Make an JSON object with our raw data
let paste_data = PasteRequest {
content: serde_json::to_string(&data).unwrap(),
};
let paste_data_str = serde_json::to_string(&paste_data).unwrap();
let est_upload_size = paste_data_str.len() / 1024; // in KB
log::info!(
"Uploading sync data to paste.hep.gg (size: {} KB)",
est_upload_size
);
// Create a new client and POST
let response = self
.client
.post(url)
.body(paste_data_str)
.header("Content-Type", "application/json")
.send()
.await?;
if !response.status().is_success() {
let status_code = response.status();
let err = response.error_for_status_ref().unwrap_err();
let response_text = response
.text()
.await
.unwrap_or_else(|_| "Unknown error".to_string());
log::error!(
"Failed to upload sync data to paste.hep.gg. Status code: {}, Response: {}",
status_code,
response_text
);
return Err(UploadError::Http(err));
}
log::info!("Uploaded sync data successfully.");
// Parse the response
let paste_response: PasteResponse = response.json().await?;
// Return the paste URL
let paste_url = format!("https://paste.hep.gg/api/{}/raw", paste_response.payload.id);
log::info!("Paste URL: {}", paste_url);
Ok(paste_url)
}
pub async fn upload_auto_sync_data(
&self,
data: SyncDataResponse,
token: String,
) -> Result<(), reqwest::Error> {
let url = "https://botapi.fanslycreatorbot.com/sync";
// Set our content type to application/json
let mut headers = reqwest::header::HeaderMap::new();
headers.insert(
reqwest::header::CONTENT_TYPE,
"application/json".parse().unwrap(),
);
// Add our auth token to the headers
headers.insert("Authorization", format!("{}", token).parse().unwrap());
let response = self
.client
.post(url)
.headers(headers)
.json(&data)
.send()
.await?;
if !response.status().is_success() {
log::error!("Failed to upload sync data...");
log::info!("Response: {:?}", response);
return Err(response.error_for_status().unwrap_err());
}
log::info!("Uploaded sync data successfully.");
Ok(())
}
pub async fn check_sync_token(&self, token: String) -> Result<Value, reqwest::Error> {
// Check if the token is valid (GET /checkSyncToken with Authorization header)
// If it is, return the data back from the API
// If it isn't, return an error
let url = "https://botapi.fanslycreatorbot.com/checkSyncToken";
// Set our content type to application/json
let mut headers = reqwest::header::HeaderMap::new();
headers.insert(
reqwest::header::CONTENT_TYPE,
"application/json".parse().unwrap(),
);
// Add our auth token to the headers
headers.insert("Authorization", format!("{}", token).parse().unwrap());
let response = self.client.get(url).headers(headers).send().await;
// If successful, return the data, otherwise return an error
match response {
Ok(response) => {
if !response.status().is_success() {
log::error!("Failed to check sync token...");
log::info!("Response: {:?}", response);
return Err(response.error_for_status().unwrap_err());
}
let json: serde_json::Value = response.json().await?;
Ok(json)
}
Err(e) => Err(e),
}
}
pub async fn sync(&mut self, auto: bool) -> Result<SyncDataResponse, String> {
// Reset progress
self.update_progress("Starting Sync".to_string(), 0, 100, false)
.await;
// Fetch profile
log::info!("[sync::process] Fetching profile...");
let profile = self.get_profile().await.map_err(|e| e.to_string())?;
if !profile.success {
return Err("Failed to fetch profile".to_string());
}
log::info!("[sync::process] Syncing profile...");
let account = profile.response.account;
let total_followers = account.follow_count;
let total_subscribers = account.subscriber_count;
log::info!(
"[sync::process] Account ID: {}, Followers: {}, Subscribers: {}",
account.id,
total_followers,
total_subscribers
);
let mut followers: Vec<String> = Vec::new();
let mut subscribers: Vec<Subscription> = Vec::new();
log::info!("[sync::process] Fetching followers...");
// Fetch followers until we have all of them
let mut offset = 0;
let mut total_requests = 0;
while followers.len() < total_followers as usize {
log::info!(
"[sync::process] Fetching followers for account {} with offset {} (total: {})",
account.id,
offset,
total_followers
);
let response = self
.fetch_followers(&account.id, &self.token.as_ref().unwrap(), offset)
.await
.map_err(|e| e.to_string())?;
log::info!(
"[sync::process] Got {} followers from API.",
response.response.len()
);
// Collect followers
for follower in response.response.clone() {
followers.push(follower.follower_id);
}
offset += 100;
total_requests += 1;
// Update progress
self.update_progress(
"Fetching Followers".to_string(),
followers.len() as u32,
total_followers as u32,
false,
)
.await;
// Every 10 requests, sleep for a bit to avoid rate limiting
if total_requests % 50 == 0 {
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
}
// If we've received no followers, break the loop
if response.clone().response.is_empty() {
log::info!("[sync::process] No more followers found, breaking the loop.");
break;
}
}
// Fetch subscribers until we have all of them
offset = 0;
while subscribers.len() < total_subscribers as usize {
log::info!(
"[sync::process] Fetching subscribers with offset {} for account {} (total: {})",
offset,
account.id,
total_subscribers
);
let response = self
.fetch_subscribers(&self.token.as_ref().unwrap(), offset)
.await
.map_err(|e| e.to_string())?;
subscribers.extend(response.clone());
offset += 100;
total_requests += 1;
// Update progress
self.update_progress(
"Fetching Subscribers".to_string(),
subscribers.len() as u32,
total_subscribers as u32,
false,
)
.await;
// Every 10 requests, sleep for a bit to avoid rate limiting
if total_requests % 50 == 0 {
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
}
// If we've received no subscribers, break the loop
if response.is_empty() {
log::info!("[sync::process] No more subscribers found, breaking the loop.");
break;
}
}
log::info!(
"[sync::process] Got {} followers and {} subscribers from API.",
followers.len(),
subscribers.len()
);
log::info!("[sync::process] Sync complete.");
// Reset progress
self.update_progress("Sync Complete".to_string(), 100, 100, true)
.await;
log::info!("[sync::process] Uploading sync data to paste.hep.gg for processing...");
// Upload sync data to paste.hep.gg
if !auto {
let paste_url = self
.upload_sync_data(SyncDataResponse {
followers: followers.clone(),
subscribers: subscribers.clone(),
sync_data_url: "".to_string(),
})
.await
.map_err(|e| e.to_string())?;
// Return JSON of what we fetched
Ok(SyncDataResponse {
followers,
subscribers,
sync_data_url: paste_url,
})
} else {
// Return JSON of what we fetched
Ok(SyncDataResponse {
followers,
subscribers,
sync_data_url: "".to_string(),
})
}
}
}

View File

@ -0,0 +1,2 @@
pub mod config;
pub mod fansly;

136
src-tauri/src/main.rs Normal file
View File

@ -0,0 +1,136 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
mod commands;
mod handlers;
mod structs;
use std::fs;
use std::io;
use commands::config::{get_config, init_config, save_config};
use commands::fansly::{
fansly_check_sync_token, fansly_get_me, fansly_get_sync_status, fansly_set_token, fansly_sync,
fansly_upload_auto_sync_data,
};
use commands::utils::quit;
use tauri::menu::Menu;
use tauri::menu::MenuItem;
use tauri::tray::TrayIconBuilder;
use tauri::AppHandle;
use tauri::Manager;
use tauri_plugin_autostart::MacosLauncher;
use tauri_plugin_dialog::DialogExt;
use tauri_plugin_dialog::MessageDialogKind;
use tauri_plugin_log::{Target, TargetKind};
fn get_log_path() -> io::Result<String> {
let mut config_dir = dirs::config_dir().ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
"Could not determine user's config directory",
)
})?;
config_dir.push("FanslySync");
fs::create_dir_all(&config_dir)?;
config_dir.push("runtime");
// Return the path as a string
Ok(config_dir.to_string_lossy().to_string())
}
fn handle_menu(app: &tauri::AppHandle, event: &tauri::menu::MenuEvent) {
match event.id().as_ref() {
"quit" => {
app.exit(0);
}
"show_window" => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
}
_ => {}
}
}
#[tokio::main]
async fn main() {
tauri::Builder::default()
.plugin(tauri_plugin_process::init())
.setup(|app| {
// Setup menu items for the tray
let quit_i = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;
let show_window_i =
MenuItem::with_id(app, "show_window", "Show Window", true, None::<&str>)?;
// Create our Menu and add the items to it
let menu = Menu::with_items(app, &[&quit_i, &show_window_i])?;
// Create our Tray using TrayIconBuilder and add the menu to it
TrayIconBuilder::new()
.icon(app.default_window_icon().unwrap().clone())
.title("FanslySync")
.tooltip("FanslySync")
.menu(&menu)
.show_menu_on_left_click(true)
.on_menu_event(|app: &AppHandle, event: tauri::menu::MenuEvent| {
handle_menu(app, &event)
})
.build(app)?;
Ok(())
})
.on_window_event(|app, event| {
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
if let Some(window) = app.get_webview_window("main") {
let _ = window.hide();
api.prevent_close();
}
}
})
.plugin(tauri_plugin_autostart::init(
MacosLauncher::LaunchAgent,
None,
))
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(
tauri_plugin_log::Builder::new()
.targets([
Target::new(TargetKind::Stdout),
Target::new(TargetKind::LogDir {
file_name: Some(get_log_path().unwrap()),
}),
Target::new(TargetKind::Webview),
])
.rotation_strategy(tauri_plugin_log::RotationStrategy::KeepOne)
.max_file_size(1024 * 1024 * 5)
.build(),
)
.plugin(tauri_plugin_single_instance::init(|app,_args,_cwd| {
// Show a dialog if the app is already running
app.dialog()
.message("FanslySync is already running in the background. Please left click the tray icon -> Show Window to open the app.")
.title("FanslySync")
.kind(MessageDialogKind::Warning)
.blocking_show();
}))
.invoke_handler(tauri::generate_handler![
init_config,
get_config,
save_config,
quit,
fansly_set_token,
fansly_get_me,
fansly_sync,
fansly_upload_auto_sync_data,
fansly_check_sync_token,
fansly_get_sync_status
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View File

@ -0,0 +1,183 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SyncDataResponse {
pub followers: Vec<String>,
pub subscribers: Vec<Subscription>,
pub sync_data_url: String,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FanslyBaseResponse<T> {
pub success: bool,
pub response: T,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FanslyBaseResponseList<T> {
pub success: bool,
pub response: Vec<T>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FanslyFollowersResponse {
pub follower_id: String,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FanslySubscriptionsResponse {
pub stats: SubscriptionsStats,
pub subscriptions: Vec<Subscription>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SubscriptionsStats {
pub total_active: i64,
pub total_expired: i64,
pub total: i64,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Subscription {
pub id: String,
pub history_id: String,
pub subscriber_id: String,
pub subscription_tier_id: String,
pub subscription_tier_name: String,
pub subscription_tier_color: String,
pub plan_id: String,
pub promo_id: Option<String>,
pub gift_code_id: Value,
pub payment_method_id: String,
pub status: i64,
pub price: i64,
pub renew_price: i64,
pub renew_correlation_id: String,
pub auto_renew: i64,
pub billing_cycle: i64,
pub duration: i64,
pub renew_date: i64,
pub version: i64,
pub created_at: i64,
pub updated_at: i64,
pub ends_at: i64,
pub promo_price: Value,
pub promo_duration: Value,
pub promo_status: Value,
pub promo_starts_at: Value,
pub promo_ends_at: Value,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FanslyAccountResponse {
pub account: Account,
pub correlation_id: String,
pub check_token: Value,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Account {
pub id: String,
pub email: String,
pub username: String,
pub display_name: Option<String>,
pub flags: i64,
pub version: i64,
pub created_at: i64,
pub follow_count: i64,
pub subscriber_count: i64,
pub permissions: Permissions,
pub timeline_stats: TimelineStats,
pub profile_access_flags: i64,
pub profile_flags: i64,
pub about: String,
pub location: String,
pub profile_socials: Vec<Value>,
pub status_id: i64,
pub last_seen_at: i64,
pub post_likes: i64,
pub streaming: Streaming,
pub account_media_likes: i64,
pub subscription_tiers: Vec<SubscriptionTier>,
pub profile_access: bool,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Permissions {
pub account_permission_flags: AccountPermissionFlags,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AccountPermissionFlags {
pub flags: i64,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TimelineStats {
pub account_id: String,
pub image_count: i64,
pub video_count: i64,
pub bundle_count: i64,
pub bundle_image_count: i64,
pub bundle_video_count: i64,
pub fetched_at: i64,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MainWallet {
pub id: String,
pub account_id: String,
pub balance: i64,
#[serde(rename = "type")]
pub type_field: i64,
pub wallet_version: i64,
pub flags: i64,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Streaming {
pub account_id: String,
pub channel: Value,
pub enabled: bool,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SubscriptionTier {
pub id: String,
pub account_id: String,
pub name: String,
pub color: String,
pub pos: i64,
pub price: i64,
pub max_subscribers: i64,
pub subscription_benefits: Vec<String>,
pub included_tier_ids: Vec<Value>,
pub plans: Vec<Plan>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Plan {
pub id: String,
pub status: i64,
pub billing_cycle: i64,
pub price: i64,
pub use_amounts: i64,
pub promos: Vec<Value>,
pub uses: i64,
}

71
src-tauri/tauri.conf.json Normal file
View File

@ -0,0 +1,71 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"build": {
"beforeBuildCommand": "npm run build",
"beforeDevCommand": "npm run dev",
"frontendDist": "../build",
"devUrl": "http://localhost:5173"
},
"bundle": {
"active": true,
"category": "DeveloperTool",
"copyright": "",
"targets": "all",
"externalBin": [],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": ""
},
"longDescription": "",
"macOS": {
"entitlements": null,
"exceptionDomain": "",
"frameworks": [],
"providerShortName": null,
"signingIdentity": null
},
"resources": [],
"shortDescription": "",
"linux": {
"deb": {
"depends": []
}
},
"createUpdaterArtifacts": true
},
"productName": "FanslySync",
"version": "0.2.0",
"identifier": "com.fanslycreatorbot.fanslysync",
"plugins": {
"updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDJFODZGRDI4NjBFMDQ1RUMKUldUc1JlQmdLUDJHTGdRdSt6dWFISXE0MThsa0tvUDA2RWdMSStjQ0J6NVBhdmU4ajRMMms4a1cK",
"active": true,
"endpoints": [
"https://cdn.crabnebula.app/update/fansly-creator-bot/fansly-sync/{{target}}-{{arch}}/{{current_version}}"
],
"dialog": true
}
},
"app": {
"windows": [
{
"fullscreen": false,
"height": 650,
"resizable": false,
"title": "FanslySync",
"width": 630
}
],
"security": {
"csp": null
}
}
}

3
src/app.css Normal file
View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

13
src/app.d.ts vendored Normal file
View File

@ -0,0 +1,13 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

11
src/app.html Normal file
View File

@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

1
src/lib/index.ts Normal file
View File

@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.

252
src/lib/types.ts Normal file
View File

@ -0,0 +1,252 @@
export type Config = {
version: number;
is_first_run: boolean;
fansly_token: string;
auto_sync_enabled: boolean;
sync_token: string;
sync_interval: number;
last_sync: number;
last_sync_data: SyncData;
};
export interface SyncData {
followers: string[];
subscribers: Subscriber[];
sync_data_url: string;
}
interface Subscriber {
id: string;
historyId: string;
subscriberId: string;
subscriptionTierId: string;
subscriptionTierName: string;
subscriptionTierColor: string;
planId: string;
promoId: null | string;
giftCodeId: null | string;
paymentMethodId: string;
status: number;
price: number;
renewPrice: number;
renewCorrelationId: string;
autoRenew: number;
billingCycle: number;
duration: number;
renewDate: number;
version: number;
createdAt: number;
updatedAt: number;
endsAt: number;
promoPrice: null | number;
promoDuration: null | number;
promoStatus: null | number;
promoStartsAt: null | number;
promoEndsAt: null | number;
}
export interface AccountInfoResponse {
success: boolean;
response: AccountInfo[];
}
export interface AccountInfo {
id: string;
username: string;
displayName: string | null;
flags: number;
version: number;
createdAt: number;
followCount: number;
subscriberCount: number;
permissions: Permissions;
profileAccessFlags: number;
profileFlags: number;
about: string;
location: string;
profileSocials: ProfileSocial[];
pinnedPosts: PinnedPost[];
walls: Wall[];
timelineStats: TimelineStats;
statusId: number;
lastSeenAt: number;
mediaStoryState: MediaStoryState;
accountMediaLikes: number;
avatar: Avatar;
banner: Avatar;
postLikes: number;
streaming: Streaming;
}
export interface SubscriptionTier {
id: string;
accountId: string;
name: string;
color: string;
pos: number;
price: number;
maxSubscribers: number;
subscriptionBenefits: string[];
includedTierIds: string[];
plans: Plan[];
}
interface Plan {
id: string;
status: number;
billingCycle: number;
price: number;
useAmounts: number;
promos: Promo[];
uses: number;
}
interface Promo {
id: string;
status: number;
price: number;
duration: number;
maxUses: number;
maxUsesBefore?: unknown;
newSubscribersOnly: number;
startsAt: number;
endsAt: number;
uses: number;
}
interface Streaming {
accountId: string;
channel: Channel;
enabled: boolean;
}
interface Channel {
id: string;
accountId: string;
playbackUrl: string;
chatRoomId: string;
status: number;
version: number;
createdAt: number;
updatedAt?: unknown;
stream: Stream;
arn?: unknown;
ingestEndpoint?: unknown;
}
interface Stream {
id: string;
historyId: string;
channelId: string;
accountId: string;
title: string;
status: number;
viewerCount: number;
version: number;
createdAt: number;
updatedAt?: unknown;
lastFetchedAt: number;
startedAt: number;
permissions: Permissions2;
}
interface Permissions2 {
permissionFlags: PermissionFlag[];
}
interface PermissionFlag {
id: string;
streamId: string;
type: number;
flags: number;
price: number;
metadata: string;
}
interface Avatar {
id: string;
type: number;
status: number;
accountId: string;
mimetype: string;
flags: number;
location: string;
width: number;
height: number;
metadata: string;
updatedAt: number;
createdAt: number;
variants: Variant[];
variantHash: VariantHash;
locations: Location[];
}
type VariantHash = unknown;
interface Variant {
id: string;
type: number;
status: number;
mimetype: string;
flags: number;
location: string;
width: number;
height: number;
metadata: string;
updatedAt: number;
locations: Location[];
}
interface Location {
locationId: string;
location: string;
}
interface MediaStoryState {
accountId: string;
status: number;
storyCount: number;
version: number;
createdAt: number;
updatedAt: number;
hasActiveStories: boolean;
}
interface TimelineStats {
accountId: string;
imageCount: number;
videoCount: number;
bundleCount: number;
bundleImageCount: number;
bundleVideoCount: number;
fetchedAt: number;
}
interface Wall {
id: string;
accountId: string;
pos: number;
name: string;
description: string;
metadata: string;
}
interface PinnedPost {
postId: string;
accountId: string;
pos: number;
createdAt: number;
}
interface ProfileSocial {
providerId: string;
handle: string;
}
interface Permissions {
accountPermissionFlags: AccountPermissionFlags;
}
interface AccountPermissionFlags {
flags: number;
}

9
src/lib/utils.ts Normal file
View File

@ -0,0 +1,9 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const awaiter = async <T>(promise: Promise<T>): Promise<[T | null, any | null]> => {
try {
const data: T = await promise;
return [data, null];
} catch (err) {
return [null, err];
}
};

Some files were not shown because too many files have changed in this diff Show More