mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-07-10 14:04:41 +02:00
Fix crash when probing audio files by updating start/end chapter data type to Long #128, remove unused files and generic database save/load methods
This commit is contained in:
parent
d5b69be7c1
commit
52fd8ac5e8
11 changed files with 33 additions and 108 deletions
|
@ -2,11 +2,6 @@ package com.audiobookshelf.app.data
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||||
import com.fasterxml.jackson.annotation.JsonInclude
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
|
||||||
import com.fasterxml.jackson.core.json.JsonReadFeature
|
|
||||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
|
||||||
import com.fasterxml.jackson.databind.jsonschema.JsonSerializableSchema
|
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class AudioProbeStream(
|
data class AudioProbeStream(
|
||||||
|
@ -27,15 +22,15 @@ data class AudioProbeChapterTags(
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class AudioProbeChapter(
|
data class AudioProbeChapter(
|
||||||
val id:Int,
|
val id:Int,
|
||||||
val start:Int,
|
val start:Long,
|
||||||
val end:Int,
|
val end:Long,
|
||||||
val tags:AudioProbeChapterTags?
|
val tags:AudioProbeChapterTags?
|
||||||
) {
|
) {
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getBookChapter():BookChapter {
|
fun getBookChapter():BookChapter {
|
||||||
var startS = start / 1000.0
|
val startS = start / 1000.0
|
||||||
var endS = end / 1000.0
|
val endS = end / 1000.0
|
||||||
var title = tags?.title ?: "Chapter $id"
|
val title = tags?.title ?: "Chapter $id"
|
||||||
return BookChapter(id, startS, endS, title)
|
return BookChapter(id, startS, endS, title)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,7 +85,7 @@ class Podcast(
|
||||||
) : MediaType(metadata, coverPath) {
|
) : MediaType(metadata, coverPath) {
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
override fun getAudioTracks():List<AudioTrack> {
|
override fun getAudioTracks():List<AudioTrack> {
|
||||||
var tracks = episodes?.map { it.audioTrack }
|
val tracks = episodes?.map { it.audioTrack }
|
||||||
return tracks?.filterNotNull() ?: mutableListOf()
|
return tracks?.filterNotNull() ?: mutableListOf()
|
||||||
}
|
}
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
|
@ -98,7 +98,7 @@ class Podcast(
|
||||||
// Add new episodes
|
// Add new episodes
|
||||||
audioTracks.forEach { at ->
|
audioTracks.forEach { at ->
|
||||||
if (episodes?.find{ it.audioTrack?.localFileId == at.localFileId } == null) {
|
if (episodes?.find{ it.audioTrack?.localFileId == at.localFileId } == null) {
|
||||||
var newEpisode = PodcastEpisode("local_" + at.localFileId,episodes?.size ?: 0 + 1,null,null,at.title,null,null,null,at,at.duration,0, null)
|
val newEpisode = PodcastEpisode("local_" + at.localFileId,episodes?.size ?: 0 + 1,null,null,at.title,null,null,null,at,at.duration,0, null)
|
||||||
episodes?.add(newEpisode)
|
episodes?.add(newEpisode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -111,7 +111,7 @@ class Podcast(
|
||||||
}
|
}
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
override fun addAudioTrack(audioTrack:AudioTrack) {
|
override fun addAudioTrack(audioTrack:AudioTrack) {
|
||||||
var newEpisode = PodcastEpisode("local_" + audioTrack.localFileId,episodes?.size ?: 0 + 1,null,null,audioTrack.title,null,null,null,audioTrack,audioTrack.duration,0, null)
|
val newEpisode = PodcastEpisode("local_" + audioTrack.localFileId,episodes?.size ?: 0 + 1,null,null,audioTrack.title,null,null,null,audioTrack,audioTrack.duration,0, null)
|
||||||
episodes?.add(newEpisode)
|
episodes?.add(newEpisode)
|
||||||
|
|
||||||
var index = 1
|
var index = 1
|
||||||
|
@ -132,7 +132,7 @@ class Podcast(
|
||||||
}
|
}
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun addEpisode(audioTrack:AudioTrack, episode:PodcastEpisode) {
|
fun addEpisode(audioTrack:AudioTrack, episode:PodcastEpisode) {
|
||||||
var newEpisode = PodcastEpisode("local_" + episode.id,episodes?.size ?: 0 + 1,episode.episode,episode.episodeType,episode.title,episode.subtitle,episode.description,null,audioTrack,audioTrack.duration,0, episode.id)
|
val newEpisode = PodcastEpisode("local_" + episode.id,episodes?.size ?: 0 + 1,episode.episode,episode.episodeType,episode.title,episode.subtitle,episode.description,null,audioTrack,audioTrack.duration,0, episode.id)
|
||||||
episodes?.add(newEpisode)
|
episodes?.add(newEpisode)
|
||||||
|
|
||||||
var index = 1
|
var index = 1
|
||||||
|
|
|
@ -17,9 +17,9 @@ class DbManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLocalLibraryItems(mediaType:String? = null):MutableList<LocalLibraryItem> {
|
fun getLocalLibraryItems(mediaType:String? = null):MutableList<LocalLibraryItem> {
|
||||||
var localLibraryItems:MutableList<LocalLibraryItem> = mutableListOf()
|
val localLibraryItems:MutableList<LocalLibraryItem> = mutableListOf()
|
||||||
Paper.book("localLibraryItems").allKeys.forEach {
|
Paper.book("localLibraryItems").allKeys.forEach {
|
||||||
var localLibraryItem:LocalLibraryItem? = Paper.book("localLibraryItems").read(it)
|
val localLibraryItem:LocalLibraryItem? = Paper.book("localLibraryItems").read(it)
|
||||||
if (localLibraryItem != null && (mediaType.isNullOrEmpty() || mediaType == localLibraryItem.mediaType)) {
|
if (localLibraryItem != null && (mediaType.isNullOrEmpty() || mediaType == localLibraryItem.mediaType)) {
|
||||||
localLibraryItems.add(localLibraryItem)
|
localLibraryItems.add(localLibraryItem)
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@ class DbManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLocalLibraryItemsInFolder(folderId:String):List<LocalLibraryItem> {
|
fun getLocalLibraryItemsInFolder(folderId:String):List<LocalLibraryItem> {
|
||||||
var localLibraryItems = getLocalLibraryItems()
|
val localLibraryItems = getLocalLibraryItems()
|
||||||
return localLibraryItems.filter {
|
return localLibraryItems.filter {
|
||||||
it.folderId == folderId
|
it.folderId == folderId
|
||||||
}
|
}
|
||||||
|
@ -65,7 +65,7 @@ class DbManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAllLocalFolders():List<LocalFolder> {
|
fun getAllLocalFolders():List<LocalFolder> {
|
||||||
var localFolders:MutableList<LocalFolder> = mutableListOf()
|
val localFolders:MutableList<LocalFolder> = mutableListOf()
|
||||||
Paper.book("localFolders").allKeys.forEach { localFolderId ->
|
Paper.book("localFolders").allKeys.forEach { localFolderId ->
|
||||||
Paper.book("localFolders").read<LocalFolder>(localFolderId)?.let {
|
Paper.book("localFolders").read<LocalFolder>(localFolderId)?.let {
|
||||||
localFolders.add(it)
|
localFolders.add(it)
|
||||||
|
@ -75,7 +75,7 @@ class DbManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeLocalFolder(folderId:String) {
|
fun removeLocalFolder(folderId:String) {
|
||||||
var localLibraryItems = getLocalLibraryItemsInFolder(folderId)
|
val localLibraryItems = getLocalLibraryItemsInFolder(folderId)
|
||||||
localLibraryItems.forEach {
|
localLibraryItems.forEach {
|
||||||
Paper.book("localLibraryItems").delete(it.id)
|
Paper.book("localLibraryItems").delete(it.id)
|
||||||
}
|
}
|
||||||
|
@ -91,7 +91,7 @@ class DbManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getDownloadItems():List<AbsDownloader.DownloadItem> {
|
fun getDownloadItems():List<AbsDownloader.DownloadItem> {
|
||||||
var downloadItems:MutableList<AbsDownloader.DownloadItem> = mutableListOf()
|
val downloadItems:MutableList<AbsDownloader.DownloadItem> = mutableListOf()
|
||||||
Paper.book("downloadItems").allKeys.forEach { downloadItemId ->
|
Paper.book("downloadItems").allKeys.forEach { downloadItemId ->
|
||||||
Paper.book("downloadItems").read<AbsDownloader.DownloadItem>(downloadItemId)?.let {
|
Paper.book("downloadItems").read<AbsDownloader.DownloadItem>(downloadItemId)?.let {
|
||||||
downloadItems.add(it)
|
downloadItems.add(it)
|
||||||
|
@ -108,7 +108,7 @@ class DbManager {
|
||||||
return Paper.book("localMediaProgress").read(localMediaProgressId)
|
return Paper.book("localMediaProgress").read(localMediaProgressId)
|
||||||
}
|
}
|
||||||
fun getAllLocalMediaProgress():List<LocalMediaProgress> {
|
fun getAllLocalMediaProgress():List<LocalMediaProgress> {
|
||||||
var mediaProgress:MutableList<LocalMediaProgress> = mutableListOf()
|
val mediaProgress:MutableList<LocalMediaProgress> = mutableListOf()
|
||||||
Paper.book("localMediaProgress").allKeys.forEach { localMediaProgressId ->
|
Paper.book("localMediaProgress").allKeys.forEach { localMediaProgressId ->
|
||||||
Paper.book("localMediaProgress").read<LocalMediaProgress>(localMediaProgressId)?.let {
|
Paper.book("localMediaProgress").read<LocalMediaProgress>(localMediaProgressId)?.let {
|
||||||
mediaProgress.add(it)
|
mediaProgress.add(it)
|
||||||
|
@ -126,14 +126,14 @@ class DbManager {
|
||||||
|
|
||||||
// Make sure all local file ids still exist
|
// Make sure all local file ids still exist
|
||||||
fun cleanLocalLibraryItems() {
|
fun cleanLocalLibraryItems() {
|
||||||
var localLibraryItems = getLocalLibraryItems()
|
val localLibraryItems = getLocalLibraryItems()
|
||||||
|
|
||||||
localLibraryItems.forEach { lli ->
|
localLibraryItems.forEach { lli ->
|
||||||
var hasUpates = false
|
var hasUpates = false
|
||||||
|
|
||||||
// Check local files
|
// Check local files
|
||||||
lli.localFiles = lli.localFiles.filter { localFile ->
|
lli.localFiles = lli.localFiles.filter { localFile ->
|
||||||
var file = File(localFile.absolutePath)
|
val file = File(localFile.absolutePath)
|
||||||
if (!file.exists()) {
|
if (!file.exists()) {
|
||||||
Log.d(tag, "cleanLocalLibraryItems: Local file ${localFile.absolutePath} was removed from library item ${lli.media.metadata.title}")
|
Log.d(tag, "cleanLocalLibraryItems: Local file ${localFile.absolutePath} was removed from library item ${lli.media.metadata.title}")
|
||||||
hasUpates = true
|
hasUpates = true
|
||||||
|
@ -143,7 +143,7 @@ class DbManager {
|
||||||
|
|
||||||
// Check audio tracks and episodes
|
// Check audio tracks and episodes
|
||||||
if (lli.isPodcast) {
|
if (lli.isPodcast) {
|
||||||
var podcast = lli.media as Podcast
|
val podcast = lli.media as Podcast
|
||||||
podcast.episodes = podcast.episodes?.filter { ep ->
|
podcast.episodes = podcast.episodes?.filter { ep ->
|
||||||
if (lli.localFiles.find { lf -> lf.id == ep.audioTrack?.localFileId } == null) {
|
if (lli.localFiles.find { lf -> lf.id == ep.audioTrack?.localFileId } == null) {
|
||||||
Log.d(tag, "cleanLocalLibraryItems: Podcast episode ${ep.title} was removed from library item ${lli.media.metadata.title}")
|
Log.d(tag, "cleanLocalLibraryItems: Podcast episode ${ep.title} was removed from library item ${lli.media.metadata.title}")
|
||||||
|
@ -152,7 +152,7 @@ class DbManager {
|
||||||
ep.audioTrack != null && lli.localFiles.find { lf -> lf.id == ep.audioTrack?.localFileId } != null
|
ep.audioTrack != null && lli.localFiles.find { lf -> lf.id == ep.audioTrack?.localFileId } != null
|
||||||
} as MutableList<PodcastEpisode>
|
} as MutableList<PodcastEpisode>
|
||||||
} else {
|
} else {
|
||||||
var book = lli.media as Book
|
val book = lli.media as Book
|
||||||
book.tracks = book.tracks?.filter { track ->
|
book.tracks = book.tracks?.filter { track ->
|
||||||
if (lli.localFiles.find { lf -> lf.id == track.localFileId } == null) {
|
if (lli.localFiles.find { lf -> lf.id == track.localFileId } == null) {
|
||||||
Log.d(tag, "cleanLocalLibraryItems: Audio track ${track.title} was removed from library item ${lli.media.metadata.title}")
|
Log.d(tag, "cleanLocalLibraryItems: Audio track ${track.title} was removed from library item ${lli.media.metadata.title}")
|
||||||
|
@ -164,7 +164,7 @@ class DbManager {
|
||||||
|
|
||||||
// Check cover still there
|
// Check cover still there
|
||||||
lli.coverAbsolutePath?.let {
|
lli.coverAbsolutePath?.let {
|
||||||
var coverFile = File(it)
|
val coverFile = File(it)
|
||||||
|
|
||||||
if (!coverFile.exists()) {
|
if (!coverFile.exists()) {
|
||||||
Log.d(tag, "cleanLocalLibraryItems: Cover $it was removed from library item ${lli.media.metadata.title}")
|
Log.d(tag, "cleanLocalLibraryItems: Cover $it was removed from library item ${lli.media.metadata.title}")
|
||||||
|
@ -183,10 +183,10 @@ class DbManager {
|
||||||
|
|
||||||
// Remove any local media progress where the local media item is not found
|
// Remove any local media progress where the local media item is not found
|
||||||
fun cleanLocalMediaProgress() {
|
fun cleanLocalMediaProgress() {
|
||||||
var localMediaProgress = getAllLocalMediaProgress()
|
val localMediaProgress = getAllLocalMediaProgress()
|
||||||
var localLibraryItems = getLocalLibraryItems()
|
val localLibraryItems = getLocalLibraryItems()
|
||||||
localMediaProgress.forEach {
|
localMediaProgress.forEach {
|
||||||
var matchingLLI = localLibraryItems.find { lli -> lli.id == it.localLibraryItemId }
|
val matchingLLI = localLibraryItems.find { lli -> lli.id == it.localLibraryItemId }
|
||||||
if (matchingLLI == null) {
|
if (matchingLLI == null) {
|
||||||
Log.d(tag, "cleanLocalMediaProgress: No matching local library item for local media progress ${it.id} - removing")
|
Log.d(tag, "cleanLocalMediaProgress: No matching local library item for local media progress ${it.id} - removing")
|
||||||
Paper.book("localMediaProgress").delete(it.id)
|
Paper.book("localMediaProgress").delete(it.id)
|
||||||
|
@ -195,8 +195,8 @@ class DbManager {
|
||||||
Log.d(tag, "cleanLocalMediaProgress: Podcast media progress has no episode id - removing")
|
Log.d(tag, "cleanLocalMediaProgress: Podcast media progress has no episode id - removing")
|
||||||
Paper.book("localMediaProgress").delete(it.id)
|
Paper.book("localMediaProgress").delete(it.id)
|
||||||
} else {
|
} else {
|
||||||
var podcast = matchingLLI.media as Podcast
|
val podcast = matchingLLI.media as Podcast
|
||||||
var matchingLEp = podcast.episodes?.find { ep -> ep.id == it.localEpisodeId }
|
val matchingLEp = podcast.episodes?.find { ep -> ep.id == it.localEpisodeId }
|
||||||
if (matchingLEp == null) {
|
if (matchingLEp == null) {
|
||||||
Log.d(tag, "cleanLocalMediaProgress: Podcast media progress for episode ${it.localEpisodeId} not found - removing")
|
Log.d(tag, "cleanLocalMediaProgress: Podcast media progress for episode ${it.localEpisodeId} not found - removing")
|
||||||
Paper.book("localMediaProgress").delete(it.id)
|
Paper.book("localMediaProgress").delete(it.id)
|
||||||
|
@ -212,15 +212,4 @@ class DbManager {
|
||||||
fun getLocalPlaybackSession(playbackSessionId:String):PlaybackSession? {
|
fun getLocalPlaybackSession(playbackSessionId:String):PlaybackSession? {
|
||||||
return Paper.book("localPlaybackSession").read(playbackSessionId)
|
return Paper.book("localPlaybackSession").read(playbackSessionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveObject(db:String, key:String, value:JSONObject) {
|
|
||||||
Log.d(tag, "Saving Object $key ${value.toString()}")
|
|
||||||
Paper.book(db).write(key, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun loadObject(db:String, key:String):JSONObject? {
|
|
||||||
var json: JSONObject? = Paper.book(db).read(key)
|
|
||||||
Log.d(tag, "Loaded Object $key $json")
|
|
||||||
return json
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -288,38 +288,4 @@ class AbsDatabase : Plugin() {
|
||||||
call.resolve()
|
call.resolve()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
|
||||||
// Generic Webview calls to db
|
|
||||||
//
|
|
||||||
@PluginMethod
|
|
||||||
fun saveFromWebview(call: PluginCall) {
|
|
||||||
var db = call.getString("db", "").toString()
|
|
||||||
var key = call.getString("key", "").toString()
|
|
||||||
var value = call.getObject("value")
|
|
||||||
|
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
|
||||||
if (db == "" || key == "" || value == null) {
|
|
||||||
Log.d(tag, "saveFromWebview Invalid key/value")
|
|
||||||
} else {
|
|
||||||
var json = value as JSONObject
|
|
||||||
DeviceManager.dbManager.saveObject(db, key, json)
|
|
||||||
}
|
|
||||||
call.resolve()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@PluginMethod
|
|
||||||
fun loadFromWebview(call:PluginCall) {
|
|
||||||
var db = call.getString("db", "").toString()
|
|
||||||
var key = call.getString("key", "").toString()
|
|
||||||
if (db == "" || key == "") {
|
|
||||||
Log.d(tag, "loadFromWebview Invalid Key")
|
|
||||||
call.resolve()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var json = DeviceManager.dbManager.loadObject(db, key)
|
|
||||||
var jsobj = JSObject.fromJSONObject(json)
|
|
||||||
call.resolve(jsobj)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,13 +9,13 @@ install! 'cocoapods', :disable_input_output_paths => true
|
||||||
def capacitor_pods
|
def capacitor_pods
|
||||||
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
||||||
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
||||||
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
|
pod 'CapacitorApp', :path => '..\..\node_modules\@capacitor\app'
|
||||||
pod 'CapacitorDialog', :path => '../../node_modules/@capacitor/dialog'
|
pod 'CapacitorDialog', :path => '..\..\node_modules\@capacitor\dialog'
|
||||||
pod 'CapacitorHaptics', :path => '../../node_modules/@capacitor/haptics'
|
pod 'CapacitorHaptics', :path => '..\..\node_modules\@capacitor\haptics'
|
||||||
pod 'CapacitorNetwork', :path => '../../node_modules/@capacitor/network'
|
pod 'CapacitorNetwork', :path => '..\..\node_modules\@capacitor\network'
|
||||||
pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
|
pod 'CapacitorStatusBar', :path => '..\..\node_modules\@capacitor\status-bar'
|
||||||
pod 'CapacitorStorage', :path => '../../node_modules/@capacitor/storage'
|
pod 'CapacitorStorage', :path => '..\..\node_modules\@capacitor\storage'
|
||||||
pod 'RobingenzCapacitorAppUpdate', :path => '../../node_modules/@robingenz/capacitor-app-update'
|
pod 'RobingenzCapacitorAppUpdate', :path => '..\..\node_modules\@robingenz\capacitor-app-update'
|
||||||
end
|
end
|
||||||
|
|
||||||
target 'App' do
|
target 'App' do
|
||||||
|
|
|
@ -6,28 +6,6 @@ const isWeb = Capacitor.getPlatform() == 'web'
|
||||||
class DbService {
|
class DbService {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
// Please dont use this, it is not implemented in ios (maybe key: primary value: any ?)
|
|
||||||
save(db, key, value) {
|
|
||||||
if (isWeb) return
|
|
||||||
return AbsDatabase.saveFromWebview({ db, key, value }).then(() => {
|
|
||||||
console.log('Saved data', db, key, JSON.stringify(value))
|
|
||||||
}).catch((error) => {
|
|
||||||
console.error('Failed to save data', error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Please dont use this, it is not implemented in ios
|
|
||||||
load(db, key) {
|
|
||||||
if (isWeb) return null
|
|
||||||
return AbsDatabase.loadFromWebview({ db, key }).then((data) => {
|
|
||||||
console.log('Loaded data', db, key, JSON.stringify(data))
|
|
||||||
return data
|
|
||||||
}).catch((error) => {
|
|
||||||
console.error('Failed to load', error)
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
getDeviceData() {
|
getDeviceData() {
|
||||||
return AbsDatabase.getDeviceData().then((data) => {
|
return AbsDatabase.getDeviceData().then((data) => {
|
||||||
console.log('Loaded device data', JSON.stringify(data))
|
console.log('Loaded device data', JSON.stringify(data))
|
||||||
|
|
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 122 KiB |
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 151 KiB |
Loading…
Add table
Add a link
Reference in a new issue