Files
DIYgod bfbce383a2 release(desktop): Release v1.6.0 (#4970)
* docs(mobile): prepare release changelog

* release(mobile): release v0.4.1

* fix(ssr): route dist-external assets (#4956)

* fix(cli): close login callback sockets (#4957)

* fix(ci): sync release branches to dev via pull request

* fix(mobile): increase ai summary font size

* fix(desktop): improve pdf export layout and branding

* fix(mobile): refresh social login state

* fix: correct French translation language detection

* build(deps): bump android-actions/setup-android from 3 to 4 (#4929)

Bumps [android-actions/setup-android](https://github.com/android-actions/setup-android) from 3 to 4.
- [Release notes](https://github.com/android-actions/setup-android/releases)
- [Commits](https://github.com/android-actions/setup-android/compare/v3...v4)

---
updated-dependencies:
- dependency-name: android-actions/setup-android
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* fix(ai-chat): show reset date for future credits

* feat: refine spotlight settings and highlighting

* feat: add spotlight settings across desktop and mobile

* build(deps): bump actions/github-script from 8 to 9 (#4961)

Bumps [actions/github-script](https://github.com/actions/github-script) from 8 to 9.
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](https://github.com/actions/github-script/compare/v8...v9)

---
updated-dependencies:
- dependency-name: actions/github-script
  dependency-version: '9'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps): bump signpath/github-action-submit-signing-request (#4960)

Bumps [signpath/github-action-submit-signing-request](https://github.com/signpath/github-action-submit-signing-request) from 2.0 to 2.1.
- [Release notes](https://github.com/signpath/github-action-submit-signing-request/releases)
- [Commits](https://github.com/signpath/github-action-submit-signing-request/compare/v2.0...v2.1)

---
updated-dependencies:
- dependency-name: signpath/github-action-submit-signing-request
  dependency-version: '2.1'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* fix(mobile): avoid YouTube embed error on iOS close #4958

* fix(mobile): patch fmt for Xcode 26.4 builds

* build(deps): bump pnpm/action-setup from 4 to 5 (#4922)

Bumps [pnpm/action-setup](https://github.com/pnpm/action-setup) from 4 to 5.
- [Release notes](https://github.com/pnpm/action-setup/releases)
- [Commits](https://github.com/pnpm/action-setup/compare/v4...v5)

---
updated-dependencies:
- dependency-name: pnpm/action-setup
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* feat(desktop): migrate tts to folo service

* docs(ota): add OTA service design spec

* feat(ota): scaffold Cloudflare worker app

* fix(ota): align scaffold with task spec

* fix(ota): use standard worker types

* fix(ota): tighten scaffold schema and dev config

* fix(ota): require at least one platform

* fix(ota): use dev env for local wrangler

* docs(ota): add implementation plan and release scoping

* chore(ota): update lockfile for worker deps

* feat(ota): add release selection primitives

* test(ota): cover incompatible newer release selection

* fix(ota): tighten release compatibility checks

* fix(ota): reject empty platform payloads

* feat(ota): add kv helpers and policy logic

* fix(ota): enforce min binary store policy

* feat(ota): fetch OTA releases from GitHub

* fix(ota): return GitHub release etags

* feat(ota): mirror release payloads into R2

* fix(ota): stream mirrored archive extraction

* fix(ota): verify mirrored asset hashes

* feat(ota): serve manifests assets and policy

* fix(ota): harden manifest and policy routes

* fix(ota): validate route semver inputs

* fix(ota): verify manifest pointer version

* feat(ota): add sync orchestration routes

* fix(ota): dedupe concurrent sync passes

* feat(mobile): enable OTA background updates

* fix(mobile): restore OTA provider typecheck

* feat(mobile): add streaming tts playback

* fix(mobile): align OTA provider state

* feat(mobile): add ota policy client and debug controls

* fix(mobile): harden ota policy and debug controls

* test(mobile): cover ota provider helpers

* ci(desktop): automate Mac App Store pkg upload

* feat(ota): generate release assets from mobile export

* test(mobile): fix ota provider test typing

* test(mobile): discover ota script test via explicit config

* fix: tighten OTA release asset metadata handling

* fix: require OTA platform asset arrays

* ci(ota): publish releases and trigger sync

* fix(ci): harden ota publish workflow

* ci(ota): tighten trailer validation

* docs(ota): add rollout and rollback runbook

* feat(mobile): upgrade expo sdk 55 and rnfirebase

* fix(ota): emit expo-compatible update identifiers

* fix(ota): support private GitHub release assets

* fix(mobile): unblock ios archive on expo sdk 55

* build(deps): bump pnpm/action-setup from 5 to 6 (#4962)

Bumps [pnpm/action-setup](https://github.com/pnpm/action-setup) from 5 to 6.
- [Release notes](https://github.com/pnpm/action-setup/releases)
- [Commits](https://github.com/pnpm/action-setup/compare/v5...v6)

---
updated-dependencies:
- dependency-name: pnpm/action-setup
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* fix(ci): restore ota typing and tailwind config compatibility

* test(mobile): fix ota provider test typing

* chore(ota): persist production cloudflare bindings

* fix(ci): restore semver types and ota test mocks

* feat(release): drive mobile release mode from config files

* fix(ota): sign manifests and pin runtime version

* fix(ci): disable frozen lockfile for local eas builds

* docs(spec): add desktop ota unification design

* feat(ota): unify desktop updates with ota service

* fix(ota): harden desktop sync and updater flow

* docs(skill): update desktop release workflow

* fix(release): repair ota and desktop update flows

* feat: cache storefront versions for ota policy

* feat: expose ota version badges

* feat: expose ota version badges

* fix: backfill github version badges

* fix: backfill github version badges

* docs(skills): align release modes with implementation

* fix: update Discord invite link in README.md

* fix: update macOS and Windows app store links in README.md

* fix: update GitHub badge labels in README.md

* build(deps): bump actions/download-artifact from 5 to 8 (#4969)

Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 5 to 8.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v5...v8)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps): bump softprops/action-gh-release from 2 to 3 (#4968)

Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2 to 3.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/v2...v3)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* docs(release): prepare desktop and mobile release metadata

* release(desktop): release v1.6.0

* fix(desktop): restore spotlight settings sync

* fix(desktop): apply spotlight highlights in entry rendering

* fix(desktop): extend spotlight highlight coverage

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-14 12:49:15 +08:00

194 lines
6.6 KiB
Swift

//
// HelperModule.swift
// Pods
//
// Created by Innei on 2025/2/7.
//
import ExpoModulesCore
import UIKit
public class HelperModule: Module {
public func definition() -> ExpoModulesCore.ModuleDefinition {
Name("Helper")
AsyncFunction("openLink") { (urlString: String, promise: Promise) in
guard let url = URL(string: urlString) else {
return
}
DispatchQueue.main.async {
guard let rootVC = Utils.getRootVC() else { return }
let onDismiss = {
promise.resolve(["type": "dismiss"])
}
WebViewManager.presentModalWebView(url: url, from: rootVC, onDismiss: onDismiss)
}
}
Function("scrollToTop") { (reactTag: Int) in
DispatchQueue.main.async { [weak self] in
if let sourceView = self?.appContext?.findView(withTag: reactTag, ofType: UIView.self) {
let scrollView = self?.findUIScrollView(view: sourceView)
guard let scrollView = scrollView else {
return
}
scrollView.scrollToTopIfPossible(animated: true)
}
}
}
AsyncFunction("isScrollToEnd") { (reactTag: Int, promise: Promise) in
DispatchQueue.main.async { [weak self] in
if let sourceView = self?.appContext?.findView(withTag: reactTag, ofType: UIView.self) {
let scrollView = self?.findUIScrollView(view: sourceView)
guard let scrollView = scrollView else {
return
}
let contentHeight = scrollView.contentSize.height
let scrollViewHeight = scrollView.bounds.size.height
let contentOffsetY = scrollView.contentOffset.y
let bottomOffset = contentHeight - scrollViewHeight
promise.resolve(contentOffsetY >= bottomOffset - 1.0)
}
}
}
Function("saveImageByHandle") { (reactTag: Int) in
DispatchQueue.main.async { [weak self] in
if let sourceView = self?.appContext?.findView(withTag: reactTag, ofType: UIView.self) {
let imageView = self?.findUIImageView(view: sourceView)
guard let imageView = imageView else {
return
}
guard let image = imageView.image else { return }
UIImageWriteToSavedPhotosAlbum(image, self, #selector(HelperModule.image), nil)
Toast.show(options: .init(type: .success, title: "Saved to photos"))
}
}
}
Function("shareImageByHandle") { (reactTag: Int, url: String?) in
DispatchQueue.main.async { [weak self] in
if let sourceView = self?.appContext?.findView(withTag: reactTag, ofType: UIView.self) {
let imageView = self?.findUIImageView(view: sourceView)
guard let imageView = imageView else {
return
}
guard let image = imageView.image else { return }
let activityViewController = UIActivityViewController(
activityItems: [
image.asActivityItemSource(
url: try? URL(string: url ?? "")
)
], applicationActivities: nil)
activityViewController.popoverPresentationController?.sourceView = sourceView
activityViewController.popoverPresentationController?.sourceRect = sourceView.bounds
activityViewController.popoverPresentationController?.permittedArrowDirections = .any
activityViewController.popoverPresentationController?.permittedArrowDirections = .any
Utils.getRootVC()?.present(activityViewController, animated: true)
}
}
}
AsyncFunction("getBase64FromImageViewByHandle") { (reactTag: Int, promise: Promise) in
DispatchQueue.main.async { [weak self] in
if let sourceView = self?.appContext?.findView(withTag: reactTag, ofType: UIView.self) {
let imageView = self?.findUIImageView(view: sourceView)
guard let imageView = imageView else {
promise.reject(
NSError(
domain: "HelperModule", code: 0,
userInfo: [NSLocalizedDescriptionKey: "Image view not found"]))
return
}
let base64 = self?.getBase64FromImageView(imageView: imageView)
promise.resolve(["base64": base64])
}
}
}
Function("copyImageByHandle") { (reactTag: Int) in
DispatchQueue.main.async { [weak self] in
if let sourceView = self?.appContext?.findView(withTag: reactTag, ofType: UIView.self) {
let imageView = self?.findUIImageView(view: sourceView)
guard let imageView = imageView else {
return
}
guard let image = imageView.image else { return }
guard let imageData = image.pngData() else { return }
UIPasteboard.general.setData(imageData, forPasteboardType: "public.png")
Toast.show(options: .init(type: .success, title: "Image copied to clipboard"))
}
}
}
}
func getBase64FromImageView(imageView: UIImageView) -> String? {
guard let image = imageView.image else { return nil }
guard let imageData = image.pngData() else { return nil }
let base64String = imageData.base64EncodedString(options: .lineLength64Characters)
return base64String
}
@objc func image(
_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer?
) {
if let error = error {
Toast.show(options: .init(type: .error, title: "Save image failed"))
} else {
Toast.show(options: .init(type: .success, title: "Saved to photos"))
}
}
private func findUIScrollView(view: UIView?) -> UIScrollView? {
return findUIViewOfType(view: view)
}
private func findUIImageView(view: UIView?) -> UIImageView? {
return findUIViewOfType(view: view)
}
private func findUIViewOfType<T: UIView>(view: UIView?) -> T? {
guard let view = view else {
return nil
}
if let view = view as? T {
return view
}
let subviews = view.subviews
for subview in subviews {
if let targetView = findUIViewOfType(view: subview) as T? {
return targetView
}
}
return nil
}
}
extension UIScrollView {
func scrollToTopIfPossible(animated: Bool) {
let encodedSelector = "X3Njcm9sbFRvVG9wSWZQb3NzaWJsZTo=" // "_scrollToTopIfPossible:"
if let decodedData = Data(base64Encoded: encodedSelector),
let decodedString = String(data: decodedData, encoding: .utf8)
{
let selector = NSSelectorFromString(decodedString)
if responds(to: selector) {
perform(selector, with: animated)
} else {
print("UIScrollView does not respond to decoded method")
setContentOffset(.zero, animated: animated)
}
} else {
setContentOffset(.zero, animated: animated)
}
}
}