Skip to content
Draft
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
65 changes: 44 additions & 21 deletions CommonsAPI/Sources/CommonsAPI/API.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1171,30 +1171,53 @@ LIMIT \(limit)
return entitiesDict
}

/// Check if a media file already exists by filename
public func checkIfFileExists(filename: String) async throws -> FilenameExistsResult {
public func checkIfFileExists(filename: String) async throws -> FilenameExistsResult? {
let result = try await checkIfFilesExists(filenames: [filename])
return result[filename]
}

/// Check if a media file already exists by filename, returns [filename: FilenameExistsResult]
public func checkIfFilesExists(filenames: [String]) async throws -> [String: FilenameExistsResult] {
let titles = filenames.map { "File:" + $0 }
let query: Parameters = [
"action": "query",
"format": "json",
"titles": "File:" + filename,
"titles": titles.joined(separator: "|"),
"formatversion": "2",
"curtimestamp": "1"
]

let request = try GET(url: commonsEndpoint, query: query)
let (data, response) = try await urlSession.data(for: request)
let parsedResponse = try parse(QueryResponse<FileExistenceResponse>.self, from: data, response: response)
guard let fileInfo = parsedResponse.query?.pages?.first else {

guard let pages = parsedResponse.query?.pages else {
throw CommonAPIError.missingResponseValues
}

if fileInfo.invalid == true {
return .invalidFilename
} else if fileInfo.missing == true {
return .doesNotExist
} else {
return .exists
var result: [String: FilenameExistsResult] = [:]

for page in pages {
guard let title = page.title else {
continue
}

var fromTitle = parsedResponse.query?.normalized?.first { normalizedResult in
normalizedResult.to == title
}?.from ?? title

fromTitle = String(fromTitle.split(separator: "File:")[0])

if page.invalid == true {
result[fromTitle] = .invalidFilename
} else if page.missing == true {
result[fromTitle] = .doesNotExist
} else {
result[fromTitle] = .exists
}
}

return result
}

// action=titleblacklist
Expand Down Expand Up @@ -1312,7 +1335,6 @@ LIMIT \(limit)
}
}
}


/// https://commons.wikimedia.org/w/api.php?action=upload&format=json&filename=&comment=&file=...&stash=1&token=&formatversion=2
public func publish(file: MediaFileUploadable, csrfToken: String, startStep: PublishingStep = .uploadData) -> AsyncStream<UploadStatus> {
Expand All @@ -1321,6 +1343,7 @@ LIMIT \(limit)

AsyncStream<UploadStatus> { continuation in
Task<Void, Never> {

do {
if startStep.unstashRequired {
var parameters: Parameters = [
Expand Down Expand Up @@ -1394,7 +1417,7 @@ LIMIT \(limit)
title: "File:\(file.filename)",
labels: file.captions,
statements: file.claims,
comment: "Created initial structured data after upload"
summary: "initial structured data"
)

continuation.yield(.published)
Expand Down Expand Up @@ -1598,7 +1621,7 @@ LIMIT \(limit)
title: String,
labels: [LanguageString],
statements: [WikidataClaim],
comment: String?
summary: String?
) async throws {
let token = try await fetchCSRFToken()

Expand All @@ -1625,24 +1648,24 @@ LIMIT \(limit)
throw CommonAPIError.failedToEncodeJSONData
}

let commentString: String
let summaryString: String

if let comment {
commentString = comment
if let summary {
summaryString = summary
} else if !labels.isEmpty, !statements.isEmpty {
commentString = "Edited labels (\(labels.map(\.languageCode).joined(separator: ", "))) and structured data statements"
summaryString = "Edited labels (\(labels.map(\.languageCode).joined(separator: ", "))) and structured data statements"
} else if labels.isEmpty {
commentString = "Edited structured data statements"
summaryString = "Edited structured data statements"
} else if statements.isEmpty {
commentString = "Edited labels (\( labels.map(\.languageCode).joined(separator: ", ")))"
summaryString = "Edited labels (\( labels.map(\.languageCode).joined(separator: ", ")))"
} else {
commentString = "Edited labels or structured data statements"
summaryString = "Edited labels or structured data statements"
assertionFailure()
}

let form: Parameters = [
"action": "wbeditentity",
"comment": commentString,
"summary": summaryString,
"token": token,
"format": "json",
"title": title,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -897,6 +897,13 @@ public struct GeosearchListResponse: Decodable, Sendable {

internal struct FileExistenceResponse: Decodable, Sendable {
let pages: [Item]?
let normalized: [Normalized]?

struct Normalized: Decodable, Sendable {
let fromencoded: Bool
let from: String
let to: String
}

struct Item: Decodable, Sendable {
let pageid: Int64?
Expand Down
3 changes: 1 addition & 2 deletions CommonsAPI/Tests/CommonsAPITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -202,14 +202,13 @@ struct CommonsEndToEndTests {

@Test("list sub-categories", arguments: ["Physics"])
func fetchCategoryInfo(category: String) async throws {
let info = try await api.fetchCategoryMembers(of: category)
let info = try await api.fetchCategoryMembers(of: category, sort: nil)

#expect(info != nil)
guard let info else { return }

#expect(info.parentCategories.count > 0)
#expect(info.subCategories.count > 5)
#expect(info.wikidataItem == .physicsCategory)
}

@Test("list images in category", arguments: ["Physics"])
Expand Down
51 changes: 35 additions & 16 deletions CommonsFinder.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -127,17 +127,24 @@
Database/FTS5Tokenizer.swift,
Database/Model/Category.swift,
Database/Model/CategoryInfo.swift,
Database/Model/Drafts/Draftable.swift,
Database/Model/Drafts/MediaFileDraft.swift,
Database/Model/Drafts/MultiDraft.swift,
Database/Model/Drafts/MultiDraftInfo.swift,
Database/Model/Drafts/Types/DraftAuthor.swift,
Database/Model/Drafts/Types/DraftSource.swift,
Database/Model/Drafts/Types/LocationHandling.swift,
"Database/Model/Extensions/MediaFile+createAttributedStringDescription.swift",
"Database/Model/Extensions/MediaFile+InitFromAPI.swift",
"Database/Model/Extensions/MediaFile+makeRandomUploaded.swift",
"Database/Model/Extensions/MediaFileDraft+DebugDraft.swift",
"Database/Model/Extensions/MediaFileInfo+ImageRequest.swift",
"Database/Model/Extensions/MediaFileInfo+sortedByLastViewed.swift",
"Database/Model/Extensions/MultiDraftInfo+DebugDraft.swift",
"Database/Model/Extensions/WikidataItem+InitFromAPI.swift",
"Database/Model/Extensions/WikidataItem+Thumbnail.swift",
Database/Model/ItemInteraction.swift,
Database/Model/MediaFile.swift,
Database/Model/MediaFileDraft.swift,
Database/Model/MediaFileInfo.swift,
DataFetching/DataAccess.swift,
"Generic Extensions/Array+popFirstN.swift",
Expand Down Expand Up @@ -181,12 +188,13 @@
Tests/OtherTests.swift,
Tests/PublishHelpersTests.swift,
Types/CaptionWithDescription.swift,
Types/DraftIDType.swift,
Types/DraftMediaLicense.swift,
"Types/DraftMediaLicense+CommonsAPI.swift",
Types/FileNameType.swift,
Types/FileNameTypeTuple.swift,
Types/ImageAnalysisResult.swift,
"Types/MediaFileDraft+uploadDisabledReason.swift",
Types/NameValidationError.swift,
Types/NewDraftOptions.swift,
Types/OptionBarState.swift,
Types/RelatedCategoriesType.swift,
Expand All @@ -200,6 +208,7 @@
"Utilities/CLLocation+gpsDictionary.swift",
Utilities/ExifData.swift,
Utilities/FileAnalysisHelpers.swift,
Utilities/FilenameUtils.swift,
Utilities/GeoPlacemarkCache.swift,
Utilities/GeoVectorMath.swift,
Utilities/Logging.swift,
Expand All @@ -221,23 +230,34 @@
Views/CategoryView/RelatedCategoryView.swift,
Views/ContextMenus/CategoryContextMenu.swift,
Views/ContextMenus/MediaFileContextMenu.swift,
Views/DraftViews/BaseDraftImageView.swift,
Views/DraftViews/DraftSheetModifer.swift,
Views/DraftViews/FileLocationMapView.swift,
Views/DraftViews/FilenameErrorButton.swift,
Views/DraftViews/FilenameTip.swift,
Views/DraftViews/LicensePicker.swift,
Views/DraftViews/Model/FileImportModel.swift,
Views/DraftViews/Model/FileItem.swift,
Views/DraftViews/Model/MultiDraftModel.swift,
Views/DraftViews/Model/NameValidationResult.swift,
Views/DraftViews/Model/SingleDraftModel.swift,
Views/DraftViews/MultiDraftListItem.swift,
Views/DraftViews/MultiDraftOverviewList.swift,
Views/DraftViews/MultiDraftSheetModifier.swift,
Views/DraftViews/MultiDraftView.swift,
Views/DraftViews/SingleDraftSheetModifier.swift,
Views/DraftViews/SingleDraftView.swift,
Views/DraftViews/TagPicker/TagButton.swift,
Views/DraftViews/TagPicker/TagLabel.swift,
Views/DraftViews/TagPicker/TagModel.swift,
Views/DraftViews/TagPicker/TagPicker.swift,
Views/DraftViews/TagPicker/TagPickerModel.swift,
Views/DraftViews/TimezonePicker.swift,
"Views/Extensions/CLLocationCoordinate2D+description.swift",
"Views/Extensions/CLLocationManager+isLocationAutorized.swift",
"Views/Extensions/MediaFile+localizedDisplayCaption.swift",
"Views/Extensions/MediaFileDraftModel+ImageRequest.swift",
"Views/Extensions/UserDefaults+accessors.swift",
Views/FileCreateView/DraftSheetModifer.swift,
Views/FileCreateView/FilenameTip.swift,
Views/FileCreateView/LicensePicker.swift,
Views/FileCreateView/Model/FileImportModel.swift,
Views/FileCreateView/Model/FileItem.swift,
Views/FileCreateView/Model/MediaFileDraftModel.swift,
Views/FileCreateView/SingleImageDraftView.swift,
Views/FileCreateView/TagPicker/TagButton.swift,
Views/FileCreateView/TagPicker/TagLabel.swift,
Views/FileCreateView/TagPicker/TagModel.swift,
Views/FileCreateView/TagPicker/TagPicker.swift,
Views/FileCreateView/TagPicker/TagPickerModel.swift,
Views/FileCreateView/TimezonePicker.swift,
Views/FileDetailView/FileDetailView.swift,
Views/FileDetailView/FileGroupBoxStyle.swift,
Views/FileDetailView/FileLoadView.swift,
Expand Down Expand Up @@ -295,7 +315,6 @@
"Views/Reusable Views/DeepCategoryToggle.swift",
"Views/Reusable Views/Deprecated/DraggablePseudoSheet.swift",
"Views/Reusable Views/Deprecated/PseudoSheet.swift",
"Views/Reusable Views/DraftFileListItem.swift",
"Views/Reusable Views/InlineMap.swift",
"Views/Reusable Views/LanguageButtons.swift",
"Views/Reusable Views/MediaDowloadSheet.swift",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"colors" : [
{
"color" : {
"color-space" : "display-p3",
"components" : {
"alpha" : "1.000",
"blue" : "1.000",
"green" : "0.534",
"red" : "0.334"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
32 changes: 10 additions & 22 deletions CommonsFinder/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,31 +46,17 @@ struct ContentView: View {
}
}

// Tab("Events", systemImage: "figure.socialdance", value: Navigation.TabItem.events) {
// NavigationStack(path: $navigation.eventsPath) {
// Text("Current and nearby events")
// .modifier(CommonNavigationDestination())
// }
// }

Tab(value: Navigation.TabItem.search, role: .search) {
NavigationStack(path: $navigation.searchPath) {
SearchView()
.modifier(CommonNavigationDestination())
}

}
}
.sheet(item: $navigation.isAuthSheetOpen, content: AuthView.init)
// .sheet(item: $navigation.isEditingDraft) { destination in
// switch destination {
// case .existing(let files):
// FileCreateView(appDatabase: appDatabase, files: files)
// case .newDraft(let options):
// FileCreateView(appDatabase: appDatabase, newDraftOptions: options)
// }
// }
.modifier(DraftSheetModifer(importModel: $navigation.isEditingDraft))
.modifier(ImportFilesModifer(importModel: $navigation.isImportingFiles))
.modifier(SingleDraftSheetModifier(draftedFileModel: $navigation.isEditingDraft))
.modifier(MultiDraftSheetModifier(multiDraftModel: $navigation.isEditingMultipleDrafts))
.onOpenURL(perform: handleURL)
.onContinueUserActivity(NSUserActivityTypeLiveActivity) { userActivity in
guard let url = userActivity.webpageURL else { return }
Expand Down Expand Up @@ -130,7 +116,7 @@ struct ContentView: View {
let drafts: [MediaFileDraft] = urls.compactMap { temporaryPath in
do {
let fileItem = try FileItem(movingLocalFileFromPath: temporaryPath)
let draft = try MediaFileDraft(fileItem)
let draft = try MediaFileDraft(fileItem, newDraftOptions: nil)
return try appDatabase.upsertAndFetch(draft)
} catch {
logger.error("Failed to move draft file from ShareExtension. \(error)")
Expand All @@ -142,11 +128,13 @@ struct ContentView: View {
Task {
// A short visually delay to allow the opening app animations to settle a moment
try? await Task.sleep(for: .milliseconds(200))

navigation.selectedTab = .home

if drafts.count > 1 {
// TODO: needs batch image implementation
navigation.selectedTab = .home
} else {
navigation.editDrafts(drafts: drafts)
navigation.editMultipleDrafts(multiDraftInfo: .init(multiDraft: .init(newDraftOptions: nil), drafts: drafts))
} else if let draft = drafts.first {
navigation.editDraft(draft: draft)
}
}

Expand Down
21 changes: 19 additions & 2 deletions CommonsFinder/Database/AppDatabase+Queries.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,34 @@ import os.log

// MARK: - Queries

/// A @Query request that observes all drafts in the database
struct AllMultiDraftsRequest: ValueObservationQueryable {
static var defaultValue: [MultiDraftInfo] { [] }

func fetch(_ db: Database) throws -> [MultiDraftInfo] {
do {
return
try MultiDraftInfo
.all()
.order(MultiDraft.Columns.addedDate.desc)
.fetchAll(db)
} catch {
logger.error("Failed to fetch all multiDrafts from db \(error)!")
return []
}
}
}

/// A @Query request that observes all drafts in the database
struct AllDraftsRequest: ValueObservationQueryable {
struct AllSingleDraftsRequest: ValueObservationQueryable {
static var defaultValue: [MediaFileDraft] { [] }

func fetch(_ db: Database) throws -> [MediaFileDraft] {
do {
return
try MediaFileDraft
.filter { $0.multiDraftId == nil }
.order(MediaFileDraft.Columns.addedDate.desc)
// .order(\.addedDate.desc)
.fetchAll(db)
} catch {
logger.error("Failed to fetch all draft files from db \(error)!")
Expand Down
Loading