Skip to content
Merged
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
20 changes: 16 additions & 4 deletions swiftchan.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@
objects = {

/* Begin PBXBuildFile section */
DA000003DDDD000100000002 /* ReverseImageSearchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA000003DDDD000100000001 /* ReverseImageSearchTests.swift */; };
DA000002DDDD000100000002 /* ReverseImageSearchMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA000002DDDD000100000001 /* ReverseImageSearchMenu.swift */; };
DA000001DDDD000100000002 /* ReverseImageSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA000001DDDD000100000001 /* ReverseImageSearch.swift */; };
CA000006CCCC000100000002 /* ThreadWatcherView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA000006CCCC000100000001 /* ThreadWatcherView.swift */; };
CA000005CCCC000100000002 /* ThreadWatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA000005CCCC000100000001 /* ThreadWatcher.swift */; };
CA000004CCCC000100000002 /* PostFiltersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA000004CCCC000100000001 /* PostFiltersView.swift */; };
CA000003CCCC000100000002 /* ChanPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA000003CCCC000100000001 /* ChanPreferences.swift */; };
CA000002CCCC000100000002 /* ReplyDraftSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA000002CCCC000100000001 /* ReplyDraftSheet.swift */; };
CA000001CCCC000100000002 /* QuotePreviewSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA000001CCCC000100000001 /* QuotePreviewSheet.swift */; };
CA000001CCCC000100000002 /* PostDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA000001CCCC000100000001 /* PostDetailView.swift */; };
AC000001CCCC000100000002 /* RecentThreadsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC000001CCCC000100000001 /* RecentThreadsView.swift */; };
AF000003AAAA000100000002 /* SavedReplyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF000003AAAA000100000001 /* SavedReplyTests.swift */; };
AF000002AAAA000100000002 /* SavedRepliesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF000002AAAA000100000001 /* SavedRepliesView.swift */; };
Expand Down Expand Up @@ -161,12 +164,15 @@
/* End PBXCopyFilesBuildPhase section */

/* Begin PBXFileReference section */
DA000003DDDD000100000001 /* ReverseImageSearchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReverseImageSearchTests.swift; sourceTree = "<group>"; };
DA000002DDDD000100000001 /* ReverseImageSearchMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReverseImageSearchMenu.swift; sourceTree = "<group>"; };
DA000001DDDD000100000001 /* ReverseImageSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReverseImageSearch.swift; sourceTree = "<group>"; };
CA000006CCCC000100000001 /* ThreadWatcherView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadWatcherView.swift; sourceTree = "<group>"; };
CA000005CCCC000100000001 /* ThreadWatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadWatcher.swift; sourceTree = "<group>"; };
CA000004CCCC000100000001 /* PostFiltersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostFiltersView.swift; sourceTree = "<group>"; };
CA000003CCCC000100000001 /* ChanPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChanPreferences.swift; sourceTree = "<group>"; };
CA000002CCCC000100000001 /* ReplyDraftSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyDraftSheet.swift; sourceTree = "<group>"; };
CA000001CCCC000100000001 /* QuotePreviewSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuotePreviewSheet.swift; sourceTree = "<group>"; };
CA000001CCCC000100000001 /* PostDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostDetailView.swift; sourceTree = "<group>"; };
AC000001CCCC000100000001 /* RecentThreadsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentThreadsView.swift; sourceTree = "<group>"; };
AF000003AAAA000100000001 /* SavedReplyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedReplyTests.swift; sourceTree = "<group>"; };
AF000002AAAA000100000001 /* SavedRepliesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedRepliesView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -339,6 +345,7 @@
950E3933255CD9BB00A00A74 /* Media */ = {
isa = PBXGroup;
children = (
DA000002DDDD000100000001 /* ReverseImageSearchMenu.swift */,
959E6152255CD4750003638E /* Gallery */,
950E393A255CD9EE00A00A74 /* Image */,
AA000004AAAA000000000004 /* Video */,
Expand All @@ -360,6 +367,7 @@
9510536725660071002E4051 /* Models */ = {
isa = PBXGroup;
children = (
DA000001DDDD000100000001 /* ReverseImageSearch.swift */,
AF000001AAAA000100000001 /* SavedReply.swift */,
BF000001EEEE000100000001 /* FavoritesBackup.swift */,
CA000003CCCC000100000001 /* ChanPreferences.swift */,
Expand Down Expand Up @@ -430,6 +438,7 @@
951817A1254CFB620032616E /* swiftchanTests */ = {
isa = PBXGroup;
children = (
DA000003DDDD000100000001 /* ReverseImageSearchTests.swift */,
AF000003AAAA000100000001 /* SavedReplyTests.swift */,
BF000004EEEE000100000001 /* FavoritesBackupTests.swift */,
EE000003EEEE000100000001 /* HiddenPostStoreTests.swift */,
Expand Down Expand Up @@ -557,7 +566,7 @@
children = (
9599CA95254DF8C0002EB026 /* ThreadView.swift */,
CA000002CCCC000100000001 /* ReplyDraftSheet.swift */,
CA000001CCCC000100000001 /* QuotePreviewSheet.swift */,
CA000001CCCC000100000001 /* PostDetailView.swift */,
9599CA9F254DFDA1002EB026 /* PostView.swift */,
95D804CD25677FF4005A3128 /* RepliesView.swift */,
9559F79228443E7D00F5803F /* ThreadAutoRefresher.swift */,
Expand Down Expand Up @@ -814,6 +823,8 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
DA000002DDDD000100000002 /* ReverseImageSearchMenu.swift in Sources */,
DA000001DDDD000100000002 /* ReverseImageSearch.swift in Sources */,
AF000002AAAA000100000002 /* SavedRepliesView.swift in Sources */,
AF000001AAAA000100000002 /* SavedReply.swift in Sources */,
954FB5832730BDF0009CC52F /* RoundedCorners.swift in Sources */,
Expand Down Expand Up @@ -855,7 +866,7 @@
9559F78F2843B07D00F5803F /* NSFWTagView.swift in Sources */,
9599CA96254DF8C0002EB026 /* ThreadView.swift in Sources */,
CA000002CCCC000100000002 /* ReplyDraftSheet.swift in Sources */,
CA000001CCCC000100000002 /* QuotePreviewSheet.swift in Sources */,
CA000001CCCC000100000002 /* PostDetailView.swift in Sources */,
95181791254CFB600032616E /* swiftchanApp.swift in Sources */,
95978F162CA7981600C520EF /* UINavigationController.swift in Sources */,
9587A51C255726D80028A143 /* CacheService.swift in Sources */,
Expand Down Expand Up @@ -915,6 +926,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
DA000003DDDD000100000002 /* ReverseImageSearchTests.swift in Sources */,
AF000003AAAA000100000002 /* SavedReplyTests.swift in Sources */,
BF000004EEEE000100000002 /* FavoritesBackupTests.swift in Sources */,
EE000003EEEE000100000002 /* HiddenPostStoreTests.swift in Sources */,
Expand Down
8 changes: 4 additions & 4 deletions swiftchan/Models/PresentationState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ class PresentationState {
var presentingGallery: Bool = false
var galleryIndex: Int = 0
var presentingIndex: Int = 0
var presentingReplies: Bool = false
var activePostContext: UUID?
var presentingReplies: Bool { activePostContext != nil }
}

extension EnvironmentValues {
/// Namespace used for the thumbnail → gallery zoom transition.
@Entry var galleryNamespace: Namespace.ID?
/// True for PostViews rendered inside RepliesView, so only one context
/// registers a matchedTransitionSource for a given media index at a time.
@Entry var inRepliesContext: Bool = false
/// Identifies each pushed post/replies page; nil identifies the main thread.
@Entry var postContextID: UUID?
}

extension View {
Expand Down
56 changes: 56 additions & 0 deletions swiftchan/Models/ReverseImageSearch.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import Foundation

enum ReverseImageSearchProvider: String, CaseIterable, Identifiable {
case googleLens, yandex, tineye, sauceNAO, iqdb, traceMoe

var id: String { rawValue }

var name: String {
switch self {
case .googleLens: "Google Lens"
case .yandex: "Yandex"
case .tineye: "TinEye"
case .sauceNAO: "SauceNAO"
case .iqdb: "IQDB"
case .traceMoe: "trace.moe"
}
}

func searchURL(for media: Media) -> URL? {
guard let imageURL = Self.sourceURL(for: media) else { return nil }
let endpoint: String
switch self {
case .googleLens: endpoint = "https://lens.google.com/uploadbyurl"
case .yandex: endpoint = "https://yandex.com/images/search"
case .tineye: endpoint = "https://tineye.com/search"
case .sauceNAO: endpoint = "https://saucenao.com/search.php"
case .iqdb: endpoint = "https://iqdb.org/"
case .traceMoe: endpoint = "https://trace.moe/"
}
var components = URLComponents(string: endpoint)
var items: [URLQueryItem] = []
if self == .yandex { items.append(URLQueryItem(name: "rpt", value: "imageview")) }
items.append(URLQueryItem(name: "url", value: imageURL.absoluteString))
components?.queryItems = items
// These providers parse form-style queries, where an unescaped + means a space.
let encodedQuery = components?.percentEncodedQuery?.replacingOccurrences(of: "+", with: "%2B")
components?.percentEncodedQuery = encodedQuery
return components?.url
}

static func sourceURL(for media: Media) -> URL? {
let source: URL
switch media.url.pathExtension.lowercased() {
case "jpg", "jpeg", "png", "gif": source = media.url
case "webm", "mp4": source = media.thumbnailUrl
default: return nil
}
guard ["http", "https"].contains(source.scheme?.lowercased() ?? ""),
let host = source.host, !host.isEmpty, source.user == nil, source.password == nil else { return nil }
return source
}

static func usesThumbnail(for media: Media) -> Bool {
["webm", "mp4"].contains(media.url.pathExtension.lowercased())
}
}
5 changes: 3 additions & 2 deletions swiftchan/Services/ThreadUITestFixture.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
post["sub"] = "/pmg/ - Precious Metals General"
if arguments.contains("--ui-general-rollover"), id == 100 { post["archived"] = 1 }
}
if arguments.contains("--ui-media-fixture"), number < id + 3 {
if arguments.contains("--ui-media-fixture"),
number < id + 3 || (arguments.contains("--ui-quote-fixture") && number == 104) {
let timestamp = 9_000_000_000 + number
post["tim"] = timestamp
post["filename"] = "collection-study-\(number - id + 1)"
Expand All @@ -29,7 +30,7 @@
post["fsize"] = 100_000
seedMedia(timestamp: timestamp, index: number - id)
}
if arguments.contains("--ui-quote-fixture"), number == 105 || number == 104 {
if arguments.contains("--ui-quote-fixture"), (103...105).contains(number) {
post["com"] = "<a class=\"quotelink\" href=\"#p\(number - 1)\">&gt;&gt;\(number - 1)</a><br>Quoted reply \(number)"
}
return post
Expand Down
25 changes: 25 additions & 0 deletions swiftchan/Views/Boards/Catalog/Thread/PostDetailView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import SwiftUI

struct PostDestination: Identifiable, Hashable {
let id: Int
}

struct PostDetailView: View {
let postID: Int
@Environment(ThreadViewModel.self) private var model

var body: some View {
Group {
if let index = model.getPostIndexFromId(String(postID)),
!model.posts[index].isHidden(boardName: model.boardName), model.filterEffect(at: index) != .hide {
RepliesView(replies: [index])
.accessibilityIdentifier("Post Detail \(postID)")
} else {
ContentUnavailableView("Post Unavailable", systemImage: "text.bubble",
description: Text("This post is hidden or is not in the loaded thread."))
}
}
.navigationTitle("#\(postID)")
.navigationBarTitleDisplayMode(.inline)
}
}
19 changes: 8 additions & 11 deletions swiftchan/Views/Boards/Catalog/Thread/PostView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ struct PostView: View {
@Environment(AppState.self) private var appState
@Environment(PresentationState.self) private var presentationState: PresentationState
@Environment(\.galleryNamespace) private var galleryNamespace
@Environment(\.inRepliesContext) private var inRepliesContext
@Environment(\.postContextID) private var postContextID

let index: Int

Expand Down Expand Up @@ -59,9 +59,8 @@ struct PostView: View {
.galleryTransitionSource(
id: mediaIndex,
namespace: galleryNamespace,
// Only one context may own a source id: the thread
// list normally, RepliesView while it is pushed.
isActive: inRepliesContext == presentationState.presentingReplies
// Only the visible thread, post, or replies page owns the source.
isActive: postContextID == presentationState.activePostContext
)
.onTapGesture {
viewModel.media[mediaIndex].isSelected = true
Expand All @@ -73,7 +72,9 @@ struct PostView: View {
Text(filename + fileExtension)
.font(Font.system(size: 8))
}

ReverseImageSearchMenu(media: media, identifier: "Image Search Post \(post.no)")
.font(.caption)
.padding(.top, 4)
}
.padding(.leading, -5)
}
Expand Down Expand Up @@ -177,12 +178,8 @@ struct PostView: View {
RepliesView(replies: replies)
.environment(viewModel)
.environment(presentationState)
.onAppear {
presentationState.presentingReplies = true
}
.onDisappear {
presentationState.presentingReplies = false
}
.navigationTitle("Replies to #\(post.no)")
.navigationBarTitleDisplayMode(.inline)
}
},
label: {
Expand Down
57 changes: 0 additions & 57 deletions swiftchan/Views/Boards/Catalog/Thread/QuotePreviewSheet.swift

This file was deleted.

Loading