774 lines
		
	
	
		
			21 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
			
		
		
	
	
			774 lines
		
	
	
		
			21 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
| {% extends "base.html" %}
 | |
| 
 | |
| {% block title %}AT URI Browser - {{ config.title }}{% endblock %}
 | |
| 
 | |
| {% block content %}
 | |
| <div class="pds-container">
 | |
|     <div class="pds-header">
 | |
|     </div>
 | |
|     
 | |
|     <!-- User Handle Input Form -->
 | |
|     <div class="pds-search-section">
 | |
|         <form class="pds-search-form" onsubmit="searchUser(); return false;">
 | |
|             <div class="form-group">
 | |
|                 <input type="text" id="handleInput" placeholder="ai.syui.ai" value="at://ai.syui.ai" />
 | |
|                 <button type="submit" id="searchButton">
 | |
|                     <i class="fab fa-bluesky"></i>
 | |
|                 </button>
 | |
|             </div>
 | |
|         </form>
 | |
|     </div>
 | |
|     
 | |
|     <!-- Current User DID -->
 | |
|     <div id="userDidSection" class="user-did-section" style="display: none;">
 | |
|         <div class="pds-display">
 | |
|             <strong>PDS:</strong> <span id="userPdsText"></span>
 | |
|         </div>
 | |
|         <div class="handle-display">
 | |
|             <strong>Handle:</strong> <span id="userHandleText"></span>
 | |
|         </div>
 | |
|         <div class="did-display">
 | |
|             <span id="userDidText"></span>
 | |
|         </div>
 | |
|     </div>
 | |
|     
 | |
|     <!-- Collection List -->
 | |
|     <div id="collectionsSection" class="collections-section" style="display: none;">
 | |
|         <div class="collections-header">
 | |
|             <button id="collectionsToggle" class="collections-toggle" onclick="toggleCollections()">[+] Collections</button>
 | |
|         </div>
 | |
|         <div id="collectionsList" class="collections-list" style="display: none;">
 | |
|             <!-- Collections will be populated here -->
 | |
|         </div>
 | |
|     </div>
 | |
|     
 | |
|     <!-- AT URI Records -->
 | |
|     <div id="recordsSection" class="records-section" style="display: none;">
 | |
|         <div id="recordsList" class="records-list">
 | |
|             <!-- Records will be populated here -->
 | |
|         </div>
 | |
|     </div>
 | |
|     
 | |
|     
 | |
| </div>
 | |
| 
 | |
| <!-- AT URI Modal -->
 | |
| <div id="atUriModal" class="at-uri-modal-overlay" style="display: none;" onclick="closeAtUriModal(event)">
 | |
|     <div class="at-uri-modal-content">
 | |
|         <button class="at-uri-modal-close" onclick="closeAtUriModal()">×</button>
 | |
|         <div id="atUriContent"></div>
 | |
|     </div>
 | |
| </div>
 | |
| 
 | |
| <style>
 | |
| .pds-container {
 | |
|     max-width: 1200px;
 | |
|     margin: 0 auto;
 | |
|     padding: 20px;
 | |
| }
 | |
| 
 | |
| .pds-header {
 | |
|     text-align: center;
 | |
|     margin-bottom: 40px;
 | |
| }
 | |
| 
 | |
| .pds-header h1 {
 | |
|     font-size: 2.5em;
 | |
|     margin-bottom: 10px;
 | |
|     color: #333;
 | |
| }
 | |
| 
 | |
| .pds-search-section {
 | |
|     border-radius: 8px;
 | |
| }
 | |
| 
 | |
| .pds-search-form {
 | |
|     display: flex;
 | |
|     justify-content: center;
 | |
|     margin-bottom: 20px;
 | |
| }
 | |
| 
 | |
| .form-group {
 | |
|     display: flex;
 | |
|     align-items: center;
 | |
|     gap: 10px;
 | |
| }
 | |
| 
 | |
| 
 | |
| .form-group input {
 | |
|     padding: 10px;
 | |
|     border: 1px solid #ddd;
 | |
|     border-radius: 4px;
 | |
|     font-size: 16px;
 | |
|     width: 300px;
 | |
| }
 | |
| 
 | |
| .form-group button {
 | |
|     padding: 10px 15px;
 | |
|     background: #1976d2;
 | |
|     color: white;
 | |
|     border: none;
 | |
|     border-radius: 4px;
 | |
|     cursor: pointer;
 | |
|     font-size: 18px;
 | |
|     min-width: 50px;
 | |
|     display: flex;
 | |
|     align-items: center;
 | |
|     justify-content: center;
 | |
| }
 | |
| 
 | |
| .form-group button:hover {
 | |
|     background: #1565c0;
 | |
| }
 | |
| 
 | |
| .user-info {
 | |
|     background: white;
 | |
|     padding: 20px;
 | |
|     border-radius: 8px;
 | |
|     box-shadow: 0 2px 4px rgba(0,0,0,0.1);
 | |
| }
 | |
| 
 | |
| .user-profile {
 | |
|     display: flex;
 | |
|     align-items: center;
 | |
|     gap: 15px;
 | |
| }
 | |
| 
 | |
| .user-details h3 {
 | |
|     margin: 0 0 5px 0;
 | |
|     color: #333;
 | |
| }
 | |
| 
 | |
| .user-details p {
 | |
|     margin: 0;
 | |
|     color: #666;
 | |
| }
 | |
| 
 | |
| .user-did-section {
 | |
|     margin: 20px 0;
 | |
| }
 | |
| 
 | |
| .did-display {
 | |
|     padding: 10px;
 | |
|     background: #f5f5f5;
 | |
|     border-radius: 4px;
 | |
|     font-family: monospace;
 | |
|     font-size: 14px;
 | |
|     color: #666;
 | |
|     word-break: break-all;
 | |
|     margin-bottom: 10px;
 | |
| }
 | |
| 
 | |
| .handle-display {
 | |
|     padding: 8px 10px;
 | |
|     background: #f0f9f0;
 | |
|     border-radius: 4px;
 | |
|     font-size: 13px;
 | |
|     color: #555;
 | |
|     margin-bottom: 8px;
 | |
| }
 | |
| 
 | |
| .handle-display strong {
 | |
|     color: #2e7d32;
 | |
| }
 | |
| 
 | |
| .handle-display span {
 | |
|     font-family: monospace;
 | |
|     font-size: 12px;
 | |
|     color: #666;
 | |
|     word-break: break-all;
 | |
| }
 | |
| 
 | |
| 
 | |
| .pds-display {
 | |
|     padding: 8px 10px;
 | |
|     background: #e8f4f8;
 | |
|     border-radius: 4px;
 | |
|     font-size: 13px;
 | |
|     color: #555;
 | |
| }
 | |
| 
 | |
| .pds-display strong {
 | |
|     color: #1976d2;
 | |
| }
 | |
| 
 | |
| .pds-display span {
 | |
|     font-family: monospace;
 | |
|     font-size: 12px;
 | |
|     color: #666;
 | |
|     word-break: break-all;
 | |
| }
 | |
| 
 | |
| .collections-section,
 | |
| .records-section {
 | |
|     margin: 20px 0;
 | |
| }
 | |
| 
 | |
| .collections-section h3,
 | |
| .records-section h3 {
 | |
|     font-size: 1.2em;
 | |
|     margin-bottom: 15px;
 | |
|     color: #333;
 | |
|     font-weight: bold;
 | |
| }
 | |
| 
 | |
| .collections-list,
 | |
| .records-list {
 | |
|     display: flex;
 | |
|     flex-direction: column;
 | |
|     gap: 8px;
 | |
| }
 | |
| 
 | |
| .at-uri-link {
 | |
|     display: block;
 | |
|     padding: 8px 12px;
 | |
|     background: #f9f9f9;
 | |
|     border-radius: 4px;
 | |
|     border: 1px solid #e0e0e0;
 | |
|     color: #1976d2;
 | |
|     text-decoration: none;
 | |
|     font-family: monospace;
 | |
|     font-size: 14px;
 | |
|     word-break: break-all;
 | |
|     transition: all 0.2s;
 | |
| }
 | |
| 
 | |
| .at-uri-link:hover {
 | |
|     background: #e8f4f8;
 | |
|     border-color: #1976d2;
 | |
|     text-decoration: none;
 | |
| }
 | |
| 
 | |
| .pds-info {
 | |
|     padding: 8px 12px;
 | |
|     background: #f0f9ff;
 | |
|     border-radius: 4px;
 | |
|     border: 1px solid #b3e5fc;
 | |
|     margin-bottom: 8px;
 | |
|     color: #1976d2;
 | |
|     font-size: 12px;
 | |
| }
 | |
| 
 | |
| .collection-info {
 | |
|     padding: 8px 12px;
 | |
|     background: #f0f9f0;
 | |
|     border-radius: 4px;
 | |
|     border: 1px solid #b3e5b3;
 | |
|     margin-bottom: 8px;
 | |
|     color: #2e7d32;
 | |
|     font-size: 12px;
 | |
| }
 | |
| 
 | |
| .collections-header {
 | |
|     margin-bottom: 10px;
 | |
| }
 | |
| 
 | |
| .collections-toggle {
 | |
|     background: #f5f5f5;
 | |
|     border: 1px solid #ddd;
 | |
|     border-radius: 4px;
 | |
|     padding: 8px 12px;
 | |
|     cursor: pointer;
 | |
|     font-size: 14px;
 | |
|     color: #333;
 | |
|     transition: background-color 0.2s;
 | |
| }
 | |
| 
 | |
| .collections-toggle:hover {
 | |
|     background: #e8f4f8;
 | |
|     border-color: #1976d2;
 | |
| }
 | |
| 
 | |
| 
 | |
| .pds-test-section,
 | |
| .pds-about-section {
 | |
|     margin-bottom: 40px;
 | |
| }
 | |
| 
 | |
| .pds-test-section h2,
 | |
| .pds-about-section h2 {
 | |
|     font-size: 1.8em;
 | |
|     margin-bottom: 20px;
 | |
|     color: #333;
 | |
|     border-bottom: 2px solid #1976d2;
 | |
|     padding-bottom: 10px;
 | |
| }
 | |
| 
 | |
| .test-uris {
 | |
|     display: flex;
 | |
|     flex-direction: column;
 | |
|     gap: 10px;
 | |
| }
 | |
| 
 | |
| .at-uri {
 | |
|     background: #f5f5f5;
 | |
|     padding: 15px;
 | |
|     border-radius: 8px;
 | |
|     font-family: monospace;
 | |
|     font-size: 14px;
 | |
|     word-break: break-all;
 | |
|     cursor: pointer;
 | |
|     transition: background-color 0.2s;
 | |
|     border: 1px solid #e0e0e0;
 | |
| }
 | |
| 
 | |
| .at-uri:hover {
 | |
|     background: #e8f4f8;
 | |
|     border-color: #1976d2;
 | |
| }
 | |
| 
 | |
| .pds-about-section ul {
 | |
|     list-style-type: none;
 | |
|     padding: 0;
 | |
| }
 | |
| 
 | |
| .pds-about-section li {
 | |
|     padding: 5px 0;
 | |
|     color: #666;
 | |
| }
 | |
| 
 | |
| 
 | |
| /* AT URI Modal Styles */
 | |
| .at-uri-modal-overlay {
 | |
|     position: fixed;
 | |
|     top: 0;
 | |
|     left: 0;
 | |
|     right: 0;
 | |
|     bottom: 0;
 | |
|     background-color: rgba(0, 0, 0, 0.5);
 | |
|     display: flex;
 | |
|     align-items: center;
 | |
|     justify-content: center;
 | |
|     z-index: 1000;
 | |
| }
 | |
| 
 | |
| .at-uri-modal-content {
 | |
|     background-color: white;
 | |
|     border-radius: 8px;
 | |
|     max-width: 800px;
 | |
|     max-height: 600px;
 | |
|     width: 90%;
 | |
|     height: 80%;
 | |
|     overflow: auto;
 | |
|     position: relative;
 | |
|     box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
 | |
| }
 | |
| 
 | |
| .at-uri-modal-close {
 | |
|     position: absolute;
 | |
|     top: 10px;
 | |
|     right: 10px;
 | |
|     background: none;
 | |
|     border: none;
 | |
|     font-size: 20px;
 | |
|     cursor: pointer;
 | |
|     z-index: 1001;
 | |
|     padding: 5px 10px;
 | |
| }
 | |
| 
 | |
| /* Loading states */
 | |
| .loading {
 | |
|     text-align: center;
 | |
|     padding: 20px;
 | |
|     color: #666;
 | |
| }
 | |
| 
 | |
| .error {
 | |
|     text-align: center;
 | |
|     padding: 20px;
 | |
|     color: #d32f2f;
 | |
|     background: #ffeaea;
 | |
|     border-radius: 4px;
 | |
|     margin: 10px 0;
 | |
| }
 | |
| 
 | |
| /* Responsive design */
 | |
| @media (max-width: 768px) {
 | |
|     .pds-search-form {
 | |
|         flex-direction: column;
 | |
|         align-items: stretch;
 | |
|     }
 | |
|     
 | |
|     .form-group {
 | |
|         flex-direction: column;
 | |
|         align-items: stretch;
 | |
|     }
 | |
|     
 | |
|     .form-group input {
 | |
|         width: 100%;
 | |
|         margin-bottom: 10px;
 | |
|     }
 | |
| }
 | |
| </style>
 | |
| 
 | |
| <script>
 | |
| // AT Protocol API functions
 | |
| const AT_PROTOCOL_CONFIG = {
 | |
|     primary: {
 | |
|         pds: 'https://syu.is',
 | |
|         plc: 'https://plc.syu.is',
 | |
|         bsky: 'https://bsky.syu.is',
 | |
|         web: 'https://web.syu.is'
 | |
|     },
 | |
|     fallback: {
 | |
|         pds: 'https://bsky.social',
 | |
|         plc: 'https://plc.directory',
 | |
|         bsky: 'https://public.api.bsky.app',
 | |
|         web: 'https://bsky.app'
 | |
|     }
 | |
| };
 | |
| 
 | |
| // Search user function
 | |
| async function searchUser() {
 | |
|     const handleInput = document.getElementById('handleInput');
 | |
|     const userInfo = document.getElementById('userInfo');
 | |
|     const collectionsList = document.getElementById('collectionsList');
 | |
|     const recordsList = document.getElementById('recordsList');
 | |
|     const searchButton = document.getElementById('searchButton');
 | |
|     
 | |
|     const input = handleInput.value.trim();
 | |
|     if (!input) {
 | |
|         alert('Handle nameまたはAT URIを入力してください');
 | |
|         return;
 | |
|     }
 | |
|     
 | |
|     searchButton.disabled = true;
 | |
|     searchButton.innerHTML = '<i class="fab fa-bluesky"></i>';
 | |
|     
 | |
|     try {
 | |
|         // Clear previous results
 | |
|         document.getElementById('userDidSection').style.display = 'none';
 | |
|         document.getElementById('collectionsSection').style.display = 'none';
 | |
|         document.getElementById('recordsSection').style.display = 'none';
 | |
|         collectionsList.innerHTML = '';
 | |
|         recordsList.innerHTML = '';
 | |
|         
 | |
|         // Check if input is AT URI
 | |
|         if (input.startsWith('at://')) {
 | |
|             // Parse AT URI to check if it's a full record or just a handle/collection
 | |
|             const uriParts = input.replace('at://', '').split('/').filter(part => part.length > 0);
 | |
|             
 | |
|             if (uriParts.length >= 3) {
 | |
|                 // Full AT URI with rkey - show in modal
 | |
|                 showAtUriModal(input);
 | |
|                 return;
 | |
|             } else if (uriParts.length === 1) {
 | |
|                 // Just handle in AT URI format (at://handle) - treat as regular handle
 | |
|                 const handle = uriParts[0];
 | |
|                 const userProfile = await resolveUserProfile(handle);
 | |
|                 
 | |
|                 if (userProfile.success) {
 | |
|                     displayUserDid(userProfile.data);
 | |
|                     await loadUserCollections(handle, userProfile.data.did);
 | |
|                 } else {
 | |
|                     alert('ユーザーが見つかりません: ' + userProfile.error);
 | |
|                 }
 | |
|                 return;
 | |
|             } else if (uriParts.length === 2) {
 | |
|                 // Collection level AT URI - load collection records
 | |
|                 const [repo, collection] = uriParts;
 | |
|                 
 | |
|                 try {
 | |
|                     // First resolve the repo to get handle if it's a DID
 | |
|                     let handle = repo;
 | |
|                     if (repo.startsWith('did:')) {
 | |
|                         // Try to resolve DID to handle - for now just use the DID
 | |
|                         handle = repo;
 | |
|                     }
 | |
|                     
 | |
|                     loadCollectionRecords(handle, collection, repo);
 | |
|                 } catch (error) {
 | |
|                     alert('コレクションの読み込みに失敗しました: ' + error.message);
 | |
|                 }
 | |
|                 return;
 | |
|             }
 | |
|         }
 | |
|         
 | |
|         // Handle regular handle search
 | |
|         const userProfile = await resolveUserProfile(input);
 | |
|         
 | |
|         if (userProfile.success) {
 | |
|             displayUserDid(userProfile.data);
 | |
|             await loadUserCollections(input, userProfile.data.did);
 | |
|         } else {
 | |
|             alert('ユーザーが見つかりません: ' + userProfile.error);
 | |
|         }
 | |
|     } catch (error) {
 | |
|         alert('エラーが発生しました: ' + error.message);
 | |
|     } finally {
 | |
|         searchButton.disabled = false;
 | |
|         searchButton.innerHTML = '<i class="fab fa-bluesky"></i>';
 | |
|     }
 | |
| }
 | |
| 
 | |
| // Resolve user profile
 | |
| async function resolveUserProfile(handle) {
 | |
|     try {
 | |
|         let response = null;
 | |
|         
 | |
|         // Try syu.is first
 | |
|         try {
 | |
|             response = await fetch(`${AT_PROTOCOL_CONFIG.primary.pds}/xrpc/com.atproto.repo.describeRepo?repo=${handle}`);
 | |
|         } catch (error) {
 | |
|             console.log('Failed to resolve from syu.is:', error);
 | |
|         }
 | |
|         
 | |
|         // If syu.is fails, try bsky.social
 | |
|         if (!response || !response.ok) {
 | |
|             response = await fetch(`${AT_PROTOCOL_CONFIG.fallback.pds}/xrpc/com.atproto.repo.describeRepo?repo=${handle}`);
 | |
|         }
 | |
|         
 | |
|         if (!response.ok) {
 | |
|             throw new Error('Failed to resolve handle');
 | |
|         }
 | |
|         
 | |
|         const repoData = await response.json();
 | |
|         
 | |
|         // Get profile data
 | |
|         const profileResponse = await fetch(`${AT_PROTOCOL_CONFIG.fallback.bsky}/xrpc/app.bsky.actor.getProfile?actor=${repoData.did}`);
 | |
|         const profileData = await profileResponse.json();
 | |
|         
 | |
|         return {
 | |
|             success: true,
 | |
|             data: {
 | |
|                 did: repoData.did,
 | |
|                 handle: profileData.handle,
 | |
|                 displayName: profileData.displayName,
 | |
|                 avatar: profileData.avatar,
 | |
|                 description: profileData.description,
 | |
|                 pds: repoData.didDoc.service.find(s => s.type === 'AtprotoPersonalDataServer')?.serviceEndpoint
 | |
|             }
 | |
|         };
 | |
|     } catch (error) {
 | |
|         return {
 | |
|             success: false,
 | |
|             error: error.message
 | |
|         };
 | |
|     }
 | |
| }
 | |
| 
 | |
| // Display user DID
 | |
| function displayUserDid(profile) {
 | |
|     document.getElementById('userPdsText').textContent = profile.pds || 'Unknown';
 | |
|     document.getElementById('userHandleText').textContent = profile.handle;
 | |
|     document.getElementById('userDidText').textContent = profile.did;
 | |
|     document.getElementById('userDidSection').style.display = 'block';
 | |
| }
 | |
| 
 | |
| // Load user collections
 | |
| async function loadUserCollections(handle, did) {
 | |
|     const collectionsList = document.getElementById('collectionsList');
 | |
|     
 | |
|     collectionsList.innerHTML = '<div class="loading">コレクションを読み込み中...</div>';
 | |
|     
 | |
|     try {
 | |
|         // Try to get collections from describeRepo
 | |
|         let response = await fetch(`${AT_PROTOCOL_CONFIG.primary.pds}/xrpc/com.atproto.repo.describeRepo?repo=${handle}`);
 | |
|         let usedPds = AT_PROTOCOL_CONFIG.primary.pds;
 | |
|         
 | |
|         // If syu.is fails, try bsky.social
 | |
|         if (!response.ok) {
 | |
|             response = await fetch(`${AT_PROTOCOL_CONFIG.fallback.pds}/xrpc/com.atproto.repo.describeRepo?repo=${handle}`);
 | |
|             usedPds = AT_PROTOCOL_CONFIG.fallback.pds;
 | |
|         }
 | |
|         
 | |
|         if (!response.ok) {
 | |
|             throw new Error('Failed to describe repository');
 | |
|         }
 | |
|         
 | |
|         const data = await response.json();
 | |
|         const collections = data.collections || [];
 | |
|         
 | |
|         // Display collections as AT URI links
 | |
|         collectionsList.innerHTML = '';
 | |
|         if (collections.length === 0) {
 | |
|             collectionsList.innerHTML = '<div class="error">コレクションが見つかりませんでした</div>';
 | |
|         } else {
 | |
|             
 | |
|             collections.forEach(collection => {
 | |
|                 const atUri = `at://${did}/${collection}/`;
 | |
|                 const collectionElement = document.createElement('a');
 | |
|                 collectionElement.className = 'at-uri-link';
 | |
|                 collectionElement.href = '#';
 | |
|                 collectionElement.textContent = atUri;
 | |
|                 collectionElement.onclick = (e) => {
 | |
|                     e.preventDefault();
 | |
|                     loadCollectionRecords(handle, collection, did);
 | |
|                     // Close collections and update toggle
 | |
|                     document.getElementById('collectionsList').style.display = 'none';
 | |
|                     document.getElementById('collectionsToggle').textContent = '[-] Collections';
 | |
|                 };
 | |
|                 collectionsList.appendChild(collectionElement);
 | |
|             });
 | |
|             
 | |
|             document.getElementById('collectionsSection').style.display = 'block';
 | |
|         }
 | |
|         
 | |
|     } catch (error) {
 | |
|         collectionsList.innerHTML = '<div class="error">コレクションの読み込みに失敗しました: ' + error.message + '</div>';
 | |
|         document.getElementById('collectionsSection').style.display = 'block';
 | |
|     }
 | |
| }
 | |
| 
 | |
| // Load collection records
 | |
| async function loadCollectionRecords(handle, collection, did) {
 | |
|     const recordsList = document.getElementById('recordsList');
 | |
|     
 | |
|     recordsList.innerHTML = '<div class="loading">レコードを読み込み中...</div>';
 | |
|     
 | |
|     try {
 | |
|         // Try with syu.is first
 | |
|         let response = await fetch(`${AT_PROTOCOL_CONFIG.primary.pds}/xrpc/com.atproto.repo.listRecords?repo=${handle}&collection=${collection}`);
 | |
|         let usedPds = AT_PROTOCOL_CONFIG.primary.pds;
 | |
|         
 | |
|         // If that fails, try with bsky.social
 | |
|         if (!response.ok) {
 | |
|             response = await fetch(`${AT_PROTOCOL_CONFIG.fallback.pds}/xrpc/com.atproto.repo.listRecords?repo=${handle}&collection=${collection}`);
 | |
|             usedPds = AT_PROTOCOL_CONFIG.fallback.pds;
 | |
|         }
 | |
|         
 | |
|         if (!response.ok) {
 | |
|             throw new Error('Failed to load records');
 | |
|         }
 | |
|         
 | |
|         const data = await response.json();
 | |
|         
 | |
|         // Display records as AT URI links
 | |
|         recordsList.innerHTML = '';
 | |
|         
 | |
|         // Add collection info for records
 | |
|         const collectionInfo = document.createElement('div');
 | |
|         collectionInfo.className = 'collection-info';
 | |
|         collectionInfo.innerHTML = `<strong>${collection}</strong>`;
 | |
|         recordsList.appendChild(collectionInfo);
 | |
|         
 | |
|         data.records.forEach(record => {
 | |
|             const atUri = record.uri;
 | |
|             const recordElement = document.createElement('a');
 | |
|             recordElement.className = 'at-uri-link';
 | |
|             recordElement.href = '#';
 | |
|             recordElement.textContent = atUri;
 | |
|             recordElement.onclick = (e) => {
 | |
|                 e.preventDefault();
 | |
|                 showAtUriModal(atUri);
 | |
|             };
 | |
|             recordsList.appendChild(recordElement);
 | |
|         });
 | |
|         
 | |
|         document.getElementById('recordsSection').style.display = 'block';
 | |
|         
 | |
|     } catch (error) {
 | |
|         recordsList.innerHTML = '<div class="error">レコードの読み込みに失敗しました: ' + error.message + '</div>';
 | |
|         document.getElementById('recordsSection').style.display = 'block';
 | |
|     }
 | |
| }
 | |
| 
 | |
| // Show AT URI modal
 | |
| function showAtUriModal(uri) {
 | |
|     const modal = document.getElementById('atUriModal');
 | |
|     const content = document.getElementById('atUriContent');
 | |
|     
 | |
|     content.innerHTML = '<div class="loading">レコードを読み込み中...</div>';
 | |
|     modal.style.display = 'flex';
 | |
|     
 | |
|     // Load record data
 | |
|     loadAtUriRecord(uri, content);
 | |
| }
 | |
| 
 | |
| // Load AT URI record
 | |
| async function loadAtUriRecord(uri, contentElement) {
 | |
|     try {
 | |
|         const parts = uri.replace('at://', '').split('/');
 | |
|         const repo = parts[0];
 | |
|         const collection = parts[1];
 | |
|         const rkey = parts[2];
 | |
|         
 | |
|         // Try with syu.is first
 | |
|         let response = await fetch(`${AT_PROTOCOL_CONFIG.primary.pds}/xrpc/com.atproto.repo.getRecord?repo=${repo}&collection=${collection}&rkey=${rkey}`);
 | |
|         
 | |
|         // If that fails, try with bsky.social
 | |
|         if (!response.ok) {
 | |
|             response = await fetch(`${AT_PROTOCOL_CONFIG.fallback.pds}/xrpc/com.atproto.repo.getRecord?repo=${repo}&collection=${collection}&rkey=${rkey}`);
 | |
|         }
 | |
|         
 | |
|         if (!response.ok) {
 | |
|             throw new Error('Failed to load record');
 | |
|         }
 | |
|         
 | |
|         const data = await response.json();
 | |
|         
 | |
|         contentElement.innerHTML = `
 | |
|             <div style="padding: 20px;">
 | |
|                 <h3>AT URI Record</h3>
 | |
|                 <div style="font-family: monospace; font-size: 14px; color: #666; margin-bottom: 20px; word-break: break-all;">
 | |
|                     ${uri}
 | |
|                 </div>
 | |
|                 <div style="font-size: 12px; color: #999; margin-bottom: 20px;">
 | |
|                     Repo: ${repo} | Collection: ${collection} | RKey: ${rkey}
 | |
|                 </div>
 | |
|                 <h4>Record Data</h4>
 | |
|                 <pre style="background: #f5f5f5; padding: 15px; border-radius: 4px; overflow: auto;">${JSON.stringify(data, null, 2)}</pre>
 | |
|             </div>
 | |
|         `;
 | |
|     } catch (error) {
 | |
|         contentElement.innerHTML = `
 | |
|             <div style="padding: 20px; color: red;">
 | |
|                 <strong>Error:</strong> ${error.message}
 | |
|                 <div style="margin-top: 10px; font-size: 12px;">
 | |
|                     <strong>URI:</strong> ${uri}
 | |
|                 </div>
 | |
|             </div>
 | |
|         `;
 | |
|     }
 | |
| }
 | |
| 
 | |
| // Close AT URI modal
 | |
| function closeAtUriModal(event) {
 | |
|     const modal = document.getElementById('atUriModal');
 | |
|     if (event && event.target !== modal) {
 | |
|         return;
 | |
|     }
 | |
|     modal.style.display = 'none';
 | |
| }
 | |
| 
 | |
| // Initialize AT URI click handlers
 | |
| document.addEventListener('DOMContentLoaded', function() {
 | |
|     // Add click handlers to existing AT URIs
 | |
|     document.querySelectorAll('.at-uri').forEach(element => {
 | |
|         element.addEventListener('click', function() {
 | |
|             const uri = this.getAttribute('data-at-uri');
 | |
|             showAtUriModal(uri);
 | |
|         });
 | |
|     });
 | |
|     
 | |
|     // ESC key to close modal
 | |
|     document.addEventListener('keydown', function(event) {
 | |
|         if (event.key === 'Escape') {
 | |
|             closeAtUriModal();
 | |
|         }
 | |
|     });
 | |
|     
 | |
|     // Enter key to search
 | |
|     document.getElementById('handleInput').addEventListener('keydown', function(event) {
 | |
|         if (event.key === 'Enter') {
 | |
|             searchUser();
 | |
|         }
 | |
|     });
 | |
|     
 | |
| });
 | |
| 
 | |
| // Toggle collections visibility
 | |
| function toggleCollections() {
 | |
|     const collectionsList = document.getElementById('collectionsList');
 | |
|     const toggleButton = document.getElementById('collectionsToggle');
 | |
|     
 | |
|     if (collectionsList.style.display === 'none') {
 | |
|         collectionsList.style.display = 'block';
 | |
|         toggleButton.textContent = '[-] Collections';
 | |
|     } else {
 | |
|         collectionsList.style.display = 'none';
 | |
|         toggleButton.textContent = '[+] Collections';
 | |
|     }
 | |
| }
 | |
| </script>
 | |
| {% endblock %} |