2026年10月5日 星期一

AM/FM 廣播搜尋器 (AM/FM Radio Finder)

 AM/FM 廣播搜尋器 (AM/FM Radio Finder)




無線調幅AM廣播電臺頻率及發射機位址紀錄

資料資源描述
無線調幅AM廣播電臺頻率及發射機位址資料表

無線調頻FM廣播電臺頻率及發射機位址紀錄

資料資源描述
無線調頻FM廣播電臺頻率及發射機位址紀錄

Python程式


import json

import urllib.request

import urllib.parse

import tkinter as tk

from tkinter import ttk, messagebox

import threading


# NCC Open Data URL

AM_URL = "https://api.ncc.gov.tw/uploaddowndoc?file=datagov/1556563683676434432.json&filedisplay=1556563683676434432_調幅AM_115_7_3.json&flag=doc"

FM_URL = "https://api.ncc.gov.tw/uploaddowndoc?file=datagov/1556562735747244032.json&filedisplay=1556562735747244032_調頻FM_115_7_3.json&flag=doc"


# 台灣縣市清單

COUNTIES = [

    "全部縣市", "基隆市", "臺北市", "新北市", "桃園市", "新竹市", "新竹縣", "苗栗縣",

    "臺中市", "彰化縣", "南投縣", "雲林縣", "嘉義市", "嘉義縣", "臺南市", "高雄市",

    "屏東縣", "宜蘭縣", "花蓮縣", "臺東縣", "澎湖縣", "金門縣", "連江縣"

]


class RadioQueryApp:

    def __init__(self, root):

        self.root = root

        self.root.title("台灣各縣市 AM/FM 廣播電台查詢系統")

        self.root.geometry("900x550")


        self.am_data = []

        self.fm_data = []


        self.create_widgets()

        

        # 背景下載資料

        threading.Thread(target=self.load_data, daemon=True).start()


    def create_widgets(self):

        # 控制區

        ctrl_frame = ttk.Frame(self.root, padding=10)

        ctrl_frame.pack(fill=tk.X)


        # 縣市選擇

        ttk.Label(ctrl_frame, text="選擇縣市:").pack(side=tk.LEFT, padx=5)

        self.city_cb = ttk.Combobox(ctrl_frame, values=COUNTIES, state="readonly", width=12)

        self.city_cb.current(0)

        self.city_cb.pack(side=tk.LEFT, padx=5)

        self.city_cb.bind("<<ComboboxSelected>>", lambda e: self.filter_data())


        # 廣播類別

        ttk.Label(ctrl_frame, text="廣播類別:").pack(side=tk.LEFT, padx=(15, 5))

        self.type_cb = ttk.Combobox(ctrl_frame, values=["全部", "AM (調幅)", "FM (調頻)"], state="readonly", width=10)

        self.type_cb.current(0)

        self.type_cb.pack(side=tk.LEFT, padx=5)

        self.type_cb.bind("<<ComboboxSelected>>", lambda e: self.filter_data())


        # 關鍵字

        ttk.Label(ctrl_frame, text="關鍵字:").pack(side=tk.LEFT, padx=(15, 5))

        self.search_entry = ttk.Entry(ctrl_frame, width=15)

        self.search_entry.pack(side=tk.LEFT, padx=5)

        self.search_entry.bind("<KeyRelease>", lambda e: self.filter_data())


        # 狀態顯示

        self.status_label = ttk.Label(self.root, text="資料載入中,請稍候...", font=("Microsoft JhengHei", 10, "italic"))

        self.status_label.pack(fill=tk.X, padx=10)


        # 表格 (Treeview)

        table_frame = ttk.Frame(self.root, padding=10)

        table_frame.pack(fill=tk.BOTH, expand=True)


        columns = ("type", "name", "freq", "power", "area")

        self.tree = ttk.Treeview(table_frame, columns=columns, show="headings")


        self.tree.heading("type", text="類別")

        self.tree.heading("name", text="電台/發射台名稱")

        self.tree.heading("freq", text="頻率")

        self.tree.heading("power", text="功率")

        self.tree.heading("area", text="設置地點/播音區域")


        self.tree.column("type", width=80, anchor=tk.CENTER)

        self.tree.column("name", width=220, anchor=tk.W)

        self.tree.column("freq", width=110, anchor=tk.CENTER)

        self.tree.column("power", width=100, anchor=tk.CENTER)

        self.tree.column("area", width=340, anchor=tk.W)


        # 捲軸

        scrollbar = ttk.Scrollbar(table_frame, orient=tk.VERTICAL, command=self.tree.yview)

        self.tree.configure(yscroll=scrollbar.set)

        

        self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)


    def safe_quote_url(self, url):

        """處理網址內的中文轉碼"""

        parsed = urllib.parse.urlparse(url)

        encoded_path = urllib.parse.quote(parsed.path)

        encoded_query = urllib.parse.quote(parsed.query, safe='=&')

        return urllib.parse.urlunparse((

            parsed.scheme, parsed.netloc, encoded_path,

            parsed.params, encoded_query, parsed.fragment

        ))


    def fetch_json(self, url):

        """獲取 JSON 資料"""

        encoded_url = self.safe_quote_url(url)

        req = urllib.request.Request(encoded_url, headers={'User-Agent': 'Mozilla/5.0'})

        with urllib.request.urlopen(req) as response:

            data = response.read().decode('utf-8-sig')

            return json.loads(data)


    def find_val(self, item, keywords, default="-"):

        """根據可能的 Key 關鍵字尋找 JSON 裡的欄位值"""

        for k, v in item.items():

            for kw in keywords:

                if kw in str(k):

                    return str(v).strip()

        return default


    def parse_records(self, raw_data, radio_type):

        """相容多種 JSON 結構與動態欄位解析"""

        records = []

        # 判斷 JSON 格式外層

        if isinstance(raw_data, list):

            items = raw_data

        elif isinstance(raw_data, dict):

            items = raw_data.get("result", {}).get("records", raw_data.get("data", []))

        else:

            items = []


        for item in items:

            if not isinstance(item, dict):

                continue

            

            # 多重比對常見欄位名稱

            name = self.find_val(item, ["事業名稱", "電台名稱", "台名", "廣播事業", "名稱"])

            freq = self.find_val(item, ["頻率", "MHz", "kHz"])

            power = self.find_val(item, ["功率", "發射功率"])

            area = self.find_val(item, ["設置地點", "發射台位置", "播音區域", "地址", "縣市", "設置區域"])


            # 將整個物件所有 value 轉成字串,用於廣泛比對

            full_text = " ".join([str(v) for v in item.values()])


            # 補全頻率單位

            unit = 'kHz' if radio_type == 'AM' else 'MHz'

            formatted_freq = f"{freq} {unit}" if freq != "-" and unit not in freq else freq


            records.append({

                "type": radio_type,

                "name": name,

                "freq": formatted_freq,

                "power": power,

                "area": area,

                "full_text": full_text

            })

        return records


    def load_data(self):

        try:

            am_raw = self.fetch_json(AM_URL)

            fm_raw = self.fetch_json(FM_URL)


            self.am_data = self.parse_records(am_raw, "AM")

            self.fm_data = self.parse_records(fm_raw, "FM")


            msg = f"載入完成!共 AM {len(self.am_data)} 筆,FM {len(self.fm_data)} 筆。"

            self.root.after(0, self.on_data_loaded, True, msg)

        except Exception as e:

            self.root.after(0, self.on_data_loaded, False, f"資料載入失敗: {str(e)}")


    def on_data_loaded(self, success, message):

        self.status_label.config(text=message)

        if success:

            self.filter_data()

        else:

            messagebox.showerror("錯誤", message)


    def filter_data(self):

        """資料篩選與繪製"""

        selected_city = self.city_cb.get()

        selected_type = self.type_cb.get()

        keyword = self.search_entry.get().strip().lower()


        # 清空清單

        for row in self.tree.get_children():

            self.tree.delete(row)


        dataset = []

        if selected_type in ["全部", "AM (調幅)"]:

            dataset.extend(self.am_data)

        if selected_type in ["全部", "FM (調頻)"]:

            dataset.extend(self.fm_data)


        count = 0

        for item in dataset:

            # 縣市比對(同時比對「臺」與「台」)

            city_match = True

            if selected_city != "全部縣市":

                city_1 = selected_city

                city_2 = selected_city.replace("臺", "台") if "臺" in selected_city else selected_city.replace("台", "臺")

                city_match = (city_1 in item["full_text"]) or (city_2 in item["full_text"])


            # 關鍵字比對

            keyword_match = True

            if keyword:

                keyword_match = keyword in item["full_text"].lower()


            if city_match and keyword_match:

                self.tree.insert("", tk.END, values=(

                    item["type"],

                    item["name"],

                    item["freq"],

                    item["power"],

                    item["area"]

                ))

                count += 1


        self.status_label.config(text=f"查詢結果:共 {count} 筆資料")


if __name__ == "__main__":

    root = tk.Tk()

    app = RadioQueryApp(root)

    root.mainloop()


 Flutter 專案的 pubspec.yaml

name: am_fm_app
description: "A new Flutter project."
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev

# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1

environment:
  sdk: ^3.13.5

# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
  flutter:
    sdk: flutter
  http: ^1.2.0 # 加入 HTTP 套件
 
  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.8

dev_dependencies:
  flutter_test:
    sdk: flutter

  # The "flutter_lints" package below contains a set of recommended lints to
  # encourage good coding practices. The lint set provided by the package is
  # activated in the `analysis_options.yaml` file located at the root of your
  # package. See that file for information about deactivating specific lint
  # rules and activating additional ones.
  flutter_lints: ^6.0.0

# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec

# The following section is specific to Flutter packages.
flutter:

  # The following line ensures that the Material Icons font is
  # included with your application, so that you can use the icons in
  # the material Icons class.
  uses-material-design: true

  # To add assets to your application, add an assets section, like this:
  # assets:
  #   - images/a_dot_burr.jpeg
  #   - images/a_dot_ham.jpeg

  # An image asset can refer to one or more resolution-specific "variants", see
  # https://flutter.dev/to/resolution-aware-images

  # For details regarding adding assets from package dependencies, see
  # https://flutter.dev/to/asset-from-package

  # To add custom fonts to your application, add a fonts section here,
  # in this "flutter" section. Each entry in this list should have a
  # "family" key with the font family name, and a "fonts" key with a
  # list giving the asset and other descriptors for the font. For
  # example:
  # fonts:
  #   - family: Schyler
  #     fonts:
  #       - asset: fonts/Schyler-Regular.ttf
  #       - asset: fonts/Schyler-Italic.ttf
  #         style: italic
  #   - family: Trajan Pro
  #     fonts:
  #       - asset: fonts/TrajanPro.ttf
  #       - asset: fonts/TrajanPro_Bold.ttf
  #         weight: 700
  #
  # For details regarding fonts from package dependencies,
  # see https://flutter.dev/to/font-from-package

main.dart 完整程式碼

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

void main() {
  runApp(const RadioApp());
}

class RadioApp extends StatelessWidget {
  const RadioApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '台灣各縣市 AM/FM 廣播電台查詢系統',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
        useMaterial3: true,
      ),
      home: const RadioHomePage(),
    );
  }
}

class RadioStation {
  final String type;
  final String name;
  final String freq;
  final String power;
  final String area;
  final String fullText;

  RadioStation({
    required this.type,
    required this.name,
    required this.freq,
    required this.power,
    required this.area,
    required this.fullText,
  });
}

class RadioHomePage extends StatefulWidget {
  const RadioHomePage({super.key});

  @override
  State<RadioHomePage> createState() => _RadioHomePageState();
}

class _RadioHomePageState extends State<RadioHomePage> {
  // NCC Open Data URL
  final String amUrl =
      "https://api.ncc.gov.tw/uploaddowndoc?file=datagov/1556563683676434432.json&filedisplay=1556563683676434432_調幅AM_115_7_3.json&flag=doc";
  final String fmUrl =
      "https://api.ncc.gov.tw/uploaddowndoc?file=datagov/1556562735747244032.json&filedisplay=1556562735747244032_調頻FM_115_7_3.json&flag=doc";

  final List<String> counties = [
    "全部縣市",
    "基隆市",
    "臺北市",
    "新北市",
    "桃園市",
    "新竹市",
    "新竹縣",
    "苗栗縣",
    "臺中市",
    "彰化縣",
    "南投縣",
    "雲林縣",
    "嘉義市",
    "嘉義縣",
    "臺南市",
    "高雄市",
    "屏東縣",
    "宜蘭縣",
    "花蓮縣",
    "臺東縣",
    "澎湖縣",
    "金門縣",
    "連江縣",
  ];

  final List<String> radioTypes = ["全部", "AM (調幅)", "FM (調頻)"];

  String selectedCity = "全部縣市";
  String selectedType = "全部";
  String keyword = "";

  List<RadioStation> amData = [];
  List<RadioStation> fmData = [];
  List<RadioStation> filteredData = [];

  bool isLoading = true;
  String statusMessage = "資料載入中...";

  @override
  void initState() {
    super.initState();
    loadData();
  }

  // 尋找 JSON 中的動態 Key
  String findVal(
    Map<String, dynamic> item,
    List<String> keywords, {
    String defaultValue = "-",
  }) {
    for (var entry in item.entries) {
      for (var kw in keywords) {
        if (entry.key.toString().contains(kw)) {
          return entry.value?.toString().trim() ?? defaultValue;
        }
      }
    }
    return defaultValue;
  }

  // 解析 JSON
  List<RadioStation> parseRecords(dynamic rawData, String radioType) {
    List<RadioStation> records = [];
    List items = [];

    if (rawData is List) {
      items = rawData;
    } else if (rawData is Map) {
      items = rawData["result"]?["records"] ?? rawData["data"] ?? [];
    }

    for (var item in items) {
      if (item is! Map<String, dynamic>) continue;

      String name = findVal(item, ["事業名稱", "電台名稱", "台名", "廣播事業", "名稱"]);
      String freq = findVal(item, ["頻率", "MHz", "kHz"]);
      String power = findVal(item, ["功率", "發射功率"]);
      String area = findVal(item, [
        "設置地點",
        "發射台位置",
        "播音區域",
        "地址",
        "縣市",
        "設置區域",
      ]);

      String fullText = item.values.map((v) => v?.toString() ?? "").join(" ");
      String unit = radioType == 'AM' ? 'kHz' : 'MHz';
      String formattedFreq = (freq != "-" && !freq.contains(unit))
          ? "$freq $unit"
          : freq;

      records.add(
        RadioStation(
          type: radioType,
          name: name,
          freq: formattedFreq,
          power: power,
          area: area,
          fullText: fullText,
        ),
      );
    }
    return records;
  }

  // 抓取 API 資料
  Future<void> loadData() async {
    try {
      final responses = await Future.wait([
        http.get(Uri.parse(amUrl), headers: {'User-Agent': 'Mozilla/5.0'}),
        http.get(Uri.parse(fmUrl), headers: {'User-Agent': 'Mozilla/5.0'}),
      ]);

      if (responses[0].statusCode == 200 && responses[1].statusCode == 200) {
        final amRaw = jsonDecode(utf8.decode(responses[0].bodyBytes));
        final fmRaw = jsonDecode(utf8.decode(responses[1].bodyBytes));

        setState(() {
          amData = parseRecords(amRaw, "AM");
          fmData = parseRecords(fmRaw, "FM");
          isLoading = false;
          filterData();
        });
      } else {
        throw Exception("HTTP 請求失敗");
      }
    } catch (e) {
      setState(() {
        isLoading = false;
        statusMessage = "資料載入失敗,請檢查網路連線。";
      });
    }
  }

  // 資料過濾
  void filterData() {
    List<RadioStation> dataset = [];
    if (selectedType == "全部" || selectedType == "AM (調幅)") {
      dataset.addAll(amData);
    }
    if (selectedType == "全部" || selectedType == "FM (調頻)") {
      dataset.addAll(fmData);
    }

    setState(() {
      filteredData = dataset.where((item) {
        bool cityMatch = true;
        if (selectedCity != "全部縣市") {
          String city1 = selectedCity;
          String city2 = selectedCity.contains("臺")
              ? selectedCity.replaceAll("臺", "台")
              : selectedCity.replaceAll("台", "臺");
          cityMatch =
              item.fullText.contains(city1) || item.fullText.contains(city2);
        }

        bool keywordMatch = true;
        if (keyword.isNotEmpty) {
          keywordMatch = item.fullText.toLowerCase().contains(
            keyword.toLowerCase(),
          );
        }

        return cityMatch && keywordMatch;
      }).toList();

      statusMessage = "找到 ${filteredData.length} 筆電台資料";
    });
  }

  // 顯示詳細資料彈窗
  void showDetailsDialog(RadioStation station) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Row(
          children: [
            Chip(
              label: Text(
                station.type,
                style: const TextStyle(
                  color: Colors.white,
                  fontWeight: FontWeight.bold,
                ),
              ),
              backgroundColor: station.type == "AM"
                  ? Colors.orange
                  : Colors.indigo,
              visualDensity: VisualDensity.compact,
            ),
            const SizedBox(width: 8),
            Expanded(
              child: Text(
                station.name,
                style: const TextStyle(fontSize: 18),
                overflow: TextOverflow.ellipsis,
              ),
            ),
          ],
        ),
        content: SingleChildScrollView(
          child: ListBody(
            children: [
              _detailRow("廣播頻率", station.freq),
              _detailRow("發射功率", station.power),
              _detailRow("設置地點/播音區域", station.area),
            ],
          ),
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text("關閉"),
          ),
        ],
      ),
    );
  }

  Widget _detailRow(String label, String value) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 6.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            label,
            style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
          ),
          const SizedBox(height: 2),
          Text(
            value,
            style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500),
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("台灣廣播電台查詢"),
        centerTitle: true,
        backgroundColor: Theme.of(context).colorScheme.primaryContainer,
      ),
      body: SafeArea(
        child: Column(
          children: [
            // 上方選擇面板 (專為手機優化)
            Container(
              padding: const EdgeInsets.all(10.0),
              color: Colors.grey.shade50,
              child: Column(
                children: [
                  Row(
                    children: [
                      // 縣市選單
                      Expanded(
                        child: DropdownButtonFormField<String>(
                          isExpanded: true,
                          value: selectedCity,
                          decoration: const InputDecoration(
                            labelText: "縣市",
                            contentPadding: EdgeInsets.symmetric(
                              horizontal: 10,
                              vertical: 8,
                            ),
                            border: OutlineInputBorder(),
                          ),
                          items: counties.map((city) {
                            return DropdownMenuItem(
                              value: city,
                              child: Text(
                                city,
                                overflow: TextOverflow.ellipsis,
                              ),
                            );
                          }).toList(),
                          onChanged: (val) {
                            if (val != null) {
                              selectedCity = val;
                              filterData();
                            }
                          },
                        ),
                      ),
                      const SizedBox(width: 8),
                      // 類別選單
                      Expanded(
                        child: DropdownButtonFormField<String>(
                          isExpanded: true,
                          value: selectedType,
                          decoration: const InputDecoration(
                            labelText: "類別",
                            contentPadding: EdgeInsets.symmetric(
                              horizontal: 10,
                              vertical: 8,
                            ),
                            border: OutlineInputBorder(),
                          ),
                          items: radioTypes.map((type) {
                            return DropdownMenuItem(
                              value: type,
                              child: Text(
                                type,
                                overflow: TextOverflow.ellipsis,
                              ),
                            );
                          }).toList(),
                          onChanged: (val) {
                            if (val != null) {
                              selectedType = val;
                              filterData();
                            }
                          },
                        ),
                      ),
                    ],
                  ),
                  const SizedBox(height: 8),
                  // 搜尋輸入框
                  SizedBox(
                    height: 45,
                    child: TextField(
                      decoration: const InputDecoration(
                        hintText: "搜尋電台名稱或地址",
                        prefixIcon: Icon(Icons.search, size: 20),
                        contentPadding: EdgeInsets.symmetric(
                          vertical: 0,
                          horizontal: 10,
                        ),
                        border: OutlineInputBorder(),
                      ),
                      onChanged: (val) {
                        keyword = val.trim();
                        filterData();
                      },
                    ),
                  ),
                ],
              ),
            ),

            // 統計條
            Container(
              width: double.infinity,
              padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
              color: Theme.of(context).colorScheme.primaryContainer
                  .withOpacity(0.4),
              child: Text(
                statusMessage,
                style: const TextStyle(
                  fontSize: 13,
                  fontWeight: FontWeight.bold,
                ),
              ),
            ),

            // 電台列表 (手機卡片視圖)
            Expanded(
              child: isLoading
                  ? const Center(child: CircularProgressIndicator())
                  : filteredData.isEmpty
                  ? const Center(child: Text("沒有找到符合條件的廣播電台"))
                  : ListView.builder(
                      padding: const EdgeInsets.all(8),
                      itemCount: filteredData.length,
                      itemBuilder: (context, index) {
                        final station = filteredData[index];
                        return Card(
                          margin: const EdgeInsets.symmetric(vertical: 4),
                          elevation: 1.5,
                          child: ListTile(
                            contentPadding: const EdgeInsets.symmetric(
                              horizontal: 12,
                              vertical: 4,
                            ),
                            leading: CircleAvatar(
                              radius: 20,
                              backgroundColor: station.type == "AM"
                                  ? Colors.orange
                                  : Colors.indigo,
                              child: Text(
                                station.type,
                                style: const TextStyle(
                                  color: Colors.white,
                                  fontSize: 12,
                                  fontWeight: FontWeight.bold,
                                ),
                              ),
                            ),
                            title: Text(
                              station.name,
                              style: const TextStyle(
                                fontWeight: FontWeight.bold,
                                fontSize: 15,
                              ),
                              maxLines: 1,
                              overflow: TextOverflow.ellipsis,
                            ),
                            subtitle: Padding(
                              padding: const EdgeInsets.only(top: 4.0),
                              child: Text(
                                station.area,
                                maxLines: 1,
                                overflow: TextOverflow.ellipsis,
                                style: TextStyle(
                                  fontSize: 12,
                                  color: Colors.grey.shade700,
                                ),
                              ),
                            ),
                            trailing: Column(
                              mainAxisAlignment: MainAxisAlignment.center,
                              crossAxisAlignment: CrossAxisAlignment.end,
                              children: [
                                Text(
                                  station.freq,
                                  style: TextStyle(
                                    fontSize: 13,
                                    fontWeight: FontWeight.bold,
                                    color: Theme.of(context)
                                        .colorScheme
                                        .primary,
                                  ),
                                ),
                                const SizedBox(height: 2),
                                const Icon(
                                  Icons.chevron_right,
                                  size: 16,
                                  color: Colors.grey,
                                ),
                              ],
                            ),
                            onTap: () => showDetailsDialog(station),
                          ),
                        );
                      },
                    ),
            ),
          ],
        ),
      ),
    );
  }
}






沒有留言:

張貼留言

AM/FM 廣播搜尋器 (AM/FM Radio Finder)

  AM/FM 廣播搜尋器 (AM/FM Radio Finder) 無線調幅AM廣播電臺頻率及發射機位址紀錄 資料下載網址 https://api.ncc.gov.tw/uploaddowndoc?file=datagov/1556563683676434432.json...