Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,24 @@ class ReferencedAssetLoader {

scope.launch {
try {
val bytes = dataSource.createLoader().load(dataSource)
val bytes = when (dataSource) {
is DataSource.Http -> {
// Check cache first for URL assets
val cachedData = URLAssetCache.getCachedData(dataSource.url)
if (cachedData != null) {
cachedData
} else {
// Download and cache
val downloadedData = dataSource.createLoader().load(dataSource)
URLAssetCache.saveToCache(dataSource.url, downloadedData)
downloadedData
}
}
else -> {
// For non-URL assets, use the loader directly
dataSource.createLoader().load(dataSource)
}
}
withContext(Dispatchers.Main) {
processAssetBytes(bytes, asset)
deferred.complete(Unit)
Expand Down Expand Up @@ -82,19 +99,23 @@ class ReferencedAssetLoader {
return object : FileAssetLoader() {
override fun loadContents(asset: FileAsset, inBandBytes: ByteArray): Boolean {
var key = asset.uniqueFilename.substringBeforeLast(".")
cache[key] = asset
cache[asset.name] = asset
var assetData = assetsData[key]

if (assetData == null) {
key = asset.name
assetData = assetsData[asset.name]
}

if (assetData == null && asset.cdnUrl != null) {
assetData = ResolvedReferencedAsset(sourceUrl = asset.cdnUrl, sourceAsset = null, image = null, sourceAssetId = null, path = null)
}

if (assetData == null) {
return false
}

cache[key] = asset

loadAsset(assetData, asset)

return true
Expand Down
65 changes: 65 additions & 0 deletions android/src/main/java/com/margelo/nitro/rive/URLAssetCache.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.margelo.nitro.rive

import android.content.Context
import android.util.Log
import com.margelo.nitro.NitroModules
import java.io.File
import java.security.MessageDigest

object URLAssetCache {
private const val CACHE_DIR_NAME = "rive_url_assets"
private const val TAG = "URLAssetCache"

private fun getCacheDir(): File? {
val context = NitroModules.applicationContext ?: return null
val cacheDir = File(context.cacheDir, CACHE_DIR_NAME)
if (!cacheDir.exists()) {
cacheDir.mkdirs()
}
return cacheDir
}

private fun urlToCacheKey(url: String): String {
val digest = MessageDigest.getInstance("SHA-256")
val hashBytes = digest.digest(url.toByteArray())
return hashBytes.joinToString("") { "%02x".format(it) }
}

private fun getCacheFile(url: String): File? {
val cacheDir = getCacheDir() ?: return null
val cacheKey = urlToCacheKey(url)
return File(cacheDir, cacheKey)
}

fun getCachedData(url: String): ByteArray? {
return try {
val cacheFile = getCacheFile(url) ?: return null
if (cacheFile.exists() && cacheFile.length() > 0) {
cacheFile.readBytes()
} else {
null
}
} catch (e: Exception) {
Log.e(TAG, "Failed to read from cache: ${e.message}")
null
}
}

fun saveToCache(url: String, data: ByteArray) {
try {
val cacheFile = getCacheFile(url) ?: return
cacheFile.writeBytes(data)
} catch (e: Exception) {
Log.e(TAG, "Failed to save to cache: ${e.message}")
}
}

fun clearCache() {
try {
val cacheDir = getCacheDir() ?: return
cacheDir.listFiles()?.forEach { it.delete() }
} catch (e: Exception) {
Log.e(TAG, "Failed to clear cache: ${e.message}")
}
}
}
43 changes: 36 additions & 7 deletions ios/ReferencedAssetLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,23 @@ final class ReferencedAssetLoader {

Task {
do {
let data = try await dataSource.createLoader().load(from: dataSource)
let data: Data

// Check cache first for URL assets
if case .http(let url) = dataSource {
if let cachedData = URLAssetCache.getCachedData(for: url.absoluteString) {
data = cachedData
} else {
// Download and cache
let downloadedData = try await dataSource.createLoader().load(from: dataSource)
URLAssetCache.saveToCache(downloadedData, for: url.absoluteString)
data = downloadedData
}
} else {
// For non-URL assets, use the loader directly
data = try await dataSource.createLoader().load(from: dataSource)
}

await MainActor.run {
self.processAssetBytes(data, asset: asset, factory: factory, completion: completion)
}
Expand All @@ -120,19 +136,32 @@ final class ReferencedAssetLoader {
)
-> LoadAsset?
{
guard let referencedAssets = referencedAssets, let referencedAssets = referencedAssets.data
guard let referencedAssets = referencedAssets, let assetsData = referencedAssets.data
else {
return nil
}
return { (asset: RiveFileAsset, _: Data, factory: RiveFactory) -> Bool in
let assetByUniqueName = referencedAssets[asset.uniqueName()]
guard let assetData = assetByUniqueName ?? referencedAssets[asset.name()] else {
return false
}

cache.value[asset.uniqueName()] = asset
cache.value[asset.name()] = asset
factoryOut.value = factory

// Look up asset data by unique name or name
var key = (asset.uniqueName() as NSString).deletingPathExtension
var assetData = assetsData[key]

if assetData == nil {
key = asset.name()
assetData = assetsData[key]
}

if assetData == nil && !asset.cdnUuid().isEmpty {
assetData = ResolvedReferencedAsset(sourceUrl: "\(asset.cdnBaseUrl())/\(asset.cdnUuid())", sourceAsset: nil, sourceAssetId: nil, path: nil, image: nil)
}

guard let assetData = assetData else {
return false
}

self.loadAssetInternal(
source: assetData, asset: asset, factory: factory,
completion: {
Expand Down
71 changes: 71 additions & 0 deletions ios/URLAssetCache.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import Foundation
import CryptoKit

enum URLAssetCache {
private static let cacheDirName = "rive_url_assets"

private static func getCacheDirectory() -> URL? {
guard let cacheDir = FileManager.default.urls(
for: .cachesDirectory,
in: .userDomainMask
).first else {
return nil
}
let riveCacheDir = cacheDir.appendingPathComponent(cacheDirName)

// Create directory if it doesn't exist
try? FileManager.default.createDirectory(
at: riveCacheDir,
withIntermediateDirectories: true,
attributes: nil
)

return riveCacheDir
}

private static func urlToCacheKey(_ url: String) -> String {
let data = Data(url.utf8)
let hash = SHA256.hash(data: data)
return hash.compactMap { String(format: "%02x", $0) }.joined()
}

private static func getCacheFileURL(for url: String) -> URL? {
guard let cacheDir = getCacheDirectory() else { return nil }
let cacheKey = urlToCacheKey(url)
return cacheDir.appendingPathComponent(cacheKey)
}

static func getCachedData(for url: String) -> Data? {
guard let cacheFileURL = getCacheFileURL(for: url) else { return nil }

do {
let data = try Data(contentsOf: cacheFileURL)
return data.isEmpty ? nil : data
} catch {
return nil
}
}

static func saveToCache(_ data: Data, for url: String) {
guard let cacheFileURL = getCacheFileURL(for: url) else { return }

do {
try data.write(to: cacheFileURL)
} catch {
// Silently fail - caching is best effort
}
}

static func clearCache() {
guard let cacheDir = getCacheDirectory() else { return }

do {
let files = try FileManager.default.contentsOfDirectory(at: cacheDir, includingPropertiesForKeys: nil)
for file in files {
try? FileManager.default.removeItem(at: file)
}
} catch {
// Silently fail
}
}
}
Loading