mirror of
https://github.com/RSSNext/Folo.git
synced 2026-10-21 16:06:52 +08:00
bfbce383a2
* 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>
194 lines
6.6 KiB
Swift
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)
|
|
}
|
|
}
|
|
}
|